@tomgiee/tsdp 1.0.1 → 1.1.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/README.md +0 -39
- package/dist/src/builder/media-builder.d.ts.map +1 -1
- package/dist/src/builder/media-builder.js +5 -12
- package/dist/src/builder/session-builder.d.ts.map +1 -1
- package/dist/src/builder/session-builder.js +4 -14
- package/dist/src/index.d.ts +6 -10
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +37 -49
- package/dist/src/parser/core.d.ts +366 -0
- package/dist/src/parser/core.d.ts.map +1 -0
- package/dist/src/parser/core.js +802 -0
- package/dist/src/parser/field-parser.d.ts +51 -8
- package/dist/src/parser/field-parser.d.ts.map +1 -1
- package/dist/src/parser/field-parser.js +91 -23
- package/dist/src/parser/media-parser.d.ts +1 -1
- package/dist/src/parser/media-parser.d.ts.map +1 -1
- package/dist/src/parser/media-parser.js +9 -15
- package/dist/src/parser/session-parser.d.ts.map +1 -1
- package/dist/src/parser/session-parser.js +16 -17
- package/dist/src/parser/time-parser.d.ts +1 -1
- package/dist/src/parser/time-parser.d.ts.map +1 -1
- package/dist/src/parser/time-parser.js +8 -8
- package/dist/src/serializer/fields.d.ts +167 -0
- package/dist/src/serializer/fields.d.ts.map +1 -0
- package/dist/src/serializer/fields.js +320 -0
- package/dist/src/serializer/media-serializer.js +6 -7
- package/dist/src/serializer/session-serializer.js +13 -15
- package/dist/src/types/attributes.d.ts.map +1 -1
- package/dist/src/types/fields.d.ts +5 -5
- package/dist/src/types/fields.d.ts.map +1 -1
- package/dist/src/types/fields.js +4 -4
- package/dist/src/types/media.d.ts +9 -10
- package/dist/src/types/media.d.ts.map +1 -1
- package/dist/src/types/media.js +2 -4
- package/dist/src/types/network.d.ts +15 -56
- package/dist/src/types/network.d.ts.map +1 -1
- package/dist/src/types/network.js +3 -34
- package/dist/src/types/primitives.d.ts +3 -147
- package/dist/src/types/primitives.d.ts.map +1 -1
- package/dist/src/types/primitives.js +2 -171
- package/dist/src/types/session.d.ts +14 -14
- package/dist/src/types/session.d.ts.map +1 -1
- package/dist/src/types/time.d.ts +4 -4
- package/dist/src/types/time.d.ts.map +1 -1
- package/dist/src/types/time.js +8 -6
- package/dist/src/utils/address-parser.d.ts +4 -4
- package/dist/src/utils/address-parser.d.ts.map +1 -1
- package/dist/src/utils/address-parser.js +9 -16
- package/dist/src/validator/validator.d.ts +94 -0
- package/dist/src/validator/validator.d.ts.map +1 -0
- package/dist/src/validator/validator.js +573 -0
- package/dist/tests/unit/parser/attribute-parser.test.js +106 -107
- package/dist/tests/unit/parser/field-parser.test.js +66 -66
- package/dist/tests/unit/parser/media-parser.test.js +38 -38
- package/dist/tests/unit/parser/primitive-parser.test.js +89 -90
- package/dist/tests/unit/parser/scanner.test.js +32 -32
- package/dist/tests/unit/parser/time-parser.test.js +22 -22
- package/dist/tests/unit/serializer/attribute-serializer.test.js +22 -22
- package/dist/tests/unit/serializer/field-serializer.test.js +57 -57
- package/dist/tests/unit/serializer/media-serializer.test.js +5 -6
- package/dist/tests/unit/serializer/session-serializer.test.js +24 -24
- package/dist/tests/unit/serializer/time-serializer.test.js +16 -16
- package/dist/tests/unit/types/network.test.js +21 -56
- package/dist/tests/unit/types/primitives.test.js +0 -39
- package/dist/tests/unit/validator/media-validator.test.js +34 -35
- package/dist/tests/unit/validator/semantic-validator.test.js +36 -37
- package/dist/tests/unit/validator/session-validator.test.js +54 -54
- package/package.json +1 -1
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core parsing infrastructure for SDP (RFC 8866)
|
|
4
|
+
*
|
|
5
|
+
* This module consolidates:
|
|
6
|
+
* - Scanner: Low-level tokenization with position tracking
|
|
7
|
+
* - Primitive parsers: Basic grammar elements (tokens, integers, addresses, times)
|
|
8
|
+
* - Attribute parsers: SDP attributes (a= fields)
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.Scanner = void 0;
|
|
12
|
+
exports.parseToken = parseToken;
|
|
13
|
+
exports.parseInteger = parseInteger;
|
|
14
|
+
exports.parseByte = parseByte;
|
|
15
|
+
exports.parseNonWSString = parseNonWSString;
|
|
16
|
+
exports.parseByteString = parseByteString;
|
|
17
|
+
exports.parseIP4Address = parseIP4Address;
|
|
18
|
+
exports.parseIP6Address = parseIP6Address;
|
|
19
|
+
exports.parseFQDN = parseFQDN;
|
|
20
|
+
exports.parseNtpTime = parseNtpTime;
|
|
21
|
+
exports.parseTypedTime = parseTypedTime;
|
|
22
|
+
exports.parseEmail = parseEmail;
|
|
23
|
+
exports.parsePhone = parsePhone;
|
|
24
|
+
exports.parseURI = parseURI;
|
|
25
|
+
exports.parseAttributeField = parseAttributeField;
|
|
26
|
+
exports.parseRtpmapValue = parseRtpmapValue;
|
|
27
|
+
exports.parseFmtpValue = parseFmtpValue;
|
|
28
|
+
exports.parsePtimeValue = parsePtimeValue;
|
|
29
|
+
exports.parseMaxptimeValue = parseMaxptimeValue;
|
|
30
|
+
exports.parseFramerateValue = parseFramerateValue;
|
|
31
|
+
exports.parseQualityValue = parseQualityValue;
|
|
32
|
+
const errors_1 = require("../types/errors");
|
|
33
|
+
const address_parser_1 = require("../utils/address-parser");
|
|
34
|
+
const time_converter_1 = require("../utils/time-converter");
|
|
35
|
+
const attributes_1 = require("../types/attributes");
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Scanner Class
|
|
38
|
+
// ============================================================================
|
|
39
|
+
/**
|
|
40
|
+
* Scanner class for parsing SDP text
|
|
41
|
+
*
|
|
42
|
+
* Maintains position information and provides character-level operations
|
|
43
|
+
* for parsing SDP fields according to RFC 8866 grammar.
|
|
44
|
+
*/
|
|
45
|
+
class Scanner {
|
|
46
|
+
/**
|
|
47
|
+
* Create a new Scanner
|
|
48
|
+
*
|
|
49
|
+
* @param input - SDP text to scan
|
|
50
|
+
*/
|
|
51
|
+
constructor(input) {
|
|
52
|
+
this.input = input;
|
|
53
|
+
this.position = 0;
|
|
54
|
+
this.line = 1;
|
|
55
|
+
this.column = 1;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Peek at the current character without consuming it
|
|
59
|
+
*
|
|
60
|
+
* @returns Current character or null if at EOF
|
|
61
|
+
*/
|
|
62
|
+
peek() {
|
|
63
|
+
if (this.isEOF()) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return this.input[this.position];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Peek ahead n characters without consuming
|
|
70
|
+
*
|
|
71
|
+
* @param n - Number of characters to peek ahead
|
|
72
|
+
* @returns Character at position+n or null if beyond EOF
|
|
73
|
+
*/
|
|
74
|
+
peekAhead(n) {
|
|
75
|
+
const pos = this.position + n;
|
|
76
|
+
if (pos >= this.input.length) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return this.input[pos];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Advance to the next character and return the current one
|
|
83
|
+
*
|
|
84
|
+
* Updates line and column tracking when encountering newlines.
|
|
85
|
+
*
|
|
86
|
+
* @returns Current character or null if at EOF
|
|
87
|
+
*/
|
|
88
|
+
advance() {
|
|
89
|
+
if (this.isEOF()) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const ch = this.input[this.position];
|
|
93
|
+
this.position++;
|
|
94
|
+
// Update line and column tracking
|
|
95
|
+
if (ch === '\n') {
|
|
96
|
+
this.line++;
|
|
97
|
+
this.column = 1;
|
|
98
|
+
}
|
|
99
|
+
else if (ch === '\r') {
|
|
100
|
+
// Handle CRLF and CR
|
|
101
|
+
if (this.peek() === '\n') {
|
|
102
|
+
// CRLF - consume the LF too
|
|
103
|
+
this.position++;
|
|
104
|
+
}
|
|
105
|
+
this.line++;
|
|
106
|
+
this.column = 1;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.column++;
|
|
110
|
+
}
|
|
111
|
+
return ch;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read characters until a delimiter is found
|
|
115
|
+
*
|
|
116
|
+
* Does NOT consume the delimiter.
|
|
117
|
+
*
|
|
118
|
+
* @param delimiter - String or RegExp to match
|
|
119
|
+
* @returns String of characters read (may be empty)
|
|
120
|
+
*/
|
|
121
|
+
readUntil(delimiter) {
|
|
122
|
+
let result = '';
|
|
123
|
+
while (!this.isEOF()) {
|
|
124
|
+
const ch = this.peek();
|
|
125
|
+
if (typeof delimiter === 'string') {
|
|
126
|
+
// String delimiter - check for exact match
|
|
127
|
+
if (ch === delimiter) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// RegExp delimiter - test current character
|
|
133
|
+
if (delimiter.test(ch)) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
result += this.advance();
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read while a condition is true
|
|
143
|
+
*
|
|
144
|
+
* @param predicate - Function that returns true to continue reading
|
|
145
|
+
* @returns String of characters read
|
|
146
|
+
*/
|
|
147
|
+
readWhile(predicate) {
|
|
148
|
+
let result = '';
|
|
149
|
+
while (!this.isEOF()) {
|
|
150
|
+
const ch = this.peek();
|
|
151
|
+
if (!predicate(ch)) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
result += this.advance();
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Skip whitespace characters (space and tab)
|
|
160
|
+
*
|
|
161
|
+
* Does NOT skip newlines (CRLF/LF/CR).
|
|
162
|
+
*/
|
|
163
|
+
skipWhitespace() {
|
|
164
|
+
while (!this.isEOF()) {
|
|
165
|
+
const ch = this.peek();
|
|
166
|
+
if (ch !== ' ' && ch !== '\t') {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
this.advance();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Skip to the next line
|
|
174
|
+
*
|
|
175
|
+
* Consumes all characters until and including the next line ending (CRLF/LF/CR).
|
|
176
|
+
*/
|
|
177
|
+
skipToNextLine() {
|
|
178
|
+
while (!this.isEOF()) {
|
|
179
|
+
const ch = this.advance();
|
|
180
|
+
if (ch === '\n' || ch === '\r') {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get current position in the input
|
|
187
|
+
*
|
|
188
|
+
* @returns Position object with offset, line, and column
|
|
189
|
+
*/
|
|
190
|
+
getPosition() {
|
|
191
|
+
return {
|
|
192
|
+
offset: this.position,
|
|
193
|
+
line: this.line,
|
|
194
|
+
column: this.column,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check if at end of file
|
|
199
|
+
*
|
|
200
|
+
* @returns true if no more characters to read
|
|
201
|
+
*/
|
|
202
|
+
isEOF() {
|
|
203
|
+
return this.position >= this.input.length;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Expect a specific string at the current position
|
|
207
|
+
*
|
|
208
|
+
* Consumes the string if it matches, throws otherwise.
|
|
209
|
+
*
|
|
210
|
+
* @param expected - String to expect
|
|
211
|
+
* @throws ParseError if string doesn't match
|
|
212
|
+
*/
|
|
213
|
+
expect(expected) {
|
|
214
|
+
const start = this.getPosition();
|
|
215
|
+
for (let i = 0; i < expected.length; i++) {
|
|
216
|
+
const ch = this.peek();
|
|
217
|
+
if (ch === null) {
|
|
218
|
+
throw new errors_1.ParseError(`Expected "${expected}" but reached end of input`, start.line, start.column, this.getContext(start));
|
|
219
|
+
}
|
|
220
|
+
if (ch !== expected[i]) {
|
|
221
|
+
throw new errors_1.ParseError(`Expected "${expected}" but found "${ch}"`, start.line, start.column, this.getContext(start));
|
|
222
|
+
}
|
|
223
|
+
this.advance();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Expect a character that matches a condition
|
|
228
|
+
*
|
|
229
|
+
* Consumes the character if it matches, throws otherwise.
|
|
230
|
+
*
|
|
231
|
+
* @param predicate - Function that returns true for valid character
|
|
232
|
+
* @param description - Description of expected character for error messages
|
|
233
|
+
* @returns The matched character
|
|
234
|
+
* @throws ParseError if character doesn't match
|
|
235
|
+
*/
|
|
236
|
+
expectChar(predicate, description) {
|
|
237
|
+
const pos = this.getPosition();
|
|
238
|
+
const ch = this.peek();
|
|
239
|
+
if (ch === null) {
|
|
240
|
+
throw new errors_1.ParseError(`Expected ${description} but reached end of input`, pos.line, pos.column, this.getContext(pos));
|
|
241
|
+
}
|
|
242
|
+
if (!predicate(ch)) {
|
|
243
|
+
throw new errors_1.ParseError(`Expected ${description} but found "${ch}"`, pos.line, pos.column, this.getContext(pos));
|
|
244
|
+
}
|
|
245
|
+
this.advance();
|
|
246
|
+
return ch;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Expect end of line (CRLF, LF, or CR)
|
|
250
|
+
*
|
|
251
|
+
* @throws ParseError if not at end of line
|
|
252
|
+
*/
|
|
253
|
+
expectEOL() {
|
|
254
|
+
const pos = this.getPosition();
|
|
255
|
+
const ch = this.peek();
|
|
256
|
+
if (ch === null) {
|
|
257
|
+
// EOF is acceptable as EOL
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (ch !== '\r' && ch !== '\n') {
|
|
261
|
+
throw new errors_1.ParseError(`Expected end of line but found "${ch}"`, pos.line, pos.column, this.getContext(pos));
|
|
262
|
+
}
|
|
263
|
+
this.advance();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Expect space character
|
|
267
|
+
*
|
|
268
|
+
* @throws ParseError if not a space
|
|
269
|
+
*/
|
|
270
|
+
expectSpace() {
|
|
271
|
+
const pos = this.getPosition();
|
|
272
|
+
const ch = this.peek();
|
|
273
|
+
if (ch !== ' ') {
|
|
274
|
+
throw new errors_1.ParseError(`Expected space but found "${ch}"`, pos.line, pos.column, this.getContext(pos));
|
|
275
|
+
}
|
|
276
|
+
this.advance();
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get context around a position for error messages
|
|
280
|
+
*
|
|
281
|
+
* Returns a snippet of the input around the position.
|
|
282
|
+
*
|
|
283
|
+
* @param pos - Position to get context for
|
|
284
|
+
* @returns Context string (may be empty)
|
|
285
|
+
*/
|
|
286
|
+
getContext(pos) {
|
|
287
|
+
const lines = this.input.split('\n');
|
|
288
|
+
if (pos.line - 1 < lines.length) {
|
|
289
|
+
return lines[pos.line - 1];
|
|
290
|
+
}
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get remaining input from current position
|
|
295
|
+
*
|
|
296
|
+
* @returns Remaining input string
|
|
297
|
+
*/
|
|
298
|
+
getRemainingInput() {
|
|
299
|
+
return this.input.substring(this.position);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get a substring of the input
|
|
303
|
+
*
|
|
304
|
+
* @param start - Start position
|
|
305
|
+
* @param end - End position (optional, defaults to current position)
|
|
306
|
+
* @returns Substring
|
|
307
|
+
*/
|
|
308
|
+
getSubstring(start, end) {
|
|
309
|
+
return this.input.substring(start, end !== null && end !== void 0 ? end : this.position);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
exports.Scanner = Scanner;
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Character Classification Helpers
|
|
315
|
+
// ============================================================================
|
|
316
|
+
/**
|
|
317
|
+
* Check if a character is a valid token character (RFC 8866)
|
|
318
|
+
*/
|
|
319
|
+
function isTokenChar(ch) {
|
|
320
|
+
const code = ch.charCodeAt(0);
|
|
321
|
+
// token-char per RFC 8866
|
|
322
|
+
return (code === 0x21 || // !
|
|
323
|
+
(code >= 0x23 && code <= 0x27) || // # $ % & '
|
|
324
|
+
(code >= 0x2a && code <= 0x2b) || // * +
|
|
325
|
+
(code >= 0x2d && code <= 0x2e) || // - .
|
|
326
|
+
(code >= 0x30 && code <= 0x39) || // 0-9
|
|
327
|
+
(code >= 0x41 && code <= 0x5a) || // A-Z
|
|
328
|
+
(code >= 0x5e && code <= 0x7e) // ^ _ ` a-z { | } ~
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Check if character is a digit
|
|
333
|
+
*/
|
|
334
|
+
function isDigit(ch) {
|
|
335
|
+
return ch >= '0' && ch <= '9';
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Check if character is non-whitespace
|
|
339
|
+
*/
|
|
340
|
+
function isNonWhitespace(ch) {
|
|
341
|
+
return ch !== ' ' && ch !== '\t' && ch !== '\r' && ch !== '\n';
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Check if character is valid in byte-string
|
|
345
|
+
*/
|
|
346
|
+
function isByteStringChar(ch) {
|
|
347
|
+
const code = ch.charCodeAt(0);
|
|
348
|
+
return ((code >= 0x01 && code <= 0x09) ||
|
|
349
|
+
(code >= 0x0b && code <= 0x0c) ||
|
|
350
|
+
(code >= 0x0e && code <= 0xff));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Check if character is a hexadecimal digit
|
|
354
|
+
*/
|
|
355
|
+
function isHexDigit(ch) {
|
|
356
|
+
return ((ch >= '0' && ch <= '9') ||
|
|
357
|
+
(ch >= 'a' && ch <= 'f') ||
|
|
358
|
+
(ch >= 'A' && ch <= 'F'));
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Check if character is alphanumeric
|
|
362
|
+
*/
|
|
363
|
+
function isAlphanumeric(ch) {
|
|
364
|
+
return ((ch >= '0' && ch <= '9') ||
|
|
365
|
+
(ch >= 'a' && ch <= 'z') ||
|
|
366
|
+
(ch >= 'A' && ch <= 'Z'));
|
|
367
|
+
}
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// Token Parsing
|
|
370
|
+
// ============================================================================
|
|
371
|
+
/**
|
|
372
|
+
* Parse a token (RFC 8866 Section 9)
|
|
373
|
+
*
|
|
374
|
+
* token = 1*token-char
|
|
375
|
+
* token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7E
|
|
376
|
+
*
|
|
377
|
+
* Essentially: any visible ASCII character except space and separators like ()[]{}:;,<>?=@\"
|
|
378
|
+
*
|
|
379
|
+
* @param scanner - Scanner instance
|
|
380
|
+
* @returns Parsed token string
|
|
381
|
+
* @throws ParseError if no valid token found
|
|
382
|
+
*/
|
|
383
|
+
function parseToken(scanner) {
|
|
384
|
+
const start = scanner.getPosition();
|
|
385
|
+
const token = scanner.readWhile(isTokenChar);
|
|
386
|
+
if (token.length === 0) {
|
|
387
|
+
throw new errors_1.ParseError('Expected token', start.line, start.column);
|
|
388
|
+
}
|
|
389
|
+
return token;
|
|
390
|
+
}
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// Integer Parsing
|
|
393
|
+
// ============================================================================
|
|
394
|
+
/**
|
|
395
|
+
* Parse an integer
|
|
396
|
+
*
|
|
397
|
+
* @param scanner - Scanner instance
|
|
398
|
+
* @returns Parsed integer
|
|
399
|
+
* @throws ParseError if no valid integer found
|
|
400
|
+
*/
|
|
401
|
+
function parseInteger(scanner) {
|
|
402
|
+
const start = scanner.getPosition();
|
|
403
|
+
const digits = scanner.readWhile(isDigit);
|
|
404
|
+
if (digits.length === 0) {
|
|
405
|
+
throw new errors_1.ParseError('Expected integer', start.line, start.column);
|
|
406
|
+
}
|
|
407
|
+
const value = parseInt(digits, 10);
|
|
408
|
+
if (isNaN(value)) {
|
|
409
|
+
throw new errors_1.ParseError(`Invalid integer: ${digits}`, start.line, start.column);
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Parse a byte (0-255)
|
|
415
|
+
*
|
|
416
|
+
* @param scanner - Scanner instance
|
|
417
|
+
* @returns Parsed byte value
|
|
418
|
+
* @throws ParseError if not a valid byte
|
|
419
|
+
*/
|
|
420
|
+
function parseByte(scanner) {
|
|
421
|
+
const start = scanner.getPosition();
|
|
422
|
+
const value = parseInteger(scanner);
|
|
423
|
+
if (value < 0 || value > 255) {
|
|
424
|
+
throw new errors_1.ParseError(`Byte value out of range: ${value} (expected 0-255)`, start.line, start.column);
|
|
425
|
+
}
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
// ============================================================================
|
|
429
|
+
// String Parsing
|
|
430
|
+
// ============================================================================
|
|
431
|
+
/**
|
|
432
|
+
* Parse a non-whitespace string
|
|
433
|
+
*
|
|
434
|
+
* Reads characters until whitespace or end of line.
|
|
435
|
+
*
|
|
436
|
+
* @param scanner - Scanner instance
|
|
437
|
+
* @returns Parsed string
|
|
438
|
+
* @throws ParseError if empty
|
|
439
|
+
*/
|
|
440
|
+
function parseNonWSString(scanner) {
|
|
441
|
+
const start = scanner.getPosition();
|
|
442
|
+
const str = scanner.readWhile(isNonWhitespace);
|
|
443
|
+
if (str.length === 0) {
|
|
444
|
+
throw new errors_1.ParseError('Expected non-whitespace string', start.line, start.column);
|
|
445
|
+
}
|
|
446
|
+
return str;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Parse a byte-string (RFC 8866 Section 9)
|
|
450
|
+
*
|
|
451
|
+
* byte-string = 1*(%x01-09 / %x0B-0C / %x0E-FF)
|
|
452
|
+
* (any byte except NUL, CR, LF)
|
|
453
|
+
*
|
|
454
|
+
* @param scanner - Scanner instance
|
|
455
|
+
* @returns Parsed byte-string
|
|
456
|
+
* @throws ParseError if invalid characters found
|
|
457
|
+
*/
|
|
458
|
+
function parseByteString(scanner) {
|
|
459
|
+
const start = scanner.getPosition();
|
|
460
|
+
const str = scanner.readWhile(isByteStringChar);
|
|
461
|
+
if (str.length === 0) {
|
|
462
|
+
throw new errors_1.ParseError('Expected byte-string', start.line, start.column);
|
|
463
|
+
}
|
|
464
|
+
return str;
|
|
465
|
+
}
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// IP Address Parsing
|
|
468
|
+
// ============================================================================
|
|
469
|
+
/**
|
|
470
|
+
* Parse an IPv4 address
|
|
471
|
+
*
|
|
472
|
+
* Format: a.b.c.d where each octet is 0-255
|
|
473
|
+
*
|
|
474
|
+
* @param scanner - Scanner instance
|
|
475
|
+
* @returns Parsed IPv4 address
|
|
476
|
+
* @throws ParseError if invalid IPv4 address
|
|
477
|
+
*/
|
|
478
|
+
function parseIP4Address(scanner) {
|
|
479
|
+
const start = scanner.getPosition();
|
|
480
|
+
// Read until whitespace or special characters
|
|
481
|
+
const addr = scanner.readWhile(ch => isDigit(ch) || ch === '.');
|
|
482
|
+
if (!(0, address_parser_1.isIP4Address)(addr)) {
|
|
483
|
+
throw new errors_1.ParseError(`Invalid IPv4 address: ${addr}`, start.line, start.column);
|
|
484
|
+
}
|
|
485
|
+
return addr;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Parse an IPv6 address
|
|
489
|
+
*
|
|
490
|
+
* Supports all IPv6 formats including compression (::) and mixed IPv4
|
|
491
|
+
*
|
|
492
|
+
* @param scanner - Scanner instance
|
|
493
|
+
* @returns Parsed IPv6 address
|
|
494
|
+
* @throws ParseError if invalid IPv6 address
|
|
495
|
+
*/
|
|
496
|
+
function parseIP6Address(scanner) {
|
|
497
|
+
const start = scanner.getPosition();
|
|
498
|
+
// Read until whitespace (IPv6 can contain : and . and hex digits)
|
|
499
|
+
const addr = scanner.readWhile(ch => isHexDigit(ch) || ch === ':' || ch === '.');
|
|
500
|
+
if (!(0, address_parser_1.isIP6Address)(addr)) {
|
|
501
|
+
throw new errors_1.ParseError(`Invalid IPv6 address: ${addr}`, start.line, start.column);
|
|
502
|
+
}
|
|
503
|
+
return addr;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Parse an FQDN (Fully Qualified Domain Name)
|
|
507
|
+
*
|
|
508
|
+
* @param scanner - Scanner instance
|
|
509
|
+
* @returns Parsed FQDN
|
|
510
|
+
* @throws ParseError if invalid FQDN
|
|
511
|
+
*/
|
|
512
|
+
function parseFQDN(scanner) {
|
|
513
|
+
const start = scanner.getPosition();
|
|
514
|
+
// Read until whitespace (FQDN can contain alphanumeric, -, and .)
|
|
515
|
+
const fqdn = scanner.readWhile(ch => isAlphanumeric(ch) || ch === '-' || ch === '.');
|
|
516
|
+
if (!(0, address_parser_1.isFQDN)(fqdn)) {
|
|
517
|
+
throw new errors_1.ParseError(`Invalid FQDN: ${fqdn}`, start.line, start.column);
|
|
518
|
+
}
|
|
519
|
+
return fqdn;
|
|
520
|
+
}
|
|
521
|
+
// ============================================================================
|
|
522
|
+
// Time Parsing
|
|
523
|
+
// ============================================================================
|
|
524
|
+
/**
|
|
525
|
+
* Parse an NTP timestamp
|
|
526
|
+
*
|
|
527
|
+
* NTP time is seconds since January 1, 1900 00:00:00 UTC
|
|
528
|
+
* 0 means unbounded/permanent
|
|
529
|
+
*
|
|
530
|
+
* @param scanner - Scanner instance
|
|
531
|
+
* @returns Parsed NTP time
|
|
532
|
+
* @throws ParseError if invalid
|
|
533
|
+
*/
|
|
534
|
+
function parseNtpTime(scanner) {
|
|
535
|
+
const start = scanner.getPosition();
|
|
536
|
+
const value = parseInteger(scanner);
|
|
537
|
+
if (value < 0) {
|
|
538
|
+
throw new errors_1.ParseError(`NTP time cannot be negative: ${value}`, start.line, start.column);
|
|
539
|
+
}
|
|
540
|
+
return value;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Parse a typed time value
|
|
544
|
+
*
|
|
545
|
+
* Format: [-]<integer>[d|h|m|s]
|
|
546
|
+
* - d = days
|
|
547
|
+
* - h = hours
|
|
548
|
+
* - m = minutes
|
|
549
|
+
* - s or no suffix = seconds
|
|
550
|
+
* - Can be negative (for timezone offsets)
|
|
551
|
+
*
|
|
552
|
+
* @param scanner - Scanner instance
|
|
553
|
+
* @returns Parsed typed time
|
|
554
|
+
* @throws ParseError if invalid
|
|
555
|
+
*/
|
|
556
|
+
function parseTypedTime(scanner) {
|
|
557
|
+
const start = scanner.getPosition();
|
|
558
|
+
// Read optional minus sign, digits, and optional unit suffix
|
|
559
|
+
const timeStr = scanner.readWhile(ch => ch === '-' || isDigit(ch) || ch === 'd' || ch === 'h' || ch === 'm' || ch === 's');
|
|
560
|
+
try {
|
|
561
|
+
return (0, time_converter_1.parseTypedTime)(timeStr);
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
throw new errors_1.ParseError(`Invalid typed time: ${timeStr}`, start.line, start.column);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// ============================================================================
|
|
568
|
+
// Email, Phone, and URI Parsing
|
|
569
|
+
// ============================================================================
|
|
570
|
+
/**
|
|
571
|
+
* Parse an email address or display name with email
|
|
572
|
+
*
|
|
573
|
+
* Format: email-address / displayable-name <email-address>
|
|
574
|
+
*
|
|
575
|
+
* @param scanner - Scanner instance
|
|
576
|
+
* @returns Parsed email string
|
|
577
|
+
*/
|
|
578
|
+
function parseEmail(scanner) {
|
|
579
|
+
// Email can contain most characters, read until end of line
|
|
580
|
+
return scanner.readUntil(/[\r\n]/);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Parse a phone number or display name with phone
|
|
584
|
+
*
|
|
585
|
+
* Format: phone-number / displayable-name <phone-number>
|
|
586
|
+
*
|
|
587
|
+
* @param scanner - Scanner instance
|
|
588
|
+
* @returns Parsed phone string
|
|
589
|
+
*/
|
|
590
|
+
function parsePhone(scanner) {
|
|
591
|
+
// Phone can contain most characters, read until end of line
|
|
592
|
+
return scanner.readUntil(/[\r\n]/);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Parse a URI
|
|
596
|
+
*
|
|
597
|
+
* @param scanner - Scanner instance
|
|
598
|
+
* @returns Parsed URI string
|
|
599
|
+
*/
|
|
600
|
+
function parseURI(scanner) {
|
|
601
|
+
// URI can contain many characters, read until end of line
|
|
602
|
+
return scanner.readUntil(/[\r\n]/);
|
|
603
|
+
}
|
|
604
|
+
// ============================================================================
|
|
605
|
+
// Attribute Field Parser (a=)
|
|
606
|
+
// ============================================================================
|
|
607
|
+
/**
|
|
608
|
+
* Parse attribute field
|
|
609
|
+
*
|
|
610
|
+
* Format:
|
|
611
|
+
* - Property attribute: a=<attribute-name>
|
|
612
|
+
* - Value attribute: a=<attribute-name>:<attribute-value>
|
|
613
|
+
*
|
|
614
|
+
* Property attributes have no value (e.g., a=recvonly, a=sendrecv).
|
|
615
|
+
* Value attributes have a value after colon (e.g., a=rtpmap:0 PCMU/8000).
|
|
616
|
+
*
|
|
617
|
+
* @param scanner - Scanner instance
|
|
618
|
+
* @returns Attribute object
|
|
619
|
+
* @throws ParseError if invalid format
|
|
620
|
+
*/
|
|
621
|
+
function parseAttributeField(scanner) {
|
|
622
|
+
// Parse attribute name (token up to : or end of line)
|
|
623
|
+
const name = parseToken(scanner);
|
|
624
|
+
// Check for colon indicating value attribute
|
|
625
|
+
const ch = scanner.peek();
|
|
626
|
+
if (ch === ':') {
|
|
627
|
+
// Value attribute
|
|
628
|
+
scanner.advance(); // consume ':'
|
|
629
|
+
// Read value until end of line
|
|
630
|
+
const value = scanner.readUntil(/[\r\n]/);
|
|
631
|
+
return (0, attributes_1.createValueAttribute)(name, value);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
// Property attribute (no value)
|
|
635
|
+
// Return predefined attributes for known types
|
|
636
|
+
switch (name) {
|
|
637
|
+
case 'recvonly':
|
|
638
|
+
return (0, attributes_1.createRecvonly)();
|
|
639
|
+
case 'sendrecv':
|
|
640
|
+
return (0, attributes_1.createSendrecv)();
|
|
641
|
+
case 'sendonly':
|
|
642
|
+
return (0, attributes_1.createSendonly)();
|
|
643
|
+
case 'inactive':
|
|
644
|
+
return (0, attributes_1.createInactive)();
|
|
645
|
+
default:
|
|
646
|
+
return (0, attributes_1.createPropertyAttribute)(name);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// ============================================================================
|
|
651
|
+
// Specialized Attribute Value Parsers
|
|
652
|
+
// ============================================================================
|
|
653
|
+
/**
|
|
654
|
+
* Parse rtpmap attribute value
|
|
655
|
+
*
|
|
656
|
+
* Format: <payload-type> <encoding-name>/<clock-rate>[/<encoding-params>]
|
|
657
|
+
*
|
|
658
|
+
* Examples:
|
|
659
|
+
* - "0 PCMU/8000"
|
|
660
|
+
* - "96 H264/90000"
|
|
661
|
+
* - "97 L16/8000/2" (2 channels)
|
|
662
|
+
*
|
|
663
|
+
* @param value - rtpmap attribute value string
|
|
664
|
+
* @returns Parsed RtpmapValue
|
|
665
|
+
* @throws ParseError if invalid format
|
|
666
|
+
*/
|
|
667
|
+
function parseRtpmapValue(value) {
|
|
668
|
+
// Split into payload type and encoding spec
|
|
669
|
+
const parts = value.split(/\s+/);
|
|
670
|
+
if (parts.length !== 2) {
|
|
671
|
+
throw new errors_1.ParseError(`Invalid rtpmap format: expected "<payload-type> <encoding-spec>", got "${value}"`, 0, 0);
|
|
672
|
+
}
|
|
673
|
+
const payloadType = parseInt(parts[0], 10);
|
|
674
|
+
if (isNaN(payloadType) || payloadType < 0 || payloadType > 127) {
|
|
675
|
+
throw new errors_1.ParseError(`Invalid payload type: ${parts[0]} (expected 0-127)`, 0, 0);
|
|
676
|
+
}
|
|
677
|
+
// Parse encoding spec: <encoding-name>/<clock-rate>[/<encoding-params>]
|
|
678
|
+
const encodingParts = parts[1].split('/');
|
|
679
|
+
if (encodingParts.length < 2) {
|
|
680
|
+
throw new errors_1.ParseError(`Invalid encoding spec: expected "<encoding-name>/<clock-rate>", got "${parts[1]}"`, 0, 0);
|
|
681
|
+
}
|
|
682
|
+
const encodingName = encodingParts[0];
|
|
683
|
+
const clockRate = parseInt(encodingParts[1], 10);
|
|
684
|
+
if (isNaN(clockRate) || clockRate <= 0) {
|
|
685
|
+
throw new errors_1.ParseError(`Invalid clock rate: ${encodingParts[1]} (must be positive)`, 0, 0);
|
|
686
|
+
}
|
|
687
|
+
// Optional encoding parameters (e.g., number of audio channels)
|
|
688
|
+
let encodingParams;
|
|
689
|
+
if (encodingParts.length > 2) {
|
|
690
|
+
encodingParams = parseInt(encodingParts[2], 10);
|
|
691
|
+
if (isNaN(encodingParams) || encodingParams <= 0) {
|
|
692
|
+
throw new errors_1.ParseError(`Invalid encoding parameters: ${encodingParts[2]} (must be positive)`, 0, 0);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
payloadType,
|
|
697
|
+
encodingName,
|
|
698
|
+
clockRate,
|
|
699
|
+
encodingParams,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Parse fmtp attribute value
|
|
704
|
+
*
|
|
705
|
+
* Format: <format> <format-specific-params>
|
|
706
|
+
*
|
|
707
|
+
* Examples:
|
|
708
|
+
* - "96 profile-level-id=42e01f;packetization-mode=1"
|
|
709
|
+
* - "97 apt=96"
|
|
710
|
+
*
|
|
711
|
+
* @param value - fmtp attribute value string
|
|
712
|
+
* @returns Parsed FmtpValue
|
|
713
|
+
* @throws ParseError if invalid format
|
|
714
|
+
*/
|
|
715
|
+
function parseFmtpValue(value) {
|
|
716
|
+
// Split on first whitespace
|
|
717
|
+
const spaceIndex = value.search(/\s/);
|
|
718
|
+
if (spaceIndex === -1) {
|
|
719
|
+
throw new errors_1.ParseError(`Invalid fmtp format: expected "<format> <parameters>", got "${value}"`, 0, 0);
|
|
720
|
+
}
|
|
721
|
+
const format = value.substring(0, spaceIndex);
|
|
722
|
+
const parameters = value.substring(spaceIndex + 1).trim();
|
|
723
|
+
if (parameters.length === 0) {
|
|
724
|
+
throw new errors_1.ParseError(`Invalid fmtp format: parameters cannot be empty`, 0, 0);
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
format,
|
|
728
|
+
parameters,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Parse ptime attribute value
|
|
733
|
+
*
|
|
734
|
+
* Format: <packet-time-in-milliseconds>
|
|
735
|
+
*
|
|
736
|
+
* RFC 8866 Section 6.4: ptime-value = non-zero-int-or-real
|
|
737
|
+
* Decimal values are allowed (e.g., "20.5")
|
|
738
|
+
*
|
|
739
|
+
* @param value - ptime attribute value string
|
|
740
|
+
* @returns Packet time in milliseconds (can be fractional)
|
|
741
|
+
* @throws ParseError if invalid
|
|
742
|
+
*/
|
|
743
|
+
function parsePtimeValue(value) {
|
|
744
|
+
const ptime = parseFloat(value);
|
|
745
|
+
if (isNaN(ptime) || ptime <= 0) {
|
|
746
|
+
throw new errors_1.ParseError(`Invalid ptime value: ${value} (must be positive number)`, 0, 0);
|
|
747
|
+
}
|
|
748
|
+
return ptime;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Parse maxptime attribute value
|
|
752
|
+
*
|
|
753
|
+
* Format: <max-packet-time-in-milliseconds>
|
|
754
|
+
*
|
|
755
|
+
* RFC 8866 Section 6.5: maxptime-value = non-zero-int-or-real
|
|
756
|
+
* Decimal values are allowed (e.g., "40.0")
|
|
757
|
+
*
|
|
758
|
+
* @param value - maxptime attribute value string
|
|
759
|
+
* @returns Maximum packet time in milliseconds (can be fractional)
|
|
760
|
+
* @throws ParseError if invalid
|
|
761
|
+
*/
|
|
762
|
+
function parseMaxptimeValue(value) {
|
|
763
|
+
const maxptime = parseFloat(value);
|
|
764
|
+
if (isNaN(maxptime) || maxptime <= 0) {
|
|
765
|
+
throw new errors_1.ParseError(`Invalid maxptime value: ${value} (must be positive number)`, 0, 0);
|
|
766
|
+
}
|
|
767
|
+
return maxptime;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Parse framerate attribute value
|
|
771
|
+
*
|
|
772
|
+
* Format: <frames-per-second>
|
|
773
|
+
*
|
|
774
|
+
* @param value - framerate attribute value string
|
|
775
|
+
* @returns Frame rate (frames per second)
|
|
776
|
+
* @throws ParseError if invalid
|
|
777
|
+
*/
|
|
778
|
+
function parseFramerateValue(value) {
|
|
779
|
+
const framerate = parseFloat(value);
|
|
780
|
+
if (isNaN(framerate) || framerate <= 0) {
|
|
781
|
+
throw new errors_1.ParseError(`Invalid framerate value: ${value} (must be positive number)`, 0, 0);
|
|
782
|
+
}
|
|
783
|
+
return framerate;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Parse quality attribute value
|
|
787
|
+
*
|
|
788
|
+
* Format: <quality-value>
|
|
789
|
+
*
|
|
790
|
+
* Quality is 0-10, where 10 is best quality.
|
|
791
|
+
*
|
|
792
|
+
* @param value - quality attribute value string
|
|
793
|
+
* @returns Quality value (0-10)
|
|
794
|
+
* @throws ParseError if invalid
|
|
795
|
+
*/
|
|
796
|
+
function parseQualityValue(value) {
|
|
797
|
+
const quality = parseInt(value, 10);
|
|
798
|
+
if (isNaN(quality) || quality < 0 || quality > 10) {
|
|
799
|
+
throw new errors_1.ParseError(`Invalid quality value: ${value} (must be 0-10)`, 0, 0);
|
|
800
|
+
}
|
|
801
|
+
return quality;
|
|
802
|
+
}
|