Skip to content

Commit ba43e34

Browse files
committed
Add tests and docs for bit_map element
1 parent 5cf0f2b commit ba43e34

6 files changed

Lines changed: 240 additions & 17 deletions

File tree

core/src/main/java/com/github/flexca/enot/core/parser/EnotParser.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import com.github.flexca.enot.core.EnotContext;
44
import com.github.flexca.enot.core.exception.EnotParsingException;
5-
import com.github.flexca.enot.core.expression.ConditionExpressionParser;
65
import com.github.flexca.enot.core.registry.EnotElementBodyResolver;
76
import com.github.flexca.enot.core.registry.EnotElementSpecification;
8-
import com.github.flexca.enot.core.registry.EnotRegistry;
97
import com.github.flexca.enot.core.registry.EnotTypeSpecification;
108
import com.github.flexca.enot.core.element.EnotElement;
119
import com.github.flexca.enot.core.element.attribute.EnotAttribute;
@@ -156,6 +154,11 @@ private Optional<EnotElement> parseElement(ObjectNode jsonElement, String parent
156154
elementBody.ifPresent(element::setBody);
157155
} else {
158156
try {
157+
158+
String compositeIdentifier = bodyResolver.getUniqueCompositeIdentifier(element);
159+
if (StringUtils.isNotBlank(compositeIdentifier)) {
160+
// TODO: detect cyclic dependency
161+
}
159162
element.setBody(bodyResolver.resolveBody(element, enotContext));
160163
} catch(Exception e) {
161164
jsonErrors.add(EnotJsonError.of(parentPath + "/" + ENOT_ELEMENT_BODY_NAME,

core/src/main/java/com/github/flexca/enot/core/registry/EnotElementBodyResolver.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,61 @@
33
import com.github.flexca.enot.core.EnotContext;
44
import com.github.flexca.enot.core.element.EnotElement;
55

6+
/**
7+
* Strategy interface for resolving an element's body dynamically at parse time.
8+
*
9+
* <p>Certain element types — most notably {@code system/reference} — do not carry
10+
* their body inline in the template. Instead, the body must be fetched or computed
11+
* when the element is first encountered during parsing. Implementations of this
12+
* interface encapsulate that resolution logic.
13+
*
14+
* <p>The interface also participates in <b>cyclic-dependency detection</b>. Because
15+
* body resolution may itself trigger further parsing (e.g. a reference that includes
16+
* another template which in turn references the first), the parser tracks a set of
17+
* identifiers that are currently being resolved. Implementations that can participate
18+
* in such cycles must return a non-{@code null} identifier from
19+
* {@link #getUniqueCompositeIdentifier}; the parser uses this value to detect and
20+
* report cycles before a {@link StackOverflowError} can occur.
21+
*/
622
public interface EnotElementBodyResolver {
723

24+
/**
25+
* Returns a unique identifier for the given element that the parser uses to
26+
* detect cyclic dependencies during resolution.
27+
*
28+
* <p>The returned string must uniquely identify the specific external resource
29+
* or body this element resolves to. For {@code system/reference} elements this
30+
* is typically a composite of the reference type and identifier, for example
31+
* {@code "test_resources:json/asn1/rfc/san/san-dns.json"}.
32+
*
33+
* <p>If cycle detection is not applicable for this resolver implementation,
34+
* return {@code null}. The parser will skip cycle detection for that element.
35+
*
36+
* @param element the element whose resolution identity is needed
37+
* @return a non-empty string that uniquely identifies the resolution target,
38+
* or {@code null} if cycle detection is not required
39+
*/
40+
String getUniqueCompositeIdentifier(EnotElement element);
41+
42+
/**
43+
* Resolves and returns the body for the given element at parse time.
44+
*
45+
* <p>This method is called by the parser when it encounters an element whose
46+
* {@link com.github.flexca.enot.core.types.system.SystemKind} has a registered
47+
* body resolver. The returned value replaces the element's inline body and is
48+
* stored directly on the parsed {@link EnotElement}.
49+
*
50+
* <p>Implementations may perform I/O, invoke further parsing via
51+
* {@link EnotContext#getEnotParser()}, or compute the body from the element's
52+
* attributes. When further parsing is involved, cyclic-dependency detection via
53+
* {@link #getUniqueCompositeIdentifier} should be implemented to prevent
54+
* infinite recursion.
55+
*
56+
* @param element the element whose body is to be resolved
57+
* @param enotContext the current parsing context, providing access to the
58+
* registry, parser, serializer, and expression engine
59+
* @return the resolved body; the concrete type depends on the element kind
60+
* (e.g. {@code List<EnotElement>} for {@code system/reference})
61+
*/
862
Object resolveBody(EnotElement element, EnotContext enotContext);
963
}

core/src/main/java/com/github/flexca/enot/core/types/system/SystemReferenceBodyResolver.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,43 @@
77
import com.github.flexca.enot.core.registry.EnotElementReferenceResolver;
88
import com.github.flexca.enot.core.types.system.attribute.SystemAttribute;
99

10+
/**
11+
* {@link EnotElementBodyResolver} for {@code system/reference} elements.
12+
*
13+
* <p>Resolves the body by delegating to the {@link com.github.flexca.enot.core.registry.EnotElementReferenceResolver}
14+
* registered in the {@link com.github.flexca.enot.core.registry.EnotRegistry} for the element's
15+
* {@code reference_type} attribute. The resolver is looked up at parse time, so all reference
16+
* types must be registered before parsing begins.
17+
*
18+
* <p>The composite identifier returned by {@link #getUniqueCompositeIdentifier} is
19+
* {@code "<reference_type>:<reference_identifier>"}, which makes cycles across different
20+
* reference type implementations distinguishable.
21+
*/
1022
public class SystemReferenceBodyResolver implements EnotElementBodyResolver {
1123

24+
@Override
25+
public String getUniqueCompositeIdentifier(EnotElement element) {
26+
27+
String referenceType = getReferenceType(element);
28+
String referenceIdentifier = getReferenceIdentifier(element);
29+
return referenceType + ":" + referenceIdentifier;
30+
}
31+
1232
@Override
1333
public Object resolveBody(EnotElement element, EnotContext enotContext) {
1434

35+
String referenceType = getReferenceType(element);
36+
EnotElementReferenceResolver referenceResolver = enotContext.getEnotRegistry().getElementReferenceResolver(referenceType);
37+
if(referenceResolver == null) {
38+
throw new EnotInvalidArgumentException("no registered EnotElementReferenceResolver found for reference type: "
39+
+ referenceType);
40+
}
41+
String referenceIdentifier = getReferenceIdentifier(element);
42+
return referenceResolver.resolve(referenceIdentifier, enotContext);
43+
}
44+
45+
private String getReferenceType(EnotElement element) {
46+
1547
Object referenceType = element.getAttribute(SystemAttribute.REFERENCE_TYPE);
1648
if (referenceType == null) {
1749
throw new EnotInvalidArgumentException("attribute " + SystemAttribute.REFERENCE_TYPE.getName()
@@ -20,12 +52,10 @@ public Object resolveBody(EnotElement element, EnotContext enotContext) {
2052
if (!(referenceType instanceof String)) {
2153
throw new EnotInvalidArgumentException("attribute " + SystemAttribute.REFERENCE_TYPE.getName() + " must be string");
2254
}
55+
return (String) referenceType;
56+
}
2357

24-
EnotElementReferenceResolver referenceResolver = enotContext.getEnotRegistry().getElementReferenceResolver((String) referenceType);
25-
if(referenceResolver == null) {
26-
throw new EnotInvalidArgumentException("no registered EnotElementReferenceResolver found for reference type: "
27-
+ referenceType);
28-
}
58+
private String getReferenceIdentifier(EnotElement element) {
2959

3060
Object referenceIdentifier = element.getAttribute(SystemAttribute.REFERENCE_IDENTIFIER);
3161
if (referenceIdentifier == null) {
@@ -36,7 +66,6 @@ public Object resolveBody(EnotElement element, EnotContext enotContext) {
3666
throw new EnotInvalidArgumentException("attribute " + SystemAttribute.REFERENCE_IDENTIFIER.getName()
3767
+ " must be string");
3868
}
39-
40-
return referenceResolver.resolve((String) referenceIdentifier, enotContext);
69+
return (String) referenceIdentifier;
4170
}
4271
}

core/src/main/java/com/github/flexca/enot/core/types/system/serializer/SystemBitMapSerializer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,15 @@ private byte[] inputToBytes(List<ElementSerializationResult> serializedBody, Byt
9191
}
9292
bitCount++;
9393
if (bitCount >= 8) {
94-
int index = ByteOrder.LITTLE_ENDIAN.equals(byteOrder) ? byteCount : (bytesLength - byteCount - 1);
94+
int index = ByteOrder.BIG_ENDIAN.equals(byteOrder) ? byteCount : (bytesLength - byteCount - 1);
9595
bytes[index] = (byte) (byteValue & 0xFF);
9696
byteValue = 0;
9797
bitCount = 0;
9898
byteCount++;
9999
}
100100
}
101101
if (serializedBody.size() % 8 != 0) {
102-
int index = ByteOrder.LITTLE_ENDIAN.equals(byteOrder) ? byteCount : (bytesLength - byteCount - 1);
102+
int index = ByteOrder.BIG_ENDIAN.equals(byteOrder) ? byteCount : (bytesLength - byteCount - 1);
103103
bytes[index] = (byte) (byteValue & 0xFF);
104104
}
105105
return bytes;

core/src/test/java/com/github/flexca/enot/core/serializer/EnotSerializerSuccessCasesTest.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.junit.jupiter.api.Test;
2121
import tools.jackson.databind.ObjectMapper;
2222

23+
import java.util.HashMap;
2324
import java.util.List;
2425
import java.util.Map;
2526

@@ -177,10 +178,132 @@ void testSerializeX509ValidityMixedDates() throws Exception {
177178
assertThat(seq.getObjectAt(1)).isInstanceOf(ASN1GeneralizedTime.class);
178179
}
179180

181+
// -----------------------------------------------------------------------
182+
// bit_map — 4 combinations of bit_order × byte_order
183+
//
184+
// byte_order and bit_order describe the INPUT data layout.
185+
// Output is always big-endian MSB-first (standard Java / network byte order).
186+
//
187+
// Logical value: [0xFE, 0xCA] (big-endian output)
188+
// 0xFE = 1111_1110, 0xCA = 1100_1010
189+
//
190+
// -----------------------------------------------------------------------
191+
192+
@Test
193+
void testBitMapMsbFirstBigEndian() throws Exception {
194+
// Input already in big-endian MSB-first order — no reordering needed.
195+
// group[0]=0xFE=[1,1,1,1,1,1,1,0] → bytes[0]
196+
// group[1]=0xCA=[1,1,0,0,1,0,1,0] → bytes[1]
197+
List<Boolean> bits = List.of(
198+
true, true, true, true, true, true, true, false,
199+
true, true, false, false, true, false, true, false);
200+
201+
byte[] result = serializeBitMap("big_endian", "msb_first", bits);
202+
assertThat(result).isEqualTo(new byte[]{(byte) 0xFE, (byte) 0xCA});
203+
}
204+
205+
@Test
206+
void testBitMapMsbFirstLittleEndian() throws Exception {
207+
// Input is little-endian (LSB group first) MSB-first within each byte.
208+
// Serializer reverses byte groups to produce big-endian output.
209+
// group[0]=0xCA=[1,1,0,0,1,0,1,0] → reversed → bytes[1]
210+
// group[1]=0xFE=[1,1,1,1,1,1,1,0] → reversed → bytes[0]
211+
List<Boolean> bits = List.of(
212+
true, true, false, false, true, false, true, false,
213+
true, true, true, true, true, true, true, false);
214+
215+
byte[] result = serializeBitMap("little_endian", "msb_first", bits);
216+
assertThat(result).isEqualTo(new byte[]{(byte) 0xFE, (byte) 0xCA});
217+
}
218+
219+
@Test
220+
void testBitMapLsbFirstBigEndian() throws Exception {
221+
// Input is big-endian, LSB-first within each byte.
222+
// 0xFE=1111_1110: bit0=0,bit1=1..bit7=1 → [F,T,T,T,T,T,T,T]
223+
// 0xCA=1100_1010: bit0=0,bit1=1,bit2=0,bit3=1,bit4=0,bit5=0,bit6=1,bit7=1 → [F,T,F,T,F,F,T,T]
224+
List<Boolean> bits = List.of(
225+
false, true, true, true, true, true, true, true,
226+
false, true, false, true, false, false, true, true);
227+
228+
byte[] result = serializeBitMap("big_endian", "lsb_first", bits);
229+
assertThat(result).isEqualTo(new byte[]{(byte) 0xFE, (byte) 0xCA});
230+
}
231+
232+
@Test
233+
void testBitMapLsbFirstLittleEndian() throws Exception {
234+
// Input is little-endian LSB-first — both orderings reversed vs output.
235+
// group[0]=0xCA(LSB first)=[F,T,F,T,F,F,T,T] → reversed → bytes[1]
236+
// group[1]=0xFE(LSB first)=[F,T,T,T,T,T,T,T] → reversed → bytes[0]
237+
List<Boolean> bits = List.of(
238+
false, true, false, true, false, false, true, true,
239+
false, true, true, true, true, true, true, true);
240+
241+
byte[] result = serializeBitMap("little_endian", "lsb_first", bits);
242+
assertThat(result).isEqualTo(new byte[]{(byte) 0xFE, (byte) 0xCA});
243+
}
244+
245+
@Test
246+
void testBitMapPartialByteSingleGroup() throws Exception {
247+
// 5 bits, big_endian msb_first: [1,0,1,1,0] packed from bit7 downward
248+
// = 1011_0000 = 0xB0, remaining 3 bits are zero
249+
List<Boolean> bits = List.of(true, false, true, true, false);
250+
251+
byte[] result = serializeBitMap("big_endian", "msb_first", bits);
252+
assertThat(result).hasSize(1);
253+
assertThat(result).isEqualTo(new byte[]{(byte) 0xB0});
254+
}
255+
256+
@Test
257+
void testBitMapPartialByteAfterCompleteGroup() throws Exception {
258+
// 10 bits, big_endian msb_first:
259+
// group[0] = [1,1,1,1,1,1,1,0] = 0xFE (complete) → bytes[0]
260+
// partial = [1,1] = 1100_0000 = 0xC0 → bytes[1]
261+
List<Boolean> bits = List.of(
262+
true, true, true, true, true, true, true, false,
263+
true, true);
264+
265+
byte[] result = serializeBitMap("big_endian", "msb_first", bits);
266+
assertThat(result).hasSize(2);
267+
assertThat(result).isEqualTo(new byte[]{(byte) 0xFE, (byte) 0xC0});
268+
}
269+
180270
// -----------------------------------------------------------------------
181271
// helpers
182272
// -----------------------------------------------------------------------
183273

274+
private byte[] serializeBitMap(String byteOrder, String bitOrder, List<Boolean> bits) throws Exception {
275+
// Build body as JSON array of individually named placeholders: ["${b0}", "${b1}", ...]
276+
// This avoids the ContextNode wrapping issue that occurs when a single placeholder
277+
// resolves to a List — individual primitive params unwrap cleanly.
278+
StringBuilder bodyArray = new StringBuilder("[");
279+
for (int i = 0; i < bits.size(); i++) {
280+
if (i > 0) bodyArray.append(",");
281+
bodyArray.append("\"${b").append(i).append("}\"");
282+
}
283+
bodyArray.append("]");
284+
285+
String json = """
286+
{
287+
"type": "system",
288+
"attributes": {
289+
"kind": "bit_map",
290+
"byte_order": "%s",
291+
"bit_order": "%s"
292+
},
293+
"body": %s
294+
}
295+
""".formatted(byteOrder, bitOrder, bodyArray);
296+
297+
Map<String, Object> params = new HashMap<>();
298+
for (int i = 0; i < bits.size(); i++) {
299+
params.put("b" + i, bits.get(i));
300+
}
301+
302+
List<byte[]> result = enotSerializer.serialize(json, ctx(params), enotContext);
303+
assertThat(result).hasSize(1);
304+
return result.get(0);
305+
}
306+
184307
/** Parses one DER blob and asserts SET { SEQUENCE { OID(2.5.4.11), UTF8String(expectedUnit) } } */
185308
private void assertOrgUnitDer(byte[] der, String expectedUnit) throws Exception {
186309
ASN1Set set = (ASN1Set) ASN1Primitive.fromByteArray(der);

docs/format/system.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,20 +217,34 @@ Converts an ordered list of boolean values into a compact bit-field byte
217217
array. Each boolean in the `body` array maps to one bit in declaration
218218
order.
219219

220+
**`byte_order` and `bit_order` describe the input data layout.**
221+
The output is always in **big-endian, MSB-first** order — standard Java /
222+
network byte order — regardless of the input layout.
223+
224+
| Attribute | Effect on input interpretation |
225+
|-----------|-------------------------------|
226+
| `byte_order: "big_endian"` | First group of 8 booleans → most-significant byte of output (no reordering) |
227+
| `byte_order: "little_endian"` | First group of 8 booleans → least-significant byte; groups are reversed in the output |
228+
| `bit_order: "msb_first"` | First boolean in a group = bit 7 (most significant bit) |
229+
| `bit_order: "lsb_first"` | First boolean in a group = bit 0 (least significant bit) |
230+
220231
**Attributes:**
221232

222233
| Attribute | Required | Description |
223234
|-----------|----------|-------------|
224235
| `kind` || `"bit_map"` |
225-
| `byte_order` || `"big_endian"` or `"little_endian"` |
226-
| `bit_order` || `"msb_first"` or `"lsb_first"` |
236+
| `byte_order` || `"big_endian"` or `"little_endian"` — describes input byte group order |
237+
| `bit_order` || `"msb_first"` or `"lsb_first"` — describes input bit order within each byte |
227238

228239
**Body:** a JSON array of boolean placeholders or literals.
229240

230241
The output is raw binary and is almost always wrapped in a
231242
`bit_string` ASN.1 element.
232243

233-
**Example — X.509 Key Usage extension:**
244+
**Example — X.509 Key Usage extension (RFC 5280):**
245+
246+
The Key Usage bit string is defined MSB-first in a single byte, so both
247+
`byte_order` and `bit_order` are `"big_endian"` / `"msb_first"`:
234248

235249
```json
236250
{
@@ -240,8 +254,8 @@ The output is raw binary and is almost always wrapped in a
240254
"type": "system",
241255
"attributes": {
242256
"kind": "bit_map",
243-
"byte_order": "little_endian",
244-
"bit_order": "lsb_first"
257+
"byte_order": "big_endian",
258+
"bit_order": "msb_first"
245259
},
246260
"body": [
247261
"${key_usage.digital_signature}",
@@ -264,8 +278,8 @@ The output is raw binary and is almost always wrapped in a
264278
{
265279
"key_usage": {
266280
"digital_signature": true,
267-
"key_encipherment": true,
268281
"non_repudiation": false,
282+
"key_encipherment": true,
269283
"data_encipherment": false,
270284
"key_agreement": false,
271285
"key_cert_sign": false,

0 commit comments

Comments
 (0)