@sdxc/zone-file 0.0.0-pre.1 → 0.0.0-stage

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/dist/lexer.js DELETED
@@ -1,128 +0,0 @@
1
- /**
2
- * Splits master-file text into entries: one per line, or one per parenthesized group of
3
- * lines, each a list of fields with its trailing comment. Quotes, escapes and parentheses
4
- * are settled here, so the entry reader sees fields and never characters.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- /** Whether a character separates fields. */
10
- function isSpace(char) {
11
- return char === " " || char === "\t" || char === "\r";
12
- }
13
- /** Whether a character ends a bare field. */
14
- function endsField(char) {
15
- return isSpace(char) || char === ";" || char === "(" || char === ")";
16
- }
17
- /**
18
- * The index of the quote closing the one at `start`, skipping escaped characters, or `-1`
19
- * when the line ends first.
20
- */
21
- function closingQuote(line, start) {
22
- let end = start + 1;
23
- while (end < line.length && line[end] !== '"')
24
- end += line[end] === "\\" ? 2 : 1;
25
- return end < line.length ? end : -1;
26
- }
27
- /**
28
- * Splits text into entries. Blank and comment-only lines produce none; an entry whose
29
- * parentheses never close runs to the end of the text and carries an error, as does one
30
- * with a quote left open at the end of a line.
31
- *
32
- * @param text - The zone file's contents.
33
- * @returns The entries in file order.
34
- * @example lex("www IN A 192.0.2.1 ; web")[0]?.tokens.length // 4
35
- */
36
- export function lex(text) {
37
- let lines = text.split(/\r?\n/);
38
- let entries = [];
39
- let current = null;
40
- let depth = 0;
41
- let firstLine = 0;
42
- for (let index = 0; index < lines.length; index++) {
43
- let line = lines[index] ?? "";
44
- if (current === null) {
45
- firstLine = index;
46
- current = {
47
- line: index + 1,
48
- endLine: index + 1,
49
- blankOwner: isSpace(line[0]),
50
- tokens: [],
51
- comment: null,
52
- error: null,
53
- input: "",
54
- };
55
- }
56
- let entry = current;
57
- entry.endLine = index + 1;
58
- let fail = (message) => {
59
- entry.error ??= message;
60
- };
61
- let position = 0;
62
- while (position < line.length) {
63
- let char = line[position];
64
- if (isSpace(char)) {
65
- position += 1;
66
- }
67
- else if (char === ";") {
68
- let comment = line.slice(position + 1).trim();
69
- if (depth === 0)
70
- entry.comment = comment.length > 0 ? comment : null;
71
- break;
72
- }
73
- else if (char === "(") {
74
- if (depth > 0)
75
- fail("Parentheses cannot nest");
76
- depth = 1;
77
- position += 1;
78
- }
79
- else if (char === ")") {
80
- if (depth === 0)
81
- fail("A closing parenthesis has no opening one");
82
- depth = 0;
83
- position += 1;
84
- }
85
- else if (char === '"') {
86
- let end = closingQuote(line, position);
87
- if (end === -1) {
88
- fail("A quoted string cannot span lines");
89
- entry.tokens.push({ text: `${line.slice(position)}"`, quoted: true });
90
- break;
91
- }
92
- entry.tokens.push({ text: line.slice(position, end + 1), quoted: true });
93
- position = end + 1;
94
- }
95
- else {
96
- let end = position;
97
- while (end < line.length && !endsField(line[end])) {
98
- if (line[end] === '"') {
99
- let closing = closingQuote(line, end);
100
- if (closing === -1) {
101
- fail("A quoted string cannot span lines");
102
- end = line.length;
103
- break;
104
- }
105
- end = closing + 1;
106
- }
107
- else {
108
- end += line[end] === "\\" ? 2 : 1;
109
- }
110
- }
111
- entry.tokens.push({ text: line.slice(position, end), quoted: false });
112
- position = end;
113
- }
114
- }
115
- if (depth === 0) {
116
- entry.input = lines.slice(firstLine, index + 1).join("\n");
117
- if (entry.tokens.length > 0 || entry.error !== null)
118
- entries.push(entry);
119
- current = null;
120
- }
121
- }
122
- if (current !== null) {
123
- current.error ??= "An opening parenthesis is never closed";
124
- current.input = lines.slice(firstLine).join("\n");
125
- entries.push(current);
126
- }
127
- return entries;
128
- }
package/dist/names.d.ts DELETED
@@ -1,49 +0,0 @@
1
- /**
2
- * Domain names in presentation form (RFC 1035 section 5.1): reads `\.` and `\DDD` escapes
3
- * into label octets, qualifies relative names against an origin, and prints one canonical
4
- * spelling, lowercased with every special octet escaped, so equal names compare equal.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- /** How relative names are read; see `ZoneFile.ParseOptions.relativeNames`. */
10
- export type RelativeNames = "rfc1035" | "origin-suffix";
11
- /**
12
- * Prints labels as a canonical name without the trailing dot, the root as `"."`. Wire
13
- * labels from RFC 3597 generic data print through here too, so both spellings agree.
14
- */
15
- export declare function printLabels(labels: readonly Uint8Array[]): string;
16
- /**
17
- * Reads a name as written, absolute or not, into its canonical spelling: lowercased, no
18
- * trailing dot, the root as `"."`. `null` when the text is not a valid name.
19
- *
20
- * @example canonicalName("Mail.Example.COM.") // "mail.example.com"
21
- * @example canonicalName("a\\065b.example.com") // "aab.example.com"
22
- */
23
- export declare function canonicalName(text: string): string | null;
24
- /**
25
- * Resolves a name as a zone file writes it to its absolute canonical spelling: `@` is the
26
- * origin, a trailing dot marks an absolute name, and any other name gets the origin
27
- * appended. `null` when the text, or the qualified name, is not a valid name.
28
- *
29
- * @param text - The name as written.
30
- * @param origin - The current origin, in canonical form.
31
- * @param mode - `"origin-suffix"` reads a relative name already ending in the origin as absolute.
32
- * @example qualifyName("www", "example.com", "rfc1035") // "www.example.com"
33
- */
34
- export declare function qualifyName(text: string, origin: string, mode: RelativeNames): string | null;
35
- /**
36
- * Writes a canonical name relative to `origin` when it is the origin (`@`) or below it, and
37
- * absolute with its trailing dot otherwise.
38
- *
39
- * @example relativeName("www.example.com", "example.com") // "www"
40
- * @example relativeName("example.net", "example.com") // "example.net."
41
- */
42
- export declare function relativeName(name: string, origin: string): string;
43
- /**
44
- * Writes a name absolute: the trailing dot added unless it is the root or already ends in
45
- * an unescaped dot.
46
- *
47
- * @example absoluteName("mx.example.com") // "mx.example.com."
48
- */
49
- export declare function absoluteName(name: string): string;
package/dist/names.js DELETED
@@ -1,167 +0,0 @@
1
- /**
2
- * Domain names in presentation form (RFC 1035 section 5.1): reads `\.` and `\DDD` escapes
3
- * into label octets, qualifies relative names against an origin, and prints one canonical
4
- * spelling, lowercased with every special octet escaped, so equal names compare equal.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- /** Encodes literal characters into the octets they stand for. */
10
- const ENCODER = new TextEncoder();
11
- /**
12
- * Octets printed with a backslash: the label separator, the escape itself, and the
13
- * characters a zone file reads as syntax (`"`, `$`, `(`, `)`, `;`, `@`).
14
- */
15
- const SPECIAL_OCTETS = new Set([0x22, 0x24, 0x28, 0x29, 0x2e, 0x3b, 0x40, 0x5c]);
16
- /** The longest label RFC 1035 allows, in octets. */
17
- const MAX_LABEL_OCTETS = 63;
18
- /** The longest name RFC 1035 allows, in wire octets: each label plus its length, plus the root. */
19
- const MAX_NAME_OCTETS = 255;
20
- /**
21
- * Reads presentation text into label octets. Fails on an empty label, a label over 63
22
- * octets, a decimal escape above 255 and a lone trailing backslash.
23
- */
24
- function readName(text) {
25
- if (text === ".")
26
- return { labels: [], absolute: true };
27
- let chars = Array.from(text);
28
- let labels = [];
29
- let label = [];
30
- let absolute = false;
31
- for (let index = 0; index < chars.length; index++) {
32
- let char = chars[index] ?? "";
33
- if (char === "\\") {
34
- let digits = chars.slice(index + 1, index + 4).join("");
35
- if (/^\d{3}$/.test(digits)) {
36
- let octet = Number(digits);
37
- if (octet > 255)
38
- return null;
39
- label.push(octet);
40
- index += 3;
41
- continue;
42
- }
43
- let next = chars[index + 1];
44
- if (next === undefined)
45
- return null;
46
- label.push(...ENCODER.encode(next));
47
- index += 1;
48
- continue;
49
- }
50
- if (char === ".") {
51
- if (label.length === 0)
52
- return null;
53
- labels.push(Uint8Array.from(label));
54
- label = [];
55
- if (index === chars.length - 1)
56
- absolute = true;
57
- continue;
58
- }
59
- label.push(...ENCODER.encode(char));
60
- }
61
- if (!absolute) {
62
- if (label.length === 0)
63
- return null;
64
- labels.push(Uint8Array.from(label));
65
- }
66
- return labels.every((part) => part.length <= MAX_LABEL_OCTETS) ? { labels, absolute } : null;
67
- }
68
- /** Whether labels fit in the 255 wire octets of one name. */
69
- function fits(labels) {
70
- return labels.reduce((total, label) => total + label.length + 1, 1) <= MAX_NAME_OCTETS;
71
- }
72
- /** One label in canonical form: ASCII letters lowercased, specials and non-printables escaped. */
73
- function printLabel(label) {
74
- let printed = "";
75
- for (let octet of label) {
76
- let folded = octet >= 0x41 && octet <= 0x5a ? octet + 0x20 : octet;
77
- if (SPECIAL_OCTETS.has(folded))
78
- printed += `\\${String.fromCharCode(folded)}`;
79
- else if (folded > 0x20 && folded < 0x7f)
80
- printed += String.fromCharCode(folded);
81
- else
82
- printed += `\\${String(folded).padStart(3, "0")}`;
83
- }
84
- return printed;
85
- }
86
- /**
87
- * Prints labels as a canonical name without the trailing dot, the root as `"."`. Wire
88
- * labels from RFC 3597 generic data print through here too, so both spellings agree.
89
- */
90
- export function printLabels(labels) {
91
- return labels.length === 0 ? "." : labels.map(printLabel).join(".");
92
- }
93
- /** The labels of a canonical name, as `canonicalName` prints it. */
94
- function labelsOf(canonical) {
95
- return readName(canonical === "." ? "." : `${canonical}.`)?.labels ?? [];
96
- }
97
- /** Whether `labels` end with every label of `suffix`, compared in canonical form. */
98
- function endsWithLabels(labels, suffix) {
99
- if (suffix.length > labels.length)
100
- return false;
101
- let offset = labels.length - suffix.length;
102
- return suffix.every((label, index) => printLabel(label) === printLabel(labels[offset + index] ?? new Uint8Array()));
103
- }
104
- /**
105
- * Reads a name as written, absolute or not, into its canonical spelling: lowercased, no
106
- * trailing dot, the root as `"."`. `null` when the text is not a valid name.
107
- *
108
- * @example canonicalName("Mail.Example.COM.") // "mail.example.com"
109
- * @example canonicalName("a\\065b.example.com") // "aab.example.com"
110
- */
111
- export function canonicalName(text) {
112
- let name = readName(text);
113
- return name && fits(name.labels) ? printLabels(name.labels) : null;
114
- }
115
- /**
116
- * Resolves a name as a zone file writes it to its absolute canonical spelling: `@` is the
117
- * origin, a trailing dot marks an absolute name, and any other name gets the origin
118
- * appended. `null` when the text, or the qualified name, is not a valid name.
119
- *
120
- * @param text - The name as written.
121
- * @param origin - The current origin, in canonical form.
122
- * @param mode - `"origin-suffix"` reads a relative name already ending in the origin as absolute.
123
- * @example qualifyName("www", "example.com", "rfc1035") // "www.example.com"
124
- */
125
- export function qualifyName(text, origin, mode) {
126
- if (text === "@")
127
- return origin;
128
- let name = readName(text);
129
- if (!name)
130
- return null;
131
- if (name.absolute)
132
- return fits(name.labels) ? printLabels(name.labels) : null;
133
- let originLabels = labelsOf(origin);
134
- if (mode === "origin-suffix" && originLabels.length > 0) {
135
- if (endsWithLabels(name.labels, originLabels))
136
- return printLabels(name.labels);
137
- }
138
- let labels = [...name.labels, ...originLabels];
139
- return fits(labels) ? printLabels(labels) : null;
140
- }
141
- /**
142
- * Writes a canonical name relative to `origin` when it is the origin (`@`) or below it, and
143
- * absolute with its trailing dot otherwise.
144
- *
145
- * @example relativeName("www.example.com", "example.com") // "www"
146
- * @example relativeName("example.net", "example.com") // "example.net."
147
- */
148
- export function relativeName(name, origin) {
149
- if (name === origin)
150
- return "@";
151
- let labels = labelsOf(name);
152
- let originLabels = labelsOf(origin);
153
- if (labels.length > originLabels.length && endsWithLabels(labels, originLabels))
154
- return printLabels(labels.slice(0, labels.length - originLabels.length));
155
- return absoluteName(name);
156
- }
157
- /**
158
- * Writes a name absolute: the trailing dot added unless it is the root or already ends in
159
- * an unescaped dot.
160
- *
161
- * @example absoluteName("mx.example.com") // "mx.example.com."
162
- */
163
- export function absoluteName(name) {
164
- let trailing = /(\\*)\.$/.exec(name);
165
- let absolute = name === "." || (trailing !== null && (trailing[1] ?? "").length % 2 === 0);
166
- return absolute ? name : `${name}.`;
167
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Reads record data in presentation format, as a zone file or a DoH JSON answer writes it,
3
- * into typed fields per record type. RFC 3597 generic data (`\# len hex`) is decoded from
4
- * its wire form, which is how some resolvers answer types such as CAA.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- import type { Result } from "@sdxc/result";
10
- import type * as ZoneFile from "./types.js";
11
- import { RecordDataError } from "./errors.js";
12
- /**
13
- * Reads a record's data into its typed fields; untyped types come back as `{ type, data }`.
14
- * Names come back canonical (lowercased, escapes resolved, no trailing dot, the root as
15
- * `"."`), and SOA timers accept BIND's TTL units (`2h`, `1w`) as zone files write them.
16
- *
17
- * @param type - The record type, as a mnemonic or RFC 3597 `TYPEnnn`.
18
- * @param data - The RDATA in presentation format.
19
- * @returns The record's fields, or why the data does not fit the type.
20
- * @template Type - The record type, which selects the fields returned.
21
- * @example parseRecordData("MX", "10 mx.example.com.") // success({ type: "MX", preference: 10, exchange: "mx.example.com" })
22
- */
23
- export declare function parseRecordData<Type extends ZoneFile.RecordType>(type: Type, data: string): Result<ZoneFile.RecordData<Type>, RecordDataError>;
@@ -1,288 +0,0 @@
1
- import { failure, isFailure, success } from "@sdxc/result";
2
- import { decodeOctets, readCharacterStrings } from "./character-strings.js";
3
- import { RecordDataError } from "./errors.js";
4
- import { canonicalName, printLabels } from "./names.js";
5
- import { canonicalType } from "./record-types.js";
6
- import { readTtl } from "./ttl.js";
7
- /**
8
- * A dotted-quad IPv4 address without leading zeros, which `inet_aton` would read as octal
9
- * and browsers as decimal, so an address carrying them has no single meaning.
10
- */
11
- const IPV4_PATTERN = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
12
- /**
13
- * Reads a record's data into its typed fields; untyped types come back as `{ type, data }`.
14
- * Names come back canonical (lowercased, escapes resolved, no trailing dot, the root as
15
- * `"."`), and SOA timers accept BIND's TTL units (`2h`, `1w`) as zone files write them.
16
- *
17
- * @param type - The record type, as a mnemonic or RFC 3597 `TYPEnnn`.
18
- * @param data - The RDATA in presentation format.
19
- * @returns The record's fields, or why the data does not fit the type.
20
- * @template Type - The record type, which selects the fields returned.
21
- * @example parseRecordData("MX", "10 mx.example.com.") // success({ type: "MX", preference: 10, exchange: "mx.example.com" })
22
- */
23
- export function parseRecordData(type, data) {
24
- let canonical = canonicalType(type);
25
- let value = data.trim();
26
- let parsed = value.startsWith("\\#")
27
- ? parseGeneric(canonical, value)
28
- : parsePresentation(canonical, value);
29
- return parsed;
30
- }
31
- /** Fails with the reason a record's data does not fit its type. */
32
- function invalid(type, data) {
33
- return failure(new RecordDataError(`Invalid ${type} record data: ${JSON.stringify(data)}`));
34
- }
35
- /** Reads an unsigned decimal field no larger than `max`, or `null`. */
36
- function readNumber(field, max) {
37
- if (field === undefined || !/^\d+$/.test(field))
38
- return null;
39
- let number = Number(field);
40
- return number <= max ? number : null;
41
- }
42
- /** An IPv6 literal in RFC 5952 canonical form, or `null` when the text is not one. */
43
- function canonicalIpv6(value) {
44
- if (!value.includes(":"))
45
- return null;
46
- let url = URL.parse(`http://[${value}]/`);
47
- return url ? url.hostname.slice(1, -1) : null;
48
- }
49
- /** Reads presentation-format data for the typed record types. */
50
- function parsePresentation(type, value) {
51
- let fields = value.split(/\s+/).filter((field) => field.length > 0);
52
- switch (type) {
53
- case "A":
54
- return IPV4_PATTERN.test(value) ? success({ type, address: value }) : invalid(type, value);
55
- case "AAAA": {
56
- let address = canonicalIpv6(value);
57
- return address ? success({ type, address }) : invalid(type, value);
58
- }
59
- case "CNAME":
60
- case "NS":
61
- case "PTR":
62
- case "DNAME": {
63
- let name = fields.length === 1 && fields[0] !== undefined ? canonicalName(fields[0]) : null;
64
- if (name === null)
65
- return invalid(type, value);
66
- return success(type === "NS" ? { type, host: name } : { type, target: name });
67
- }
68
- case "MX": {
69
- let preference = readNumber(fields[0], 0xffff);
70
- let exchange = fields[1] === undefined ? null : canonicalName(fields[1]);
71
- if (fields.length !== 2 || preference === null || exchange === null)
72
- return invalid(type, value);
73
- return success({ type, preference, exchange });
74
- }
75
- case "TXT": {
76
- let strings = readCharacterStrings(value);
77
- if (isFailure(strings))
78
- return strings;
79
- return success({ type, text: strings.data.join(""), strings: strings.data });
80
- }
81
- case "CAA": {
82
- let match = /^(\d+)\s+([A-Za-z0-9]+)\s+(.+)$/.exec(value);
83
- let flags = readNumber(match?.[1], 0xff);
84
- if (!match || flags === null || match[2] === undefined || match[3] === undefined)
85
- return invalid(type, value);
86
- let raw = match[3];
87
- let text = raw;
88
- if (raw.startsWith('"')) {
89
- let strings = readCharacterStrings(raw);
90
- if (isFailure(strings))
91
- return strings;
92
- text = strings.data.join("");
93
- }
94
- return success({
95
- type,
96
- flags,
97
- critical: (flags & 0x80) !== 0,
98
- tag: match[2].toLowerCase(),
99
- value: text,
100
- });
101
- }
102
- case "SRV": {
103
- let [priority, weight, port] = fields.slice(0, 3).map((field) => readNumber(field, 0xffff));
104
- let target = fields[3] === undefined ? null : canonicalName(fields[3]);
105
- if (fields.length !== 4 ||
106
- priority == null ||
107
- weight == null ||
108
- port == null ||
109
- target === null)
110
- return invalid(type, value);
111
- return success({ type, priority, weight, port, target });
112
- }
113
- case "SOA": {
114
- let [primary, mailbox] = fields.slice(0, 2).map((field) => canonicalName(field));
115
- let numbers = fields
116
- .slice(2)
117
- .map((field, index) => readNumber(field, 0xffffffff) ?? (index > 0 ? readTtl(field) : null));
118
- if (fields.length !== 7 ||
119
- primary == null ||
120
- mailbox == null ||
121
- numbers.some((n) => n === null)) {
122
- return invalid(type, value);
123
- }
124
- let [serial, refresh, retry, expire, minimum] = numbers;
125
- return success({
126
- type,
127
- primary,
128
- mailbox,
129
- serial,
130
- refresh,
131
- retry,
132
- expire,
133
- minimum,
134
- });
135
- }
136
- default:
137
- return success({ type, data: value });
138
- }
139
- }
140
- /** Decodes RFC 3597 generic data (`\# <length> <hex>`) and reads the octets for the type. */
141
- function parseGeneric(type, value) {
142
- let match = /^\\#\s+(\d+)((?:\s*[0-9a-fA-F]+)*)\s*$/.exec(value);
143
- if (!match)
144
- return invalid(type, value);
145
- let hex = (match[2] ?? "").replace(/\s+/g, "");
146
- let length = Number(match[1]);
147
- if (hex.length !== length * 2)
148
- return invalid(type, value);
149
- let octets = new Uint8Array(length);
150
- for (let index = 0; index < length; index++)
151
- octets[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
152
- let fields = new WireReader(octets).record(type);
153
- if (fields === "unknown")
154
- return success({ type, data: value });
155
- return fields ? success({ type, ...fields }) : invalid(type, value);
156
- }
157
- /**
158
- * Reads the wire form of RDATA front to back. Every read returns `null` past the end, and a
159
- * record parses only when it consumes every octet, since trailing bytes mean a wrong type.
160
- */
161
- class WireReader {
162
- #octets;
163
- #offset = 0;
164
- /** @param octets - The RDATA octets. */
165
- constructor(octets) {
166
- this.#octets = octets;
167
- }
168
- /**
169
- * The typed fields for `type`, `null` when the octets do not fit it, or `"unknown"` for a
170
- * type without a typed reading.
171
- */
172
- record(type) {
173
- let fields = this.#fields(type);
174
- if (fields === null || fields === "unknown")
175
- return fields;
176
- return this.#offset === this.#octets.length ? fields : null;
177
- }
178
- /** Reads the type's fields, leaving the offset after the last octet read. */
179
- #fields(type) {
180
- switch (type) {
181
- case "A": {
182
- let bytes = this.#bytes(4);
183
- return bytes ? { address: Array.from(bytes).join(".") } : null;
184
- }
185
- case "AAAA": {
186
- let bytes = this.#bytes(16);
187
- if (!bytes)
188
- return null;
189
- let groups = Array.from({ length: 8 }, (_, index) => (((bytes[index * 2] ?? 0) << 8) | (bytes[index * 2 + 1] ?? 0)).toString(16));
190
- return { address: canonicalIpv6(groups.join(":")) ?? groups.join(":") };
191
- }
192
- case "CNAME":
193
- case "PTR":
194
- case "DNAME": {
195
- let target = this.#name();
196
- return target === null ? null : { target };
197
- }
198
- case "NS": {
199
- let host = this.#name();
200
- return host === null ? null : { host };
201
- }
202
- case "MX": {
203
- let preference = this.#uint(2);
204
- let exchange = this.#name();
205
- return preference === null || exchange === null ? null : { preference, exchange };
206
- }
207
- case "TXT": {
208
- let strings = [];
209
- while (this.#offset < this.#octets.length) {
210
- let length = this.#uint(1);
211
- let bytes = length === null ? null : this.#bytes(length);
212
- if (!bytes)
213
- return null;
214
- strings.push(decodeOctets(bytes));
215
- }
216
- return { text: strings.join(""), strings };
217
- }
218
- case "CAA": {
219
- let flags = this.#uint(1);
220
- let tagLength = this.#uint(1);
221
- let tag = tagLength === null ? null : this.#bytes(tagLength);
222
- if (flags === null || !tag || tag.length === 0)
223
- return null;
224
- let rest = this.#bytes(this.#octets.length - this.#offset) ?? new Uint8Array();
225
- return {
226
- flags,
227
- critical: (flags & 0x80) !== 0,
228
- tag: decodeOctets(tag).toLowerCase(),
229
- value: decodeOctets(rest),
230
- };
231
- }
232
- case "SRV": {
233
- let priority = this.#uint(2);
234
- let weight = this.#uint(2);
235
- let port = this.#uint(2);
236
- let target = this.#name();
237
- if (priority === null || weight === null || port === null || target === null)
238
- return null;
239
- return { priority, weight, port, target };
240
- }
241
- case "SOA": {
242
- let primary = this.#name();
243
- let mailbox = this.#name();
244
- let numbers = Array.from({ length: 5 }, () => this.#uint(4));
245
- if (primary === null || mailbox === null || numbers.some((n) => n === null))
246
- return null;
247
- let [serial, refresh, retry, expire, minimum] = numbers;
248
- return { primary, mailbox, serial, refresh, retry, expire, minimum };
249
- }
250
- default:
251
- return "unknown";
252
- }
253
- }
254
- /** The next `length` octets, or `null` past the end. */
255
- #bytes(length) {
256
- if (this.#offset + length > this.#octets.length)
257
- return null;
258
- let bytes = this.#octets.subarray(this.#offset, this.#offset + length);
259
- this.#offset += length;
260
- return bytes;
261
- }
262
- /** A big-endian unsigned integer of `size` octets. */
263
- #uint(size) {
264
- let bytes = this.#bytes(size);
265
- if (!bytes)
266
- return null;
267
- return bytes.reduce((total, byte) => total * 256 + byte, 0);
268
- }
269
- /**
270
- * An uncompressed domain name, printed like presentation names. RFC 3597 forbids
271
- * compression in generic data, so a compression pointer fails the read.
272
- */
273
- #name() {
274
- let labels = [];
275
- while (true) {
276
- let length = this.#uint(1);
277
- if (length === null || length > 63)
278
- return null;
279
- if (length === 0)
280
- break;
281
- let label = this.#bytes(length);
282
- if (!label)
283
- return null;
284
- labels.push(label);
285
- }
286
- return printLabels(labels);
287
- }
288
- }
package/dist/parse.d.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Reads an RFC 1035 master file into the records it declares and the entries it could not
3
- * use: directives, inherited owners, TTLs and classes, `$INCLUDE` through a caller-supplied
4
- * reader, and names qualified in owners and in typed RDATA.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- import type { Result } from "@sdxc/result";
10
- import type * as ZoneFile from "./types.js";
11
- import { ZoneFileError } from "./errors.js";
12
- /**
13
- * Reads a zone file. The only failure is `too-large`; every unreadable entry is a
14
- * `Rejection` beside the records that did parse, so one bad line never costs the rest, and
15
- * a caller that wants all-or-nothing checks `rejected.length`.
16
- *
17
- * Records outside the origin, repeats, non-`IN` classes and every record type are all
18
- * returned: which of them to keep is the caller's decision.
19
- *
20
- * @param text - The zone file's contents.
21
- * @param options - The origin, and how to read TTLs, includes and relative names.
22
- * @returns The records and rejections, or the size that passed `maxBytes`.
23
- * @example parse("www 300 IN A 192.0.2.1", { origin: "example.com" }) // records: [{ name: "www.example.com", ttl: 300, … }]
24
- */
25
- export declare function parse(text: string, options: ZoneFile.ParseOptions): Result<ZoneFile.Zone, ZoneFileError>;