@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/README.md CHANGED
@@ -1,248 +1,3 @@
1
- # @sdxc/zone-file
1
+ # Temporary Holding Version
2
2
 
3
- Read and write RFC 1035 DNS zone files, with the record data codec for every typed record.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm add @sdxc/zone-file
9
- ```
10
-
11
- `parse` returns a [`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result) value, which
12
- installs alongside this package.
13
-
14
- A zone file (a BIND "master file") is how DNS operators export, import, review and version a
15
- zone. `parse` reads the whole RFC 1035 grammar — `$ORIGIN`, `$TTL`, `$INCLUDE`, parentheses,
16
- blank owners, escapes, TTL units, any class and any type — and returns the records it read beside
17
- every entry it could not use, each with its line and a reason. `stringify` writes records back, so
18
- `parse(stringify(zone))` gives the same records.
19
-
20
- ## Usage
21
-
22
- ### Read A Zone File
23
-
24
- ```typescript
25
- import * as ZoneFile from "@sdxc/zone-file";
26
- import { isSuccess } from "@sdxc/result";
27
-
28
- let parsed = ZoneFile.parse(text, { origin: "example.com" });
29
- if (isSuccess(parsed)) {
30
- for (let record of parsed.data.records) console.log(record.name, record.ttl, record.type);
31
- for (let rejection of parsed.data.rejected) console.warn(rejection.line, rejection.message);
32
- }
33
- ```
34
-
35
- ### Write One
36
-
37
- ```typescript
38
- import * as ZoneFile from "@sdxc/zone-file";
39
-
40
- let text = ZoneFile.stringify(
41
- {
42
- origin: "example.com",
43
- ttl: 3600,
44
- records: [
45
- { name: "example.com", type: "MX", preference: 10, exchange: "mail.example.com" },
46
- { name: "www.example.com", ttl: 300, type: "CNAME", target: "example.com" },
47
- ],
48
- },
49
- { relative: true },
50
- );
51
- // $ORIGIN example.com.
52
- // $TTL 3600
53
- // @ IN MX 10 mail
54
- // www 300 IN CNAME @
55
- ```
56
-
57
- ### Read Record Data From Anywhere
58
-
59
- ```typescript
60
- import { parseRecordData } from "@sdxc/zone-file";
61
-
62
- parseRecordData("MX", "10 Mail.Example.com.");
63
- // success({ type: "MX", preference: 10, exchange: "mail.example.com" })
64
- ```
65
-
66
- ## API
67
-
68
- ### `parse(text, options): Result<ZoneFile.Zone, ZoneFileError>`
69
-
70
- Reads a zone file into `{ origin, records, rejected }`. The only failure is a `ZoneFileError`
71
- for input past `maxBytes`; any other problem — a bad line, an unknown directive, RDATA that does
72
- not fit its type — is a `Rejection` beside the records that did parse, so a caller wanting
73
- all-or-nothing checks `rejected.length`.
74
-
75
- | Option | Default | Meaning |
76
- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- |
77
- | `origin` | Required | The initial `$ORIGIN`; `@` and relative names resolve against it until a `$ORIGIN` line changes it |
78
- | `ttl` | `null` | The TTL a record gets when neither it, a `$TTL` nor an earlier record states one |
79
- | `include` | None | `(fileName, origin) => string \| null`, the text of an `$INCLUDE`d file; without it every `$INCLUDE` is rejected |
80
- | `relativeNames` | `"rfc1035"` | `"origin-suffix"` also reads a dotless name that equals the origin or ends in it as absolute |
81
- | `maxBytes` | 1 MiB | The largest input in UTF-8 bytes, counted across included files |
82
-
83
- The grammar:
84
-
85
- - `;` starts a comment outside quotes; a record's trailing comment is kept as `comment`.
86
- - `"…"` quotes a field, with `\"`, `\\` and `\DDD` escapes; a `;` or parenthesis inside is data.
87
- - `(` … `)` joins lines into one entry; `line` is where it starts and `endLine` where it ends.
88
- - `@` is the current origin, and a name without a trailing dot gets the origin appended, in owners
89
- and in the name fields of typed RDATA.
90
- - Names come back absolute, lowercased, without the trailing dot, `\DDD` and `\.` escapes resolved
91
- to one canonical spelling; the root is `"."`.
92
- - A line starting with whitespace takes the previous record's owner.
93
- - TTL and class are optional, in either order. A TTL is seconds or BIND units (`1h30m`, `2W`), up
94
- to 2³¹−1. A record without one takes `$TTL`, else the previous record's TTL, else `options.ttl`.
95
- - `$ORIGIN`, `$TTL` and `$INCLUDE file [origin]` are read. An included file starts with the given
96
- or current origin, and the origin, `$TTL` and owner revert after it; includes nest 8 deep.
97
- `$GENERATE` is rejected.
98
- - Classes are `IN`, `CH`, `HS`, `CS` and `CLASSnnn`; types are any mnemonic or `TYPEnnn`, and RFC
99
- 3597 generic data (`\# 4 C0000201`) is read for every type.
100
-
101
- Records outside the origin, a record listed twice, classes other than `IN` and every type are all
102
- returned: which of them to keep is the caller's decision.
103
-
104
- ### `stringify(zone, options?): string`
105
-
106
- Writes `{ origin?, ttl?, records }` (a parsed `Zone` is one) as one tab-separated line per record.
107
- `$ORIGIN` and `$TTL` lead when the input names them, and a record's TTL is left out when it equals
108
- `$TTL`. `relative: true` writes names under the origin relative to it and the apex as `@`;
109
- otherwise every name is absolute. An untyped record read under another origin gets an `$ORIGIN`
110
- line first, so relative names in its verbatim data keep their meaning. Comments are written back
111
- after `;`; spacing, standalone comments and parentheses are not kept.
112
-
113
- ### `parseRecordData(type, data): Result<ZoneFile.RecordData<Type>, RecordDataError>`
114
-
115
- Reads RDATA in presentation format into typed fields. Typed types: `A` and `AAAA` (`address`, IPv6
116
- in RFC 5952 form), `CNAME`, `PTR` and `DNAME` (`target`), `NS` (`host`), `MX` (`preference`,
117
- `exchange`), `TXT` (`text`, `strings`), `CAA` (`flags`, `critical`, `tag`, `value`), `SRV`
118
- (`priority`, `weight`, `port`, `target`) and `SOA` (`primary`, `mailbox`, `serial`, `refresh`,
119
- `retry`, `expire`, `minimum`, the timers accepting TTL units). Any other type returns
120
- `{ type, data }`. RFC 3597 generic data is decoded for the typed types. Names are not qualified:
121
- relative names stay relative.
122
-
123
- ```typescript
124
- parseRecordData("TXT", '"v=DKIM1; p=AAA" "BBB"');
125
- // success({ type: "TXT", text: "v=DKIM1; p=AAABBB", strings: ["v=DKIM1; p=AAA", "BBB"] })
126
- ```
127
-
128
- ### `formatRecordData(data): string`
129
-
130
- Prints record data in canonical presentation format, the inverse of `parseRecordData`: names
131
- absolute with the trailing dot, TXT strings and the CAA value quoted with every octet outside
132
- printable ASCII as `\DDD`, the CAA tag lowercased, untyped data as is. Two spellings of one record —
133
- `0 ISSUE "ca.example"` and its generic form — print to one string.
134
-
135
- ```typescript
136
- formatRecordData({ type: "MX", preference: 10, exchange: "mx.example.com" }); // "10 mx.example.com."
137
- ```
138
-
139
- ### `canonicalType(type): string` / `typeName(code): string`
140
-
141
- `canonicalType("txt")` is `"TXT"` and `canonicalType("TYPE16")` is `"TXT"`; `typeName(16)` is
142
- `"TXT"`, and a code without a mnemonic is `"TYPEnnn"`.
143
-
144
- ### Errors
145
-
146
- | Class | When |
147
- | ----------------- | -------------------------------------------------------------------------------------- |
148
- | `ZoneFileError` | `code: "too-large"`: the input, with its includes, passed `maxBytes`; `bytes` holds it |
149
- | `RecordDataError` | `parseRecordData` on data that does not fit the type |
150
-
151
- ### Rejection reasons
152
-
153
- | `reason` | When |
154
- | ----------------------- | ----------------------------------------------------------------------------------------- |
155
- | `malformed` | An unterminated quote, unbalanced parentheses, a bad owner or TTL, a missing type or data |
156
- | `invalid-data` | RDATA that does not fit its type; `message` holds the codec's error |
157
- | `missing-owner` | A blank owner with no earlier record to take it from |
158
- | `include` | `$INCLUDE` without the `include` option, a file it returned `null` for, or nesting past 8 |
159
- | `unsupported-directive` | `$GENERATE` or any other `$` word |
160
-
161
- Each rejection carries `file` (`null` for the text passed to `parse`), `line`, `endLine`, `input`
162
- (the entry as written) and `message`.
163
-
164
- ### `ZoneFile` namespace
165
-
166
- Types only: `Zone`, `Record`, `RecordFor<Type>`, `RecordFields`, `UntypedRecord`, `Rejection`,
167
- `RejectionReason`, `ParseOptions`, `ZoneInput`, `RecordInput`, `StringifyOptions`, `RecordType`,
168
- `TypedRecordType`, `RecordClass`, `RecordData<Type>`, and one data interface per typed type
169
- (`AData` through `SOAData`) plus `UnknownData`.
170
-
171
- ## Pattern: Import Only What You Track
172
-
173
- `parse` returns everything; the policy is a filter.
174
-
175
- ```typescript
176
- import * as ZoneFile from "@sdxc/zone-file";
177
- import { isFailure } from "@sdxc/result";
178
-
179
- let parsed = ZoneFile.parse(text, { origin: domain, relativeNames: "origin-suffix" });
180
- if (isFailure(parsed)) throw new Error(parsed.error.message);
181
-
182
- let tracked = parsed.data.records.filter(
183
- (record) =>
184
- record.class === "IN" &&
185
- ["A", "AAAA", "MX", "TXT"].includes(record.type) &&
186
- (record.name === domain || record.name.endsWith(`.${domain}`)),
187
- );
188
- ```
189
-
190
- ## Pattern: Normalize A Zone File For Review
191
-
192
- Reading and printing gives every zone one spelling — absolute names, canonical addresses, quoted
193
- TXT — so two exports diff only where their records differ.
194
-
195
- ```typescript
196
- import * as ZoneFile from "@sdxc/zone-file";
197
- import { unwrap } from "@sdxc/result";
198
-
199
- function normalize(text: string, origin: string): string {
200
- let zone = unwrap(ZoneFile.parse(text, { origin }));
201
- let records = zone.records.toSorted((a, b) => a.name.localeCompare(b.name));
202
- return ZoneFile.stringify({ origin, records }, { relative: true });
203
- }
204
- ```
205
-
206
- ## Pattern: Read A Zone With Includes
207
-
208
- ```typescript
209
- import { readFileSync } from "node:fs";
210
- import { join } from "node:path";
211
-
212
- import * as ZoneFile from "@sdxc/zone-file";
213
-
214
- let parsed = ZoneFile.parse(readFileSync("zones/example.com.zone", "utf8"), {
215
- origin: "example.com",
216
- include: (fileName) => readFileSync(join("zones", fileName), "utf8"),
217
- });
218
- ```
219
-
220
- ## Versioning
221
-
222
- Releases are dated rather than semantic. A version is the UTC date it was published,
223
- written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one
224
- release goes out per day.
225
-
226
- Those numbers say when, not what: a later date means a later release and carries no
227
- compatibility promise. Any release may change or remove an export.
228
-
229
- Depend on one exact date, and move it when you are ready to take the change:
230
-
231
- ```json
232
- {
233
- "dependencies": {
234
- "@sdxc/zone-file": "2026.9.4"
235
- }
236
- }
237
- ```
238
-
239
- A caret or tilde range reads the date as major, minor and patch, so it accepts every
240
- later release in the same year. An exact version keeps the upgrade yours to schedule.
241
-
242
- ## License
243
-
244
- MIT
245
-
246
- ## Author
247
-
248
- [Sergio Xalambrí](https://sergiodxa.com)
3
+ This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
package/package.json CHANGED
@@ -1,31 +1,6 @@
1
1
  {
2
- "name": "@sdxc/zone-file",
3
- "version": "0.0.0-pre.1",
4
- "description": "Read and write RFC 1035 DNS zone files, with the record data codec for every typed record",
5
- "license": "MIT",
6
- "type": "module",
7
- "exports": {
8
- ".": "./dist/index.js"
9
- },
10
- "dependencies": {
11
- "@sdxc/result": "2026.10.6"
12
- },
13
- "gitHead": "1c786b7f150de036687eda373ba05791637b9342",
14
- "publishConfig": {
15
- "access": "public"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/sergiodxa/monorepo.git",
20
- "directory": "packages/zone-file"
21
- },
22
- "homepage": "https://sdxc.sergiodxa.com/api/zone-file",
23
- "author": {
24
- "name": "Sergio Xalambrí",
25
- "url": "https://sergiodxa.com"
26
- },
27
- "funding": {
28
- "type": "github",
29
- "url": "https://github.com/sponsors/sergiodxa"
30
- }
31
- }
2
+ "name": "@sdxc/zone-file",
3
+ "version": "0.0.0-stage",
4
+ "stub": true,
5
+ "description": "Temporary package placeholder for staged publishing"
6
+ }
package/LICENSE.md DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Sergio Xalambrí
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,22 +0,0 @@
1
- /**
2
- * Reads the character-strings of TXT RDATA in presentation format (RFC 1035 section 5.1):
3
- * quoted strings keep their spaces, bare words split on whitespace, and `\"`, `\\` and
4
- * `\DDD` escapes are decoded, so a resolver's quoting never leaks into the text.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- import type { Result } from "@sdxc/result";
10
- import { RecordDataError } from "./errors.js";
11
- /**
12
- * Splits TXT presentation data into its character-strings. Escapes are decoded to octets
13
- * and each string is read as UTF-8, so text a resolver writes as `\195\169` comes back as
14
- * `é`. Fails on an unterminated quote or a decimal escape above 255.
15
- *
16
- * @param data - One or more quoted or bare character-strings.
17
- * @returns The strings in order, empty strings included.
18
- * @example readCharacterStrings('"v=DKIM1; p=AAA" "BBB"') // success(["v=DKIM1; p=AAA", "BBB"])
19
- */
20
- export declare function readCharacterStrings(data: string): Result<string[], RecordDataError>;
21
- /** Decodes octets as UTF-8, the reading applied to every character-string. */
22
- export declare function decodeOctets(octets: Uint8Array): string;
@@ -1,72 +0,0 @@
1
- import { failure, success } from "@sdxc/result";
2
- import { RecordDataError } from "./errors.js";
3
- /** Decodes a character-string's octets as UTF-8, replacing invalid sequences with U+FFFD. */
4
- const DECODER = new TextDecoder();
5
- /** Encodes literal characters into the octets they stand for. */
6
- const ENCODER = new TextEncoder();
7
- /** Whether a character separates character-strings. */
8
- function isSpace(char) {
9
- return char === " " || char === "\t" || char === "\n" || char === "\r";
10
- }
11
- /**
12
- * Splits TXT presentation data into its character-strings. Escapes are decoded to octets
13
- * and each string is read as UTF-8, so text a resolver writes as `\195\169` comes back as
14
- * `é`. Fails on an unterminated quote or a decimal escape above 255.
15
- *
16
- * @param data - One or more quoted or bare character-strings.
17
- * @returns The strings in order, empty strings included.
18
- * @example readCharacterStrings('"v=DKIM1; p=AAA" "BBB"') // success(["v=DKIM1; p=AAA", "BBB"])
19
- */
20
- export function readCharacterStrings(data) {
21
- let chars = Array.from(data);
22
- let strings = [];
23
- let index = 0;
24
- while (index < chars.length) {
25
- if (isSpace(chars[index])) {
26
- index += 1;
27
- continue;
28
- }
29
- let quoted = chars[index] === '"';
30
- if (quoted)
31
- index += 1;
32
- let octets = [];
33
- let terminated = !quoted;
34
- while (index < chars.length) {
35
- let char = chars[index] ?? "";
36
- if (quoted && char === '"') {
37
- terminated = true;
38
- index += 1;
39
- break;
40
- }
41
- if (!quoted && isSpace(char))
42
- break;
43
- if (char === "\\") {
44
- let digits = chars.slice(index + 1, index + 4).join("");
45
- if (/^\d{3}$/.test(digits)) {
46
- let octet = Number(digits);
47
- if (octet > 255)
48
- return failure(new RecordDataError(`Invalid escape \\${digits} in TXT data`));
49
- octets.push(octet);
50
- index += 4;
51
- continue;
52
- }
53
- let next = chars[index + 1];
54
- if (next === undefined)
55
- return failure(new RecordDataError("TXT data ends in a lone backslash"));
56
- octets.push(...ENCODER.encode(next));
57
- index += 2;
58
- continue;
59
- }
60
- octets.push(...ENCODER.encode(char));
61
- index += 1;
62
- }
63
- if (!terminated)
64
- return failure(new RecordDataError("TXT data has an unterminated quoted string"));
65
- strings.push(DECODER.decode(new Uint8Array(octets)));
66
- }
67
- return success(strings);
68
- }
69
- /** Decodes octets as UTF-8, the reading applied to every character-string. */
70
- export function decodeOctets(octets) {
71
- return DECODER.decode(octets);
72
- }
package/dist/errors.d.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * The two failures this package returns: RDATA that does not fit its type, and a zone file
3
- * too large to read at all. Everything else wrong with a file is a line-level rejection
4
- * beside the records that did parse.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- /** RDATA that does not parse for its type. */
10
- export declare class RecordDataError extends Error {
11
- name: string;
12
- }
13
- /** A zone file that `parse` refuses whole; its `code` names why. */
14
- export declare class ZoneFileError extends Error {
15
- name: string;
16
- /** The input, with its includes, passed `maxBytes`. */
17
- readonly code = "too-large";
18
- /** UTF-8 bytes read when the limit was passed, included files counted. */
19
- readonly bytes: number;
20
- /**
21
- * @param bytes - The bytes read so far.
22
- * @param maxBytes - The limit they passed.
23
- */
24
- constructor(bytes: number, maxBytes: number);
25
- }
package/dist/errors.js DELETED
@@ -1,28 +0,0 @@
1
- /**
2
- * The two failures this package returns: RDATA that does not fit its type, and a zone file
3
- * too large to read at all. Everything else wrong with a file is a line-level rejection
4
- * beside the records that did parse.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- /** RDATA that does not parse for its type. */
10
- export class RecordDataError extends Error {
11
- name = "RecordDataError";
12
- }
13
- /** A zone file that `parse` refuses whole; its `code` names why. */
14
- export class ZoneFileError extends Error {
15
- name = "ZoneFileError";
16
- /** The input, with its includes, passed `maxBytes`. */
17
- code = "too-large";
18
- /** UTF-8 bytes read when the limit was passed, included files counted. */
19
- bytes;
20
- /**
21
- * @param bytes - The bytes read so far.
22
- * @param maxBytes - The limit they passed.
23
- */
24
- constructor(bytes, maxBytes) {
25
- super(`The zone file is ${bytes} bytes, over the ${maxBytes} byte limit`);
26
- this.bytes = bytes;
27
- }
28
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Prints typed record data back to canonical presentation format, the inverse of
3
- * `parseRecordData`, so two spellings of one record (a zone file's text and a resolver's
4
- * RFC 3597 generic form) print to one string a diff can key on.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- import type * as ZoneFile from "./types.js";
10
- /**
11
- * Prints a record's data in canonical presentation format. Names print absolute with the
12
- * trailing dot (the root stays `"."`), TXT and CAA values print quoted with `"` and `\`
13
- * escaped and every octet outside printable ASCII as `\DDD`, and untyped data prints as is.
14
- * `parseRecordData(data.type, formatRecordData(data))` gives back `data`.
15
- *
16
- * @param data - Record data, as `parseRecordData` produces it.
17
- * @returns The RDATA in presentation format.
18
- * @example formatRecordData({ type: "MX", preference: 10, exchange: "mx.example.com" }) // "10 mx.example.com."
19
- * @example formatRecordData({ type: "CAA", flags: 0, critical: false, tag: "issue", value: "pki.goog" }) // '0 issue "pki.goog"'
20
- */
21
- export declare function formatRecordData(data: ZoneFile.RecordData<ZoneFile.RecordType>): string;
22
- /**
23
- * Prints record data with every name written by `printName`, which lets a zone file write
24
- * names relative to its origin while the data's other fields print as `formatRecordData`
25
- * prints them.
26
- *
27
- * @param data - Typed or untyped record data.
28
- * @param printName - Writes one canonical name as it appears in the output.
29
- */
30
- export declare function printRecordData(data: {
31
- type: string;
32
- }, printName: (name: string) => string): string;
@@ -1,84 +0,0 @@
1
- import { absoluteName } from "./names.js";
2
- /** Encodes a character-string into the octets `\DDD` escapes stand for. */
3
- const ENCODER = new TextEncoder();
4
- /**
5
- * Prints a record's data in canonical presentation format. Names print absolute with the
6
- * trailing dot (the root stays `"."`), TXT and CAA values print quoted with `"` and `\`
7
- * escaped and every octet outside printable ASCII as `\DDD`, and untyped data prints as is.
8
- * `parseRecordData(data.type, formatRecordData(data))` gives back `data`.
9
- *
10
- * @param data - Record data, as `parseRecordData` produces it.
11
- * @returns The RDATA in presentation format.
12
- * @example formatRecordData({ type: "MX", preference: 10, exchange: "mx.example.com" }) // "10 mx.example.com."
13
- * @example formatRecordData({ type: "CAA", flags: 0, critical: false, tag: "issue", value: "pki.goog" }) // '0 issue "pki.goog"'
14
- */
15
- export function formatRecordData(data) {
16
- return printRecordData(data, absoluteName);
17
- }
18
- /**
19
- * Prints record data with every name written by `printName`, which lets a zone file write
20
- * names relative to its origin while the data's other fields print as `formatRecordData`
21
- * prints them.
22
- *
23
- * @param data - Typed or untyped record data.
24
- * @param printName - Writes one canonical name as it appears in the output.
25
- */
26
- export function printRecordData(data, printName) {
27
- switch (data.type) {
28
- case "A":
29
- case "AAAA":
30
- return data.address;
31
- case "CNAME":
32
- case "PTR":
33
- case "DNAME":
34
- return printName(data.target);
35
- case "NS":
36
- return printName(data.host);
37
- case "MX": {
38
- let { preference, exchange } = data;
39
- return `${preference} ${printName(exchange)}`;
40
- }
41
- case "TXT": {
42
- let { strings } = data;
43
- return strings.length === 0 ? "\\# 0" : strings.map(quote).join(" ");
44
- }
45
- case "CAA": {
46
- let { flags, tag, value } = data;
47
- return `${flags} ${tag.toLowerCase()} ${quote(value)}`;
48
- }
49
- case "SRV": {
50
- let { priority, weight, port, target } = data;
51
- return `${priority} ${weight} ${port} ${printName(target)}`;
52
- }
53
- case "SOA": {
54
- let soa = data;
55
- return [
56
- printName(soa.primary),
57
- printName(soa.mailbox),
58
- soa.serial,
59
- soa.refresh,
60
- soa.retry,
61
- soa.expire,
62
- soa.minimum,
63
- ].join(" ");
64
- }
65
- default:
66
- return String(data.data);
67
- }
68
- }
69
- /**
70
- * One quoted character-string: `"` and `\` escaped, every octet outside printable ASCII
71
- * written `\DDD`, so the text reads back to the same UTF-8 whatever the reader's encoding.
72
- */
73
- function quote(text) {
74
- let printed = "";
75
- for (let octet of ENCODER.encode(text)) {
76
- if (octet === 0x22 || octet === 0x5c)
77
- printed += `\\${String.fromCharCode(octet)}`;
78
- else if (octet >= 0x20 && octet <= 0x7e)
79
- printed += String.fromCharCode(octet);
80
- else
81
- printed += `\\${String(octet).padStart(3, "0")}`;
82
- }
83
- return `"${printed}"`;
84
- }
package/dist/index.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Reads and writes RFC 1035 master files (BIND zone files), reporting every entry it cannot
3
- * use beside the records it read, and owns the RDATA presentation codec, so record data from
4
- * a zone file and from a resolver reads into the same typed fields.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- export type * from "./types.js";
10
- export type * as ZoneFile from "./types.js";
11
- export { RecordDataError, ZoneFileError } from "./errors.js";
12
- export { formatRecordData } from "./format-record-data.js";
13
- export { parse } from "./parse.js";
14
- export { parseRecordData } from "./parse-record-data.js";
15
- export { canonicalType, typeName } from "./record-types.js";
16
- export { stringify } from "./stringify.js";
package/dist/index.js DELETED
@@ -1,14 +0,0 @@
1
- /**
2
- * Reads and writes RFC 1035 master files (BIND zone files), reporting every entry it cannot
3
- * use beside the records it read, and owns the RDATA presentation codec, so record data from
4
- * a zone file and from a resolver reads into the same typed fields.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- export { RecordDataError, ZoneFileError } from "./errors.js";
10
- export { formatRecordData } from "./format-record-data.js";
11
- export { parse } from "./parse.js";
12
- export { parseRecordData } from "./parse-record-data.js";
13
- export { canonicalType, typeName } from "./record-types.js";
14
- export { stringify } from "./stringify.js";
package/dist/lexer.d.ts DELETED
@@ -1,37 +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
- /** One field of an entry, as written: quotes and escapes kept for the field's own reader. */
10
- export interface Token {
11
- text: string;
12
- quoted: boolean;
13
- }
14
- /** One logical entry of a zone file: a directive, a record, or something unreadable. */
15
- export interface Entry {
16
- line: number;
17
- endLine: number;
18
- /** The first line starts with whitespace, which leaves the owner to the previous entry. */
19
- blankOwner: boolean;
20
- tokens: Token[];
21
- /** The `;` comment outside parentheses, trimmed; `null` without one. */
22
- comment: string | null;
23
- /** Why the entry cannot be read; `null` when it lexed cleanly. */
24
- error: string | null;
25
- /** The entry as written, every line of it. */
26
- input: string;
27
- }
28
- /**
29
- * Splits text into entries. Blank and comment-only lines produce none; an entry whose
30
- * parentheses never close runs to the end of the text and carries an error, as does one
31
- * with a quote left open at the end of a line.
32
- *
33
- * @param text - The zone file's contents.
34
- * @returns The entries in file order.
35
- * @example lex("www IN A 192.0.2.1 ; web")[0]?.tokens.length // 4
36
- */
37
- export declare function lex(text: string): Entry[];