layout-data 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/LICENSE +373 -0
- package/README.md +123 -2
- package/dist/types/Ldata.d.ts +180 -0
- package/dist/types/Ltag.d.ts +74 -0
- package/dist/types/codecs.d.ts +11 -0
- package/dist/types/dispatch.d.ts +11 -0
- package/dist/types/endian.d.ts +65 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/internal_types.d.ts +65 -0
- package/dist/types/layout.d.ts +54 -0
- package/dist/types/observer.d.ts +43 -0
- package/dist/types/registry.d.ts +47 -0
- package/dist/types/reserved.d.ts +9 -0
- package/dist/types/structure.d.ts +27 -0
- package/dist/types/typedefs.d.ts +155 -0
- package/dist/types/types.d.ts +25 -0
- package/package.json +49 -3
- package/src/Ldata.js +541 -0
- package/src/Ltag.js +87 -0
- package/src/codecs.js +64 -0
- package/src/dispatch.js +23 -0
- package/src/endian.js +197 -0
- package/src/index.js +8 -0
- package/src/internal_types.js +47 -0
- package/src/layout.js +303 -0
- package/src/observer.js +147 -0
- package/src/registry.js +83 -0
- package/src/reserved.js +35 -0
- package/src/structure.js +238 -0
- package/src/typedefs.js +143 -0
- package/src/types.js +74 -0
package/src/dispatch.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Dispatch of events that does not stop at a listener that throws
|
|
2
|
+
// (doc/spec-data-api.md §4.4, doc/design.md §5.1). Internal.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calls every listener of an event, in the order they subscribed, and pushes
|
|
6
|
+
* what a listener throws to `errors` instead of letting it stop the others.
|
|
7
|
+
* A `once` listener removes itself as usual.
|
|
8
|
+
*
|
|
9
|
+
* @param {import("node:events").EventEmitter | undefined} emitter - Nothing happens for undefined.
|
|
10
|
+
* @param {string} event
|
|
11
|
+
* @param {any[]} args
|
|
12
|
+
* @param {unknown[]} errors
|
|
13
|
+
*/
|
|
14
|
+
export function emit_all(emitter, event, args, errors) {
|
|
15
|
+
if (!emitter) return;
|
|
16
|
+
for (const listener of emitter.rawListeners(event)) {
|
|
17
|
+
try {
|
|
18
|
+
listener.apply(emitter, args);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
errors.push(error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/endian.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Conversion between the device byte order of an IO buffer and the big-endian
|
|
2
|
+
// buffer of an Ldata, per compiled layout entry (doc/spec-data-api.md §2.9,
|
|
3
|
+
// doc/design.md §6). Device byte order acts per 16-bit register and per 32-bit
|
|
4
|
+
// value; the IO buffer is indexed like the Ldata buffer. Only io_index_table is
|
|
5
|
+
// exported by the package: Ldata calls the other functions with its own buffer.
|
|
6
|
+
import { compiled_of } from "./registry.js";
|
|
7
|
+
|
|
8
|
+
/** @import { Endian, Layout } from "./typedefs.js" */
|
|
9
|
+
/** @import { Layout_Entry } from "./internal_types.js" */
|
|
10
|
+
|
|
11
|
+
// The device byte orders; there are no aliases.
|
|
12
|
+
const ENDIANS = ["BE", "LE", "BEBS", "LEBS"];
|
|
13
|
+
|
|
14
|
+
// Endians that swap the two bytes of a 16-bit register.
|
|
15
|
+
const BYTE_SWAP_ENDIANS = ["LE", "BEBS"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Throws for a byte order that is not one of BE, LE, BEBS and LEBS, instead of
|
|
19
|
+
* treating it as BE and corrupting the data silently.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} endian
|
|
22
|
+
* @throws {RangeError}
|
|
23
|
+
*/
|
|
24
|
+
export function check_endian(endian) {
|
|
25
|
+
if (!ENDIANS.includes(endian)) {
|
|
26
|
+
throw new RangeError(`unknown byte order ${endian}: use BE, LE, BEBS or LEBS`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Converts the endianness of a 2-byte value between the specified endian format and Big Endian (BE).
|
|
32
|
+
*
|
|
33
|
+
* This function performs bidirectional conversion:
|
|
34
|
+
* - If the data is not in BE, it converts it to BE.
|
|
35
|
+
* - If the data is in BE, it converts it to the specified endian format.
|
|
36
|
+
*
|
|
37
|
+
* @param {Uint8Array} buffer_from - The source buffer containing the data to be converted.
|
|
38
|
+
* @param {Uint8Array} buffer_to - The destination buffer where the converted data will be written.
|
|
39
|
+
* @param {number} index - The starting index in the buffers for the 2-byte value.
|
|
40
|
+
* @param {Endian} endian - The endian format to convert to/from.
|
|
41
|
+
*/
|
|
42
|
+
function convert_endian_2byte(buffer_from, buffer_to, index, endian) {
|
|
43
|
+
if (BYTE_SWAP_ENDIANS.includes(endian)) { // LE, BEBS: BA
|
|
44
|
+
buffer_to[index] = buffer_from[index + 1];
|
|
45
|
+
buffer_to[index + 1] = buffer_from[index];
|
|
46
|
+
} else { // BE, LEBS: AB
|
|
47
|
+
buffer_to[index] = buffer_from[index];
|
|
48
|
+
buffer_to[index + 1] = buffer_from[index + 1];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Converts the endianness of a 4-byte value between the specified endian format and Big Endian (BE).
|
|
54
|
+
*
|
|
55
|
+
* This function performs bidirectional conversion:
|
|
56
|
+
* - If the data is not in BE, it converts it to BE.
|
|
57
|
+
* - If the data is in BE, it converts it to the specified endian format.
|
|
58
|
+
*
|
|
59
|
+
* The function handles different endian formats:
|
|
60
|
+
* - LE (Little Endian): DCBA <-> ABCD
|
|
61
|
+
* - BEBS (Big Endian Byte Swap): BADC <-> ABCD
|
|
62
|
+
* - LEBS (Little Endian Byte Swap): CDAB <-> ABCD
|
|
63
|
+
*
|
|
64
|
+
* @param {Uint8Array} buffer_from - The source buffer containing the data to be converted.
|
|
65
|
+
* @param {Uint8Array} buffer_to - The destination buffer where the converted data will be written.
|
|
66
|
+
* @param {number} index - The starting index in the buffers for the 4-byte value.
|
|
67
|
+
* @param {Endian} endian - The endian format to convert to/from.
|
|
68
|
+
*/
|
|
69
|
+
function convert_endian_4bytes(buffer_from, buffer_to, index, endian) {
|
|
70
|
+
switch (endian) {
|
|
71
|
+
case "LE": { // DCBA <-> ABCD
|
|
72
|
+
buffer_to[index] = buffer_from[index + 3];
|
|
73
|
+
buffer_to[index + 1] = buffer_from[index + 2];
|
|
74
|
+
buffer_to[index + 2] = buffer_from[index + 1];
|
|
75
|
+
buffer_to[index + 3] = buffer_from[index];
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "BEBS": { // BADC <-> ABCD
|
|
79
|
+
buffer_to[index] = buffer_from[index + 1];
|
|
80
|
+
buffer_to[index + 1] = buffer_from[index];
|
|
81
|
+
buffer_to[index + 2] = buffer_from[index + 3];
|
|
82
|
+
buffer_to[index + 3] = buffer_from[index + 2];
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "LEBS": { // CDAB <-> ABCD
|
|
86
|
+
buffer_to[index] = buffer_from[index + 2];
|
|
87
|
+
buffer_to[index + 1] = buffer_from[index + 3];
|
|
88
|
+
buffer_to[index + 2] = buffer_from[index];
|
|
89
|
+
buffer_to[index + 3] = buffer_from[index + 1];
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
default: { // BE
|
|
93
|
+
for (let k = 0; k < 4; k++) buffer_to[index + k] = buffer_from[index + k];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Index in a device-ordered IO buffer of the byte of a single-byte entry
|
|
100
|
+
* (Bool, Byte, SInt, USInt): when the endian swaps the register bytes, the
|
|
101
|
+
* byte sits in the other half of its register.
|
|
102
|
+
*
|
|
103
|
+
* @param {number} byte - Byte offset of the entry.
|
|
104
|
+
* @param {Endian} endian - Device byte order.
|
|
105
|
+
* @returns {number}
|
|
106
|
+
*/
|
|
107
|
+
export function io_index(byte, endian) {
|
|
108
|
+
return BYTE_SWAP_ENDIANS.includes(endian) ? byte ^ 1 : byte;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The index in a device-ordered IO buffer of every byte of a data block: the
|
|
113
|
+
* byte `i` of the block is the byte `table[i]` of the IO buffer. The table
|
|
114
|
+
* makes loading and storing a whole block a plain byte copy, and it agrees
|
|
115
|
+
* with `load_entry` / `store_entry` for every entry (a Bool or a single-byte
|
|
116
|
+
* entry maps as `io_index`, a multi-byte entry as its endian).
|
|
117
|
+
*
|
|
118
|
+
* @param {Layout} layout - The compiled layout of the data block.
|
|
119
|
+
* @param {Endian} endian - Device byte order: BE, LE, BEBS or LEBS.
|
|
120
|
+
* @returns {Int32Array}
|
|
121
|
+
* @throws {RangeError} For another byte order.
|
|
122
|
+
*/
|
|
123
|
+
export function io_index_table(layout, endian) {
|
|
124
|
+
check_endian(endian);
|
|
125
|
+
const table = new Int32Array(layout.size);
|
|
126
|
+
for (let i = 0; i < table.length; i++) table[i] = io_index(i, endian);
|
|
127
|
+
for (const { byte, size, type } of compiled_of(layout).entries.values()) {
|
|
128
|
+
if (size !== 4 || type === "bool") continue;
|
|
129
|
+
for (let k = 0; k < 4; k++) {
|
|
130
|
+
switch (endian) {
|
|
131
|
+
case "LE": table[byte + k] = byte + 3 - k; break;
|
|
132
|
+
case "BEBS": table[byte + k] = byte + (k ^ 1); break;
|
|
133
|
+
case "LEBS": table[byte + k] = byte + ((k + 2) % 4); break;
|
|
134
|
+
default: table[byte + k] = byte + k;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return table;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Byte range [start, end) an entry occupies in a device-ordered IO buffer.
|
|
143
|
+
*
|
|
144
|
+
* @param {Layout_Entry} entry
|
|
145
|
+
* @param {Endian} endian - Device byte order.
|
|
146
|
+
* @returns {{start: number, end: number}}
|
|
147
|
+
*/
|
|
148
|
+
export function io_range(entry, endian) {
|
|
149
|
+
const start = entry.size === 1 ? io_index(entry.byte, endian) : entry.byte;
|
|
150
|
+
return { start, end: start + entry.size };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Copies an entry from a device-ordered IO buffer into a big-endian buffer.
|
|
155
|
+
* A Bool only changes its own bit. No change is detected here, and the byte
|
|
156
|
+
* order and the range in the IO buffer are not checked (Ldata does).
|
|
157
|
+
*
|
|
158
|
+
* @param {Layout_Entry} entry
|
|
159
|
+
* @param {Uint8Array} io_buffer - The device-ordered source.
|
|
160
|
+
* @param {Uint8Array} buffer - The big-endian destination.
|
|
161
|
+
* @param {Endian} endian - Device byte order.
|
|
162
|
+
*/
|
|
163
|
+
export function load_entry(entry, io_buffer, buffer, endian) {
|
|
164
|
+
const { byte, size, mask } = entry;
|
|
165
|
+
if (entry.type === "bool") {
|
|
166
|
+
const value = io_buffer[io_index(byte, endian)] & mask;
|
|
167
|
+
buffer[byte] = value ? buffer[byte] | mask : buffer[byte] & ~mask;
|
|
168
|
+
} else if (size === 1) {
|
|
169
|
+
buffer[byte] = io_buffer[io_index(byte, endian)];
|
|
170
|
+
} else if (size === 2) {
|
|
171
|
+
convert_endian_2byte(io_buffer, buffer, byte, endian);
|
|
172
|
+
} else {
|
|
173
|
+
convert_endian_4bytes(io_buffer, buffer, byte, endian);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Copies an entry from a big-endian buffer into a device-ordered IO buffer.
|
|
179
|
+
*
|
|
180
|
+
* @param {Layout_Entry} entry
|
|
181
|
+
* @param {Uint8Array} buffer - The big-endian source.
|
|
182
|
+
* @param {Uint8Array} io_buffer - The device-ordered destination.
|
|
183
|
+
* @param {Endian} endian - Device byte order.
|
|
184
|
+
*/
|
|
185
|
+
export function store_entry(entry, buffer, io_buffer, endian) {
|
|
186
|
+
const { byte, size, mask } = entry;
|
|
187
|
+
if (entry.type === "bool") {
|
|
188
|
+
const index = io_index(byte, endian);
|
|
189
|
+
io_buffer[index] = buffer[byte] & mask ? io_buffer[index] | mask : io_buffer[index] & ~mask;
|
|
190
|
+
} else if (size === 1) {
|
|
191
|
+
io_buffer[io_index(byte, endian)] = buffer[byte];
|
|
192
|
+
} else if (size === 2) {
|
|
193
|
+
convert_endian_2byte(buffer, io_buffer, byte, endian);
|
|
194
|
+
} else {
|
|
195
|
+
convert_endian_4bytes(buffer, io_buffer, byte, endian);
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Public interface of the package (doc/design.md §1).
|
|
2
|
+
export { Ldata } from "./Ldata.js";
|
|
3
|
+
export { Ltag } from "./Ltag.js";
|
|
4
|
+
export { register_layout } from "./layout.js";
|
|
5
|
+
export { get_layout } from "./registry.js";
|
|
6
|
+
export { io_index_table } from "./endian.js";
|
|
7
|
+
export { render_types } from "./types.js";
|
|
8
|
+
export * from "./typedefs.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Type definitions of the internals. This file has no run-time content, and
|
|
2
|
+
// index.js does not export it: the compiled data of a layout is not part of
|
|
3
|
+
// the public interface (doc/spec-data-api.md §6.6).
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Codec of one tag type on a big-endian buffer (doc/design.md §4).
|
|
7
|
+
*
|
|
8
|
+
* @typedef {object} Codec
|
|
9
|
+
* @property {number} size - Size in bytes.
|
|
10
|
+
* @property {(value: any) => string | null} check - The reason a value is refused, null if it can be written.
|
|
11
|
+
* @property {(buffer: Buffer, byte: number, mask: number) => number | boolean} get
|
|
12
|
+
* @property {(buffer: Buffer, byte: number, mask: number, value: number | boolean) => void} set
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One tag of a compiled layout.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {object} Layout_Entry
|
|
19
|
+
* @property {string} name
|
|
20
|
+
* @property {import("./typedefs.js").Tag_Type} type - Lower case type name.
|
|
21
|
+
* @property {number | boolean} init_value
|
|
22
|
+
* @property {number} byte - Byte offset.
|
|
23
|
+
* @property {number} index - Position among the entries of the layout.
|
|
24
|
+
* @property {number} size - Size in bytes.
|
|
25
|
+
* @property {number} mask - Bit mask of a Bool, 0xff otherwise.
|
|
26
|
+
* @property {Codec} codec
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The expanded structure of a data block (the result of build_structure).
|
|
31
|
+
*
|
|
32
|
+
* @typedef {object} Data_Struct
|
|
33
|
+
* @property {string} name
|
|
34
|
+
* @property {number} length - Total length in bits.
|
|
35
|
+
* @property {import("./typedefs.js").Layout_Item[]} items
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The compiled data of a layout, kept apart from the public Layout
|
|
40
|
+
* (doc/design.md §3).
|
|
41
|
+
*
|
|
42
|
+
* @typedef {object} Compiled_Layout
|
|
43
|
+
* @property {Map<string, Layout_Entry>} entries
|
|
44
|
+
* @property {Layout_Entry[][]} tags_at_byte - The entries covering each byte.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export {};
|
package/src/layout.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parseDocument } from "yaml";
|
|
3
|
+
import { CODECS } from "./codecs.js";
|
|
4
|
+
import { register, registered_of, set_compiled } from "./registry.js";
|
|
5
|
+
import { build_structure, validate_structure, TYPE_BITS } from "./structure.js";
|
|
6
|
+
|
|
7
|
+
/** @import { Layout_Source, Registered_Layouts, Layout } from "./typedefs.js" */
|
|
8
|
+
/** @import { Data_Struct } from "./internal_types.js" */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The canonical text of a value: object keys sorted, `comment` left out (it
|
|
12
|
+
* does not change how a layout works) and the type names in lower case.
|
|
13
|
+
*/
|
|
14
|
+
function canonical(value, key) {
|
|
15
|
+
if (Array.isArray(value)) return `[${value.map((v) => canonical(v)).join(",")}]`;
|
|
16
|
+
if (value !== null && typeof value === "object") {
|
|
17
|
+
const fields = Object.keys(value).filter((k) => k !== "comment" && value[k] !== undefined).sort();
|
|
18
|
+
return `{${fields.map((k) => `${JSON.stringify(k)}:${canonical(value[k], k)}`).join(",")}}`;
|
|
19
|
+
}
|
|
20
|
+
if (key === "type" && typeof value === "string") return JSON.stringify(value.toLowerCase());
|
|
21
|
+
return JSON.stringify(value) ?? "null";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compiles an expanded structure (the result of build_structure) into a Layout
|
|
26
|
+
* (doc/design.md §3) and registers it under its name, the one source of the
|
|
27
|
+
* layouts an Ldata is created from. Registering a structure whose canonical
|
|
28
|
+
* text equals the registered one of that name gives the registered Layout, so
|
|
29
|
+
* the same layout described twice is compiled once; another structure under a
|
|
30
|
+
* used name throws. Throws on an unknown type or a duplicated tag name; the
|
|
31
|
+
* other layout rules are checked by validate_structure. Internal.
|
|
32
|
+
*
|
|
33
|
+
* @param {Data_Struct} data_struct
|
|
34
|
+
* @returns {Layout}
|
|
35
|
+
* @throws {Error} When the name is used by another layout.
|
|
36
|
+
*/
|
|
37
|
+
export function register_structure(data_struct) {
|
|
38
|
+
const { name } = data_struct;
|
|
39
|
+
if (typeof name !== "string" || name === "") throw new Error("a layout needs a name");
|
|
40
|
+
const key = key_of(data_struct);
|
|
41
|
+
const known = registered_of(name);
|
|
42
|
+
if (known) {
|
|
43
|
+
if (known.key !== key) throw new Error(`another layout is already named ${name}`);
|
|
44
|
+
return known.layout;
|
|
45
|
+
}
|
|
46
|
+
const layout = compile_structure(data_struct);
|
|
47
|
+
register(name, { key, layout });
|
|
48
|
+
return layout;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function key_of(data_struct) {
|
|
52
|
+
return canonical({ name: data_struct.name, length: data_struct.length, items: data_struct.items });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compile_structure(data_struct) {
|
|
56
|
+
const size = data_struct.length >> 3;
|
|
57
|
+
const entries = new Map();
|
|
58
|
+
const tags_at_byte = Array.from({ length: size }, () => []);
|
|
59
|
+
for (const item of data_struct.items) {
|
|
60
|
+
const { name, offset, init_value } = item;
|
|
61
|
+
const type = item.type.toLowerCase();
|
|
62
|
+
const codec = CODECS[type];
|
|
63
|
+
const fail = (message) => {
|
|
64
|
+
throw new Error(`${data_struct.name}.${name}: ${message}`);
|
|
65
|
+
};
|
|
66
|
+
if (!codec) fail(`unknown tag type ${item.type}`);
|
|
67
|
+
if (entries.has(name)) fail("duplicated tag name");
|
|
68
|
+
// defensive checks of what an Ldata relies on; validate_structure reports them all
|
|
69
|
+
if (type !== "bool" && offset % 8 !== 0) fail(`bit offset ${offset % 8} on a ${item.type}`);
|
|
70
|
+
if (codec.size > 1 && (offset >> 3) % 2 !== 0) fail(`${item.type} at the odd byte ${offset >> 3}`);
|
|
71
|
+
if (!["number", "boolean"].includes(typeof init_value)) fail(`init_value ${init_value} is not a number or boolean`);
|
|
72
|
+
const byte = offset >> 3;
|
|
73
|
+
const entry = {
|
|
74
|
+
name, type, init_value, byte,
|
|
75
|
+
index: entries.size,
|
|
76
|
+
size: codec.size,
|
|
77
|
+
mask: type === "bool" ? 1 << (offset % 8) : 0xff,
|
|
78
|
+
codec,
|
|
79
|
+
};
|
|
80
|
+
entries.set(name, entry);
|
|
81
|
+
// entries keep the item order: a combined word comes before its bits
|
|
82
|
+
for (let i = byte; i < byte + codec.size && i < size; i++) tags_at_byte[i].push(entry);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// the public layout holds the name, the sizes and frozen copies of the items;
|
|
86
|
+
// the entries and the lookup table stay out of reach (doc/spec-data-api.md §6.6)
|
|
87
|
+
const layout = Object.freeze({
|
|
88
|
+
name: data_struct.name,
|
|
89
|
+
length: data_struct.length,
|
|
90
|
+
size,
|
|
91
|
+
items: Object.freeze(data_struct.items.map(public_tag)),
|
|
92
|
+
});
|
|
93
|
+
set_compiled(layout, { entries, tags_at_byte });
|
|
94
|
+
return layout;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The frozen description of a tag of the public layout: the documented fields
|
|
99
|
+
* only, the length filled in from the type when the item has none.
|
|
100
|
+
*/
|
|
101
|
+
function public_tag(item) {
|
|
102
|
+
const tag = {
|
|
103
|
+
name: item.name,
|
|
104
|
+
type: item.type,
|
|
105
|
+
offset: item.offset,
|
|
106
|
+
length: item.length ?? TYPE_BITS[item.type.toLowerCase()],
|
|
107
|
+
init_value: item.init_value,
|
|
108
|
+
};
|
|
109
|
+
if (item.comment !== undefined) tag.comment = item.comment;
|
|
110
|
+
tag.coupling = Object.freeze([...(item.coupling ?? [])]);
|
|
111
|
+
return Object.freeze(tag);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolves the fragment references of a list of items: `coupling` and
|
|
116
|
+
* `includes` given as a fragment name are replaced by a copy of that
|
|
117
|
+
* fragment, recursively. Omitted lengths are filled in from the type.
|
|
118
|
+
*/
|
|
119
|
+
function resolve_items(list, fragments, stack, where, errors) {
|
|
120
|
+
if (!Array.isArray(list)) {
|
|
121
|
+
errors.push(`${where}: expected a list of items`);
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const resolve_ref = (ref, field) => {
|
|
125
|
+
if (Array.isArray(ref)) return resolve_items(ref, fragments, stack, where, errors);
|
|
126
|
+
if (typeof ref !== "string") {
|
|
127
|
+
errors.push(`${where}: ${field} must be a fragment name or a list of items`);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
if (stack.includes(ref)) {
|
|
131
|
+
errors.push(`${where}: circular fragment reference ${[...stack, ref].join(" -> ")}`);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
if (!Object.hasOwn(fragments, ref)) {
|
|
135
|
+
errors.push(`${where}: unknown fragment ${ref}`);
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
return resolve_items(fragments[ref], fragments, [...stack, ref], `fragment ${ref}`, errors);
|
|
139
|
+
};
|
|
140
|
+
return list.map((_item) => {
|
|
141
|
+
const item = { ..._item };
|
|
142
|
+
if (item.coupling !== undefined) item.coupling = resolve_ref(item.coupling, "coupling");
|
|
143
|
+
if (item.includes !== undefined) item.includes = resolve_ref(item.includes, "includes");
|
|
144
|
+
const bits = TYPE_BITS[item.type?.toLowerCase()];
|
|
145
|
+
if (item.length === undefined && bits !== undefined) item.length = bits;
|
|
146
|
+
return item;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Reads the sources of the layouts (doc/design.md §3), merges their layouts
|
|
152
|
+
* and fragments into one description and registers it with
|
|
153
|
+
* register_layout_data. Each source is the path of a YAML layout file or a
|
|
154
|
+
* parsed description; a list of sources is merged, so a layout may name a
|
|
155
|
+
* fragment of any source whatever the order. The root key `layouts` of a
|
|
156
|
+
* source maps layout names to item lists; every other root key is a fragment
|
|
157
|
+
* referenced by name.
|
|
158
|
+
*
|
|
159
|
+
* @param {Layout_Source | Layout_Source[]} sources
|
|
160
|
+
* @returns {Registered_Layouts}
|
|
161
|
+
* @throws {Error} With `errors`, every violation found, when the sources are not valid.
|
|
162
|
+
*/
|
|
163
|
+
export function register_sources(sources) {
|
|
164
|
+
const errors = [];
|
|
165
|
+
const list = Array.isArray(sources) ? sources : [sources];
|
|
166
|
+
if (list.length === 0) errors.push("no source is given");
|
|
167
|
+
const origins = [];
|
|
168
|
+
const layout_defs = {};
|
|
169
|
+
const fragment_defs = {};
|
|
170
|
+
// the origin of each layout and fragment, to name both in a duplicate
|
|
171
|
+
const owners = { layout: new Map(), fragment: new Map() };
|
|
172
|
+
const add = (origin, root) => {
|
|
173
|
+
origins.push(origin);
|
|
174
|
+
if (root === null || typeof root !== "object" || Array.isArray(root)) {
|
|
175
|
+
errors.push(`${origin}: the root must be a mapping`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const { layouts, ...fragments } = root;
|
|
179
|
+
if (layouts !== undefined && (layouts === null || typeof layouts !== "object" || Array.isArray(layouts))) {
|
|
180
|
+
errors.push(`${origin}: the root key \`layouts\` must map layout names to item lists`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const merge = (kind, defs, into) => {
|
|
184
|
+
for (const [name, value] of Object.entries(defs)) {
|
|
185
|
+
if (owners[kind].has(name)) {
|
|
186
|
+
errors.push(`${kind} ${name} is defined in ${owners[kind].get(name)} and ${origin}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
owners[kind].set(name, origin);
|
|
190
|
+
into[name] = value;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
merge("layout", layouts ?? {}, layout_defs);
|
|
194
|
+
merge("fragment", fragments, fragment_defs);
|
|
195
|
+
};
|
|
196
|
+
list.forEach((source, index) => {
|
|
197
|
+
const label = Array.isArray(sources) ? `layout description #${index + 1}` : "layout description";
|
|
198
|
+
if (Array.isArray(source)) {
|
|
199
|
+
errors.push(`${label}: a list of sources must not contain a list`);
|
|
200
|
+
} else if (typeof source === "string") {
|
|
201
|
+
let text;
|
|
202
|
+
try {
|
|
203
|
+
text = readFileSync(source, "utf8");
|
|
204
|
+
} catch (error) {
|
|
205
|
+
errors.push(error.message);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const doc = parseDocument(text, { version: "1.2" });
|
|
209
|
+
if (doc.errors.length > 0) errors.push(...doc.errors.map((e) => `${source}: ${e.message}`));
|
|
210
|
+
else add(source, doc.toJS());
|
|
211
|
+
} else {
|
|
212
|
+
add(label, source);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
if (errors.length === 0 && Object.keys(layout_defs).length === 0) errors.push("no layout is defined in the sources");
|
|
216
|
+
if (errors.length > 0) throw layout_error("layout sources", errors);
|
|
217
|
+
return register_layout_data({ layouts: layout_defs, ...fragment_defs }, origins.join(", "));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Set once a call of register_layout has registered the layouts. */
|
|
221
|
+
let registered_once = false;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Registers the layouts of the sources, once per process: a layout is
|
|
225
|
+
* described, registered under its name, and then used by that name
|
|
226
|
+
* (doc/spec-data-api.md §6.4). Nothing is registered when a source is not
|
|
227
|
+
* valid, and such a call does not use up the single call.
|
|
228
|
+
*
|
|
229
|
+
* @param {Layout_Source | Layout_Source[]} sources - The path of a YAML layout file, a parsed description, or a list of them.
|
|
230
|
+
* @returns {Registered_Layouts}
|
|
231
|
+
* @throws {Error} With `errors`, every violation found, when the sources are not valid.
|
|
232
|
+
* @throws {Error} When the layouts were registered by an earlier call.
|
|
233
|
+
*/
|
|
234
|
+
export function register_layout(sources) {
|
|
235
|
+
if (registered_once) throw new Error("register_layout can be called only once");
|
|
236
|
+
const result = register_sources(sources);
|
|
237
|
+
registered_once = true;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function layout_error(subject, errors) {
|
|
242
|
+
const error = new Error(`invalid ${subject}:\n${errors.join("\n")}`);
|
|
243
|
+
error.errors = errors;
|
|
244
|
+
return error;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The conflicts of structures with the layouts registered under their names.
|
|
249
|
+
*
|
|
250
|
+
* @param {Record<string, Data_Struct>} structs - By layout name.
|
|
251
|
+
* @returns {string[]}
|
|
252
|
+
*/
|
|
253
|
+
function find_conflicts(structs) {
|
|
254
|
+
const conflicts = [];
|
|
255
|
+
for (const [name, struct] of Object.entries(structs)) {
|
|
256
|
+
const known = registered_of(name);
|
|
257
|
+
if (known && known.key !== key_of(struct)) conflicts.push(`layout ${name}: another layout is already named ${name}`);
|
|
258
|
+
}
|
|
259
|
+
return conflicts;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Registers the layouts of one description that is parsed and complete: the
|
|
264
|
+
* root key `layouts` and the fragments the layouts name. Internal.
|
|
265
|
+
*
|
|
266
|
+
* @param {Record<string, any>} root - The parsed description: `layouts` and the fragments.
|
|
267
|
+
* @param {string} [origin] - Names the source in errors.
|
|
268
|
+
* @returns {Registered_Layouts}
|
|
269
|
+
* @throws {Error} With `errors`, every violation found, when the description is not valid.
|
|
270
|
+
*/
|
|
271
|
+
export function register_layout_data(root, origin = "the given object") {
|
|
272
|
+
const fail = (errors) => {
|
|
273
|
+
throw layout_error(`layout description from ${origin}`, errors);
|
|
274
|
+
};
|
|
275
|
+
if (root === null || typeof root !== "object" || Array.isArray(root)) fail(["the root must be a mapping"]);
|
|
276
|
+
const { layouts: layout_defs, ...fragment_defs } = root;
|
|
277
|
+
if (layout_defs === null || typeof layout_defs !== "object" || Array.isArray(layout_defs)) {
|
|
278
|
+
fail(["the root key `layouts` must map layout names to item lists"]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const errors = [];
|
|
282
|
+
const fragments = {};
|
|
283
|
+
for (const [name, list] of Object.entries(fragment_defs)) {
|
|
284
|
+
fragments[name] = resolve_items(list, fragment_defs, [name], `fragment ${name}`, errors);
|
|
285
|
+
}
|
|
286
|
+
const structs = {};
|
|
287
|
+
for (const [name, list] of Object.entries(layout_defs)) {
|
|
288
|
+
const items = resolve_items(list, fragment_defs, [], `layout ${name}`, errors);
|
|
289
|
+
structs[name] = build_structure({ name }, items);
|
|
290
|
+
}
|
|
291
|
+
if (errors.length === 0) {
|
|
292
|
+
for (const struct of Object.values(structs)) errors.push(...validate_structure(struct));
|
|
293
|
+
}
|
|
294
|
+
if (errors.length === 0) {
|
|
295
|
+
// all or none: a conflict is found before any of the layouts is registered
|
|
296
|
+
errors.push(...find_conflicts(structs));
|
|
297
|
+
}
|
|
298
|
+
if (errors.length > 0) fail(errors);
|
|
299
|
+
|
|
300
|
+
const layouts = {};
|
|
301
|
+
for (const [name, struct] of Object.entries(structs)) layouts[name] = register_structure(struct);
|
|
302
|
+
return { layouts, fragments };
|
|
303
|
+
}
|