@tomgiee/tsdp 1.0.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/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/src/builder/media-builder.d.ts +221 -0
- package/dist/src/builder/media-builder.d.ts.map +1 -0
- package/dist/src/builder/media-builder.js +385 -0
- package/dist/src/builder/session-builder.d.ts +195 -0
- package/dist/src/builder/session-builder.d.ts.map +1 -0
- package/dist/src/builder/session-builder.js +366 -0
- package/dist/src/index.d.ts +67 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +250 -0
- package/dist/src/parser/attribute-parser.d.ts +100 -0
- package/dist/src/parser/attribute-parser.d.ts.map +1 -0
- package/dist/src/parser/attribute-parser.js +217 -0
- package/dist/src/parser/field-parser.d.ts +124 -0
- package/dist/src/parser/field-parser.d.ts.map +1 -0
- package/dist/src/parser/field-parser.js +335 -0
- package/dist/src/parser/media-parser.d.ts +45 -0
- package/dist/src/parser/media-parser.d.ts.map +1 -0
- package/dist/src/parser/media-parser.js +157 -0
- package/dist/src/parser/primitive-parser.d.ts +138 -0
- package/dist/src/parser/primitive-parser.d.ts.map +1 -0
- package/dist/src/parser/primitive-parser.js +316 -0
- package/dist/src/parser/scanner.d.ts +142 -0
- package/dist/src/parser/scanner.d.ts.map +1 -0
- package/dist/src/parser/scanner.js +284 -0
- package/dist/src/parser/session-parser.d.ts +35 -0
- package/dist/src/parser/session-parser.d.ts.map +1 -0
- package/dist/src/parser/session-parser.js +207 -0
- package/dist/src/parser/time-parser.d.ts +74 -0
- package/dist/src/parser/time-parser.d.ts.map +1 -0
- package/dist/src/parser/time-parser.js +168 -0
- package/dist/src/serializer/attribute-serializer.d.ts +18 -0
- package/dist/src/serializer/attribute-serializer.d.ts.map +1 -0
- package/dist/src/serializer/attribute-serializer.js +34 -0
- package/dist/src/serializer/field-serializer.d.ts +112 -0
- package/dist/src/serializer/field-serializer.d.ts.map +1 -0
- package/dist/src/serializer/field-serializer.js +212 -0
- package/dist/src/serializer/media-serializer.d.ts +31 -0
- package/dist/src/serializer/media-serializer.d.ts.map +1 -0
- package/dist/src/serializer/media-serializer.js +83 -0
- package/dist/src/serializer/session-serializer.d.ts +29 -0
- package/dist/src/serializer/session-serializer.d.ts.map +1 -0
- package/dist/src/serializer/session-serializer.js +99 -0
- package/dist/src/serializer/time-serializer.d.ts +46 -0
- package/dist/src/serializer/time-serializer.d.ts.map +1 -0
- package/dist/src/serializer/time-serializer.js +86 -0
- package/dist/src/types/attributes.d.ts +318 -0
- package/dist/src/types/attributes.d.ts.map +1 -0
- package/dist/src/types/attributes.js +225 -0
- package/dist/src/types/errors.d.ts +129 -0
- package/dist/src/types/errors.d.ts.map +1 -0
- package/dist/src/types/errors.js +186 -0
- package/dist/src/types/fields.d.ts +100 -0
- package/dist/src/types/fields.d.ts.map +1 -0
- package/dist/src/types/fields.js +48 -0
- package/dist/src/types/media.d.ts +148 -0
- package/dist/src/types/media.d.ts.map +1 -0
- package/dist/src/types/media.js +137 -0
- package/dist/src/types/network.d.ts +136 -0
- package/dist/src/types/network.d.ts.map +1 -0
- package/dist/src/types/network.js +130 -0
- package/dist/src/types/primitives.d.ts +193 -0
- package/dist/src/types/primitives.d.ts.map +1 -0
- package/dist/src/types/primitives.js +195 -0
- package/dist/src/types/session.d.ts +122 -0
- package/dist/src/types/session.d.ts.map +1 -0
- package/dist/src/types/session.js +81 -0
- package/dist/src/types/time.d.ts +129 -0
- package/dist/src/types/time.d.ts.map +1 -0
- package/dist/src/types/time.js +84 -0
- package/dist/src/utils/address-parser.d.ts +100 -0
- package/dist/src/utils/address-parser.d.ts.map +1 -0
- package/dist/src/utils/address-parser.js +338 -0
- package/dist/src/utils/format-validators.d.ts +77 -0
- package/dist/src/utils/format-validators.d.ts.map +1 -0
- package/dist/src/utils/format-validators.js +504 -0
- package/dist/src/utils/line-reader.d.ts +84 -0
- package/dist/src/utils/line-reader.d.ts.map +1 -0
- package/dist/src/utils/line-reader.js +169 -0
- package/dist/src/utils/time-converter.d.ts +99 -0
- package/dist/src/utils/time-converter.d.ts.map +1 -0
- package/dist/src/utils/time-converter.js +195 -0
- package/dist/src/validator/media-validator.d.ts +27 -0
- package/dist/src/validator/media-validator.d.ts.map +1 -0
- package/dist/src/validator/media-validator.js +241 -0
- package/dist/src/validator/semantic-validator.d.ts +47 -0
- package/dist/src/validator/semantic-validator.d.ts.map +1 -0
- package/dist/src/validator/semantic-validator.js +207 -0
- package/dist/src/validator/session-validator.d.ts +36 -0
- package/dist/src/validator/session-validator.d.ts.map +1 -0
- package/dist/src/validator/session-validator.js +280 -0
- package/dist/tests/integration/round-trip.test.d.ts +5 -0
- package/dist/tests/integration/round-trip.test.d.ts.map +1 -0
- package/dist/tests/integration/round-trip.test.js +320 -0
- package/dist/tests/integration/voip-examples.test.d.ts +5 -0
- package/dist/tests/integration/voip-examples.test.d.ts.map +1 -0
- package/dist/tests/integration/voip-examples.test.js +361 -0
- package/dist/tests/unit/builder/media-builder.test.d.ts +5 -0
- package/dist/tests/unit/builder/media-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/builder/media-builder.test.js +524 -0
- package/dist/tests/unit/builder/session-builder.test.d.ts +5 -0
- package/dist/tests/unit/builder/session-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/builder/session-builder.test.js +367 -0
- package/dist/tests/unit/parser/attribute-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/attribute-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/attribute-parser.test.js +319 -0
- package/dist/tests/unit/parser/field-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/field-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/field-parser.test.js +355 -0
- package/dist/tests/unit/parser/media-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/media-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/media-parser.test.js +241 -0
- package/dist/tests/unit/parser/primitive-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/primitive-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/primitive-parser.test.js +261 -0
- package/dist/tests/unit/parser/scanner.test.d.ts +5 -0
- package/dist/tests/unit/parser/scanner.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/scanner.test.js +241 -0
- package/dist/tests/unit/parser/session-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/session-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/session-parser.test.js +346 -0
- package/dist/tests/unit/parser/time-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/time-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/time-parser.test.js +173 -0
- package/dist/tests/unit/serializer/attribute-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/attribute-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/attribute-serializer.test.js +78 -0
- package/dist/tests/unit/serializer/field-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/field-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/field-serializer.test.js +159 -0
- package/dist/tests/unit/serializer/media-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/media-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/media-serializer.test.js +155 -0
- package/dist/tests/unit/serializer/session-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/session-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/session-serializer.test.js +317 -0
- package/dist/tests/unit/serializer/time-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/time-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/time-serializer.test.js +115 -0
- package/dist/tests/unit/types/errors.test.d.ts +5 -0
- package/dist/tests/unit/types/errors.test.d.ts.map +1 -0
- package/dist/tests/unit/types/errors.test.js +127 -0
- package/dist/tests/unit/types/network.test.d.ts +5 -0
- package/dist/tests/unit/types/network.test.d.ts.map +1 -0
- package/dist/tests/unit/types/network.test.js +132 -0
- package/dist/tests/unit/types/primitives.test.d.ts +5 -0
- package/dist/tests/unit/types/primitives.test.d.ts.map +1 -0
- package/dist/tests/unit/types/primitives.test.js +108 -0
- package/dist/tests/unit/utils/address-parser.test.d.ts +5 -0
- package/dist/tests/unit/utils/address-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/address-parser.test.js +203 -0
- package/dist/tests/unit/utils/format-validators.test.d.ts +5 -0
- package/dist/tests/unit/utils/format-validators.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/format-validators.test.js +224 -0
- package/dist/tests/unit/utils/line-reader.test.d.ts +5 -0
- package/dist/tests/unit/utils/line-reader.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/line-reader.test.js +157 -0
- package/dist/tests/unit/utils/time-converter.test.d.ts +5 -0
- package/dist/tests/unit/utils/time-converter.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/time-converter.test.js +190 -0
- package/dist/tests/unit/validator/media-validator.test.d.ts +5 -0
- package/dist/tests/unit/validator/media-validator.test.d.ts.map +1 -0
- package/dist/tests/unit/validator/media-validator.test.js +313 -0
- package/dist/tests/unit/validator/semantic-validator.test.d.ts +5 -0
- package/dist/tests/unit/validator/semantic-validator.test.d.ts.map +1 -0
- package/dist/tests/unit/validator/semantic-validator.test.js +262 -0
- package/dist/tests/unit/validator/session-validator.test.d.ts +5 -0
- package/dist/tests/unit/validator/session-validator.test.d.ts.map +1 -0
- package/dist/tests/unit/validator/session-validator.test.js +447 -0
- package/package.json +50 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scanner for SDP parsing (RFC 8866)
|
|
4
|
+
*
|
|
5
|
+
* Provides low-level tokenization and character-level operations
|
|
6
|
+
* with position tracking for detailed error reporting.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.Scanner = void 0;
|
|
10
|
+
const errors_1 = require("../types/errors");
|
|
11
|
+
/**
|
|
12
|
+
* Scanner class for parsing SDP text
|
|
13
|
+
*
|
|
14
|
+
* Maintains position information and provides character-level operations
|
|
15
|
+
* for parsing SDP fields according to RFC 8866 grammar.
|
|
16
|
+
*/
|
|
17
|
+
class Scanner {
|
|
18
|
+
/**
|
|
19
|
+
* Create a new Scanner
|
|
20
|
+
*
|
|
21
|
+
* @param input - SDP text to scan
|
|
22
|
+
*/
|
|
23
|
+
constructor(input) {
|
|
24
|
+
this.input = input;
|
|
25
|
+
this.position = 0;
|
|
26
|
+
this.line = 1;
|
|
27
|
+
this.column = 1;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Peek at the current character without consuming it
|
|
31
|
+
*
|
|
32
|
+
* @returns Current character or null if at EOF
|
|
33
|
+
*/
|
|
34
|
+
peek() {
|
|
35
|
+
if (this.isEOF()) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return this.input[this.position];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Peek ahead n characters without consuming
|
|
42
|
+
*
|
|
43
|
+
* @param n - Number of characters to peek ahead
|
|
44
|
+
* @returns Character at position+n or null if beyond EOF
|
|
45
|
+
*/
|
|
46
|
+
peekAhead(n) {
|
|
47
|
+
const pos = this.position + n;
|
|
48
|
+
if (pos >= this.input.length) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return this.input[pos];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Advance to the next character and return the current one
|
|
55
|
+
*
|
|
56
|
+
* Updates line and column tracking when encountering newlines.
|
|
57
|
+
*
|
|
58
|
+
* @returns Current character or null if at EOF
|
|
59
|
+
*/
|
|
60
|
+
advance() {
|
|
61
|
+
if (this.isEOF()) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const ch = this.input[this.position];
|
|
65
|
+
this.position++;
|
|
66
|
+
// Update line and column tracking
|
|
67
|
+
if (ch === '\n') {
|
|
68
|
+
this.line++;
|
|
69
|
+
this.column = 1;
|
|
70
|
+
}
|
|
71
|
+
else if (ch === '\r') {
|
|
72
|
+
// Handle CRLF and CR
|
|
73
|
+
if (this.peek() === '\n') {
|
|
74
|
+
// CRLF - consume the LF too
|
|
75
|
+
this.position++;
|
|
76
|
+
}
|
|
77
|
+
this.line++;
|
|
78
|
+
this.column = 1;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.column++;
|
|
82
|
+
}
|
|
83
|
+
return ch;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read characters until a delimiter is found
|
|
87
|
+
*
|
|
88
|
+
* Does NOT consume the delimiter.
|
|
89
|
+
*
|
|
90
|
+
* @param delimiter - String or RegExp to match
|
|
91
|
+
* @returns String of characters read (may be empty)
|
|
92
|
+
*/
|
|
93
|
+
readUntil(delimiter) {
|
|
94
|
+
let result = '';
|
|
95
|
+
while (!this.isEOF()) {
|
|
96
|
+
const ch = this.peek();
|
|
97
|
+
if (typeof delimiter === 'string') {
|
|
98
|
+
// String delimiter - check for exact match
|
|
99
|
+
if (ch === delimiter) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// RegExp delimiter - test current character
|
|
105
|
+
if (delimiter.test(ch)) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
result += this.advance();
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read while a condition is true
|
|
115
|
+
*
|
|
116
|
+
* @param predicate - Function that returns true to continue reading
|
|
117
|
+
* @returns String of characters read
|
|
118
|
+
*/
|
|
119
|
+
readWhile(predicate) {
|
|
120
|
+
let result = '';
|
|
121
|
+
while (!this.isEOF()) {
|
|
122
|
+
const ch = this.peek();
|
|
123
|
+
if (!predicate(ch)) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
result += this.advance();
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Skip whitespace characters (space and tab)
|
|
132
|
+
*
|
|
133
|
+
* Does NOT skip newlines (CRLF/LF/CR).
|
|
134
|
+
*/
|
|
135
|
+
skipWhitespace() {
|
|
136
|
+
while (!this.isEOF()) {
|
|
137
|
+
const ch = this.peek();
|
|
138
|
+
if (ch !== ' ' && ch !== '\t') {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
this.advance();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Skip to the next line
|
|
146
|
+
*
|
|
147
|
+
* Consumes all characters until and including the next line ending (CRLF/LF/CR).
|
|
148
|
+
*/
|
|
149
|
+
skipToNextLine() {
|
|
150
|
+
while (!this.isEOF()) {
|
|
151
|
+
const ch = this.advance();
|
|
152
|
+
if (ch === '\n' || ch === '\r') {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get current position in the input
|
|
159
|
+
*
|
|
160
|
+
* @returns Position object with offset, line, and column
|
|
161
|
+
*/
|
|
162
|
+
getPosition() {
|
|
163
|
+
return {
|
|
164
|
+
offset: this.position,
|
|
165
|
+
line: this.line,
|
|
166
|
+
column: this.column,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if at end of file
|
|
171
|
+
*
|
|
172
|
+
* @returns true if no more characters to read
|
|
173
|
+
*/
|
|
174
|
+
isEOF() {
|
|
175
|
+
return this.position >= this.input.length;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Expect a specific string at the current position
|
|
179
|
+
*
|
|
180
|
+
* Consumes the string if it matches, throws otherwise.
|
|
181
|
+
*
|
|
182
|
+
* @param expected - String to expect
|
|
183
|
+
* @throws ParseError if string doesn't match
|
|
184
|
+
*/
|
|
185
|
+
expect(expected) {
|
|
186
|
+
const start = this.getPosition();
|
|
187
|
+
for (let i = 0; i < expected.length; i++) {
|
|
188
|
+
const ch = this.peek();
|
|
189
|
+
if (ch === null) {
|
|
190
|
+
throw new errors_1.ParseError(`Expected "${expected}" but reached end of input`, start.line, start.column, this.getContext(start));
|
|
191
|
+
}
|
|
192
|
+
if (ch !== expected[i]) {
|
|
193
|
+
throw new errors_1.ParseError(`Expected "${expected}" but found "${ch}"`, start.line, start.column, this.getContext(start));
|
|
194
|
+
}
|
|
195
|
+
this.advance();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Expect a character that matches a condition
|
|
200
|
+
*
|
|
201
|
+
* Consumes the character if it matches, throws otherwise.
|
|
202
|
+
*
|
|
203
|
+
* @param predicate - Function that returns true for valid character
|
|
204
|
+
* @param description - Description of expected character for error messages
|
|
205
|
+
* @returns The matched character
|
|
206
|
+
* @throws ParseError if character doesn't match
|
|
207
|
+
*/
|
|
208
|
+
expectChar(predicate, description) {
|
|
209
|
+
const pos = this.getPosition();
|
|
210
|
+
const ch = this.peek();
|
|
211
|
+
if (ch === null) {
|
|
212
|
+
throw new errors_1.ParseError(`Expected ${description} but reached end of input`, pos.line, pos.column, this.getContext(pos));
|
|
213
|
+
}
|
|
214
|
+
if (!predicate(ch)) {
|
|
215
|
+
throw new errors_1.ParseError(`Expected ${description} but found "${ch}"`, pos.line, pos.column, this.getContext(pos));
|
|
216
|
+
}
|
|
217
|
+
this.advance();
|
|
218
|
+
return ch;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Expect end of line (CRLF, LF, or CR)
|
|
222
|
+
*
|
|
223
|
+
* @throws ParseError if not at end of line
|
|
224
|
+
*/
|
|
225
|
+
expectEOL() {
|
|
226
|
+
const pos = this.getPosition();
|
|
227
|
+
const ch = this.peek();
|
|
228
|
+
if (ch === null) {
|
|
229
|
+
// EOF is acceptable as EOL
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (ch !== '\r' && ch !== '\n') {
|
|
233
|
+
throw new errors_1.ParseError(`Expected end of line but found "${ch}"`, pos.line, pos.column, this.getContext(pos));
|
|
234
|
+
}
|
|
235
|
+
this.advance();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Expect space character
|
|
239
|
+
*
|
|
240
|
+
* @throws ParseError if not a space
|
|
241
|
+
*/
|
|
242
|
+
expectSpace() {
|
|
243
|
+
const pos = this.getPosition();
|
|
244
|
+
const ch = this.peek();
|
|
245
|
+
if (ch !== ' ') {
|
|
246
|
+
throw new errors_1.ParseError(`Expected space but found "${ch}"`, pos.line, pos.column, this.getContext(pos));
|
|
247
|
+
}
|
|
248
|
+
this.advance();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get context around a position for error messages
|
|
252
|
+
*
|
|
253
|
+
* Returns a snippet of the input around the position.
|
|
254
|
+
*
|
|
255
|
+
* @param pos - Position to get context for
|
|
256
|
+
* @returns Context string (may be empty)
|
|
257
|
+
*/
|
|
258
|
+
getContext(pos) {
|
|
259
|
+
const lines = this.input.split('\n');
|
|
260
|
+
if (pos.line - 1 < lines.length) {
|
|
261
|
+
return lines[pos.line - 1];
|
|
262
|
+
}
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get remaining input from current position
|
|
267
|
+
*
|
|
268
|
+
* @returns Remaining input string
|
|
269
|
+
*/
|
|
270
|
+
getRemainingInput() {
|
|
271
|
+
return this.input.substring(this.position);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get a substring of the input
|
|
275
|
+
*
|
|
276
|
+
* @param start - Start position
|
|
277
|
+
* @param end - End position (optional, defaults to current position)
|
|
278
|
+
* @returns Substring
|
|
279
|
+
*/
|
|
280
|
+
getSubstring(start, end) {
|
|
281
|
+
return this.input.substring(start, end !== null && end !== void 0 ? end : this.position);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
exports.Scanner = Scanner;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session description parser for SDP (RFC 8866)
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for parsing complete SDP session descriptions
|
|
5
|
+
*/
|
|
6
|
+
import { SessionDescription } from '../types/session';
|
|
7
|
+
/**
|
|
8
|
+
* Parse a complete SDP session description
|
|
9
|
+
*
|
|
10
|
+
* RFC 8866 Section 5 defines the exact ordering of fields:
|
|
11
|
+
* 1. v= (version) [required, exactly one]
|
|
12
|
+
* 2. o= (origin) [required, exactly one]
|
|
13
|
+
* 3. s= (session name) [required, exactly one]
|
|
14
|
+
* 4. i= (session information) [optional, at most one]
|
|
15
|
+
* 5. u= (URI) [optional, at most one]
|
|
16
|
+
* 6. e= (email) [optional, multiple]
|
|
17
|
+
* 7. p= (phone) [optional, multiple]
|
|
18
|
+
* 8. c= (connection) [optional, at most one at session level]
|
|
19
|
+
* 9. b= (bandwidth) [optional, multiple]
|
|
20
|
+
* 10. Time descriptions [required, at least one]:
|
|
21
|
+
* - t= (timing) [required]
|
|
22
|
+
* - r= (repeat) [optional, multiple]
|
|
23
|
+
* - z= (timezone) [optional, at most one]
|
|
24
|
+
* 11. k= (key) [optional, at most one]
|
|
25
|
+
* 12. a= (attributes) [optional, multiple]
|
|
26
|
+
* 13. Media descriptions [optional, zero or more]:
|
|
27
|
+
* - m= (media) [required]
|
|
28
|
+
* - i= c= b= k= a= (optional media-level fields)
|
|
29
|
+
*
|
|
30
|
+
* @param input - Raw SDP text
|
|
31
|
+
* @returns Parsed SessionDescription
|
|
32
|
+
* @throws ParseError if input is invalid or not RFC 8866 compliant
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseSessionDescription(input: string): SessionDescription;
|
|
35
|
+
//# sourceMappingURL=session-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-parser.d.ts","sourceRoot":"","sources":["../../../src/parser/session-parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmBH,OAAO,EAAE,kBAAkB,EAA4B,MAAM,kBAAkB,CAAC;AAYhF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAkNzE"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Session description parser for SDP (RFC 8866)
|
|
4
|
+
*
|
|
5
|
+
* Main entry point for parsing complete SDP session descriptions
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.parseSessionDescription = parseSessionDescription;
|
|
9
|
+
const scanner_1 = require("./scanner");
|
|
10
|
+
const field_parser_1 = require("./field-parser");
|
|
11
|
+
const time_parser_1 = require("./time-parser");
|
|
12
|
+
const attribute_parser_1 = require("./attribute-parser");
|
|
13
|
+
const media_parser_1 = require("./media-parser");
|
|
14
|
+
const errors_1 = require("../types/errors");
|
|
15
|
+
const session_1 = require("../types/session");
|
|
16
|
+
const line_reader_1 = require("../utils/line-reader");
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Session Parser
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Parse a complete SDP session description
|
|
22
|
+
*
|
|
23
|
+
* RFC 8866 Section 5 defines the exact ordering of fields:
|
|
24
|
+
* 1. v= (version) [required, exactly one]
|
|
25
|
+
* 2. o= (origin) [required, exactly one]
|
|
26
|
+
* 3. s= (session name) [required, exactly one]
|
|
27
|
+
* 4. i= (session information) [optional, at most one]
|
|
28
|
+
* 5. u= (URI) [optional, at most one]
|
|
29
|
+
* 6. e= (email) [optional, multiple]
|
|
30
|
+
* 7. p= (phone) [optional, multiple]
|
|
31
|
+
* 8. c= (connection) [optional, at most one at session level]
|
|
32
|
+
* 9. b= (bandwidth) [optional, multiple]
|
|
33
|
+
* 10. Time descriptions [required, at least one]:
|
|
34
|
+
* - t= (timing) [required]
|
|
35
|
+
* - r= (repeat) [optional, multiple]
|
|
36
|
+
* - z= (timezone) [optional, at most one]
|
|
37
|
+
* 11. k= (key) [optional, at most one]
|
|
38
|
+
* 12. a= (attributes) [optional, multiple]
|
|
39
|
+
* 13. Media descriptions [optional, zero or more]:
|
|
40
|
+
* - m= (media) [required]
|
|
41
|
+
* - i= c= b= k= a= (optional media-level fields)
|
|
42
|
+
*
|
|
43
|
+
* @param input - Raw SDP text
|
|
44
|
+
* @returns Parsed SessionDescription
|
|
45
|
+
* @throws ParseError if input is invalid or not RFC 8866 compliant
|
|
46
|
+
*/
|
|
47
|
+
function parseSessionDescription(input) {
|
|
48
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
49
|
+
// Parse lines
|
|
50
|
+
const lines = (0, line_reader_1.parseLines)(input);
|
|
51
|
+
if (lines.length === 0) {
|
|
52
|
+
throw new errors_1.ParseError('Empty SDP input', 0, 0);
|
|
53
|
+
}
|
|
54
|
+
let index = 0;
|
|
55
|
+
// Helper to get current line
|
|
56
|
+
const current = () => lines[index];
|
|
57
|
+
// Helper to consume a line
|
|
58
|
+
const consume = () => {
|
|
59
|
+
if (index >= lines.length) {
|
|
60
|
+
throw new errors_1.ParseError('Unexpected end of SDP', 0, 0);
|
|
61
|
+
}
|
|
62
|
+
return lines[index++];
|
|
63
|
+
};
|
|
64
|
+
// Helper to expect a specific line type
|
|
65
|
+
const expect = (type, fieldName) => {
|
|
66
|
+
var _a;
|
|
67
|
+
const line = current();
|
|
68
|
+
if (!line || line.type !== type) {
|
|
69
|
+
throw new errors_1.ParseError(`Expected ${fieldName} field (${type}=) at line ${index + 1}, but got ${line ? line.type + '=' : 'end of input'}`, (_a = line === null || line === void 0 ? void 0 : line.lineNumber) !== null && _a !== void 0 ? _a : 0, 0);
|
|
70
|
+
}
|
|
71
|
+
return consume();
|
|
72
|
+
};
|
|
73
|
+
// ========================================================================
|
|
74
|
+
// 1. Parse required session-level fields (v=, o=, s=)
|
|
75
|
+
// ========================================================================
|
|
76
|
+
// v= version (required, exactly one)
|
|
77
|
+
const vLine = expect('v', 'version');
|
|
78
|
+
const version = (0, field_parser_1.parseVersionField)(new scanner_1.Scanner(vLine.value));
|
|
79
|
+
// o= origin (required, exactly one)
|
|
80
|
+
const oLine = expect('o', 'origin');
|
|
81
|
+
const origin = (0, field_parser_1.parseOriginField)(new scanner_1.Scanner(oLine.value));
|
|
82
|
+
// s= session name (required, exactly one)
|
|
83
|
+
const sLine = expect('s', 'session name');
|
|
84
|
+
const sessionName = (0, field_parser_1.parseSessionNameField)(new scanner_1.Scanner(sLine.value));
|
|
85
|
+
// ========================================================================
|
|
86
|
+
// 2. Parse optional session-level fields (i=, u=, e=, p=, c=, b=)
|
|
87
|
+
// ========================================================================
|
|
88
|
+
let sessionInformation;
|
|
89
|
+
let uri;
|
|
90
|
+
const emails = [];
|
|
91
|
+
const phones = [];
|
|
92
|
+
let connection;
|
|
93
|
+
const bandwidths = [];
|
|
94
|
+
// i= session information (optional, at most one)
|
|
95
|
+
if (((_a = current()) === null || _a === void 0 ? void 0 : _a.type) === 'i') {
|
|
96
|
+
const iLine = consume();
|
|
97
|
+
sessionInformation = (0, field_parser_1.parseInformationField)(new scanner_1.Scanner(iLine.value));
|
|
98
|
+
}
|
|
99
|
+
// u= URI (optional, at most one)
|
|
100
|
+
if (((_b = current()) === null || _b === void 0 ? void 0 : _b.type) === 'u') {
|
|
101
|
+
const uLine = consume();
|
|
102
|
+
uri = (0, field_parser_1.parseUriField)(new scanner_1.Scanner(uLine.value));
|
|
103
|
+
}
|
|
104
|
+
// e= email (optional, multiple)
|
|
105
|
+
while (((_c = current()) === null || _c === void 0 ? void 0 : _c.type) === 'e') {
|
|
106
|
+
const eLine = consume();
|
|
107
|
+
emails.push((0, field_parser_1.parseEmailField)(new scanner_1.Scanner(eLine.value)));
|
|
108
|
+
}
|
|
109
|
+
// p= phone (optional, multiple)
|
|
110
|
+
while (((_d = current()) === null || _d === void 0 ? void 0 : _d.type) === 'p') {
|
|
111
|
+
const pLine = consume();
|
|
112
|
+
phones.push((0, field_parser_1.parsePhoneField)(new scanner_1.Scanner(pLine.value)));
|
|
113
|
+
}
|
|
114
|
+
// c= connection (optional, at most one at session level)
|
|
115
|
+
if (((_e = current()) === null || _e === void 0 ? void 0 : _e.type) === 'c') {
|
|
116
|
+
const cLine = consume();
|
|
117
|
+
connection = (0, field_parser_1.parseConnectionField)(new scanner_1.Scanner(cLine.value));
|
|
118
|
+
}
|
|
119
|
+
// b= bandwidth (optional, multiple)
|
|
120
|
+
while (((_f = current()) === null || _f === void 0 ? void 0 : _f.type) === 'b') {
|
|
121
|
+
const bLine = consume();
|
|
122
|
+
bandwidths.push((0, field_parser_1.parseBandwidthField)(new scanner_1.Scanner(bLine.value)));
|
|
123
|
+
}
|
|
124
|
+
// ========================================================================
|
|
125
|
+
// 3. Parse time descriptions (required, at least one)
|
|
126
|
+
// ========================================================================
|
|
127
|
+
const timeDescriptions = [];
|
|
128
|
+
// Must have at least one time description
|
|
129
|
+
if (((_g = current()) === null || _g === void 0 ? void 0 : _g.type) !== 't') {
|
|
130
|
+
throw new errors_1.ParseError(`Expected at least one time description (t=) at line ${index + 1}`, (_j = (_h = current()) === null || _h === void 0 ? void 0 : _h.lineNumber) !== null && _j !== void 0 ? _j : 0, 0);
|
|
131
|
+
}
|
|
132
|
+
// Parse time descriptions until we hit k=, a=, m=, or end
|
|
133
|
+
while (((_k = current()) === null || _k === void 0 ? void 0 : _k.type) === 't') {
|
|
134
|
+
const tLine = consume();
|
|
135
|
+
const timing = (0, time_parser_1.parseTimingField)(new scanner_1.Scanner(tLine.value));
|
|
136
|
+
// Parse optional repeat times (r=)
|
|
137
|
+
const repeats = [];
|
|
138
|
+
while (((_l = current()) === null || _l === void 0 ? void 0 : _l.type) === 'r') {
|
|
139
|
+
const rLine = consume();
|
|
140
|
+
repeats.push((0, time_parser_1.parseRepeatField)(new scanner_1.Scanner(rLine.value)));
|
|
141
|
+
}
|
|
142
|
+
// Parse optional timezone adjustments (z=)
|
|
143
|
+
let timezone;
|
|
144
|
+
if (((_m = current()) === null || _m === void 0 ? void 0 : _m.type) === 'z') {
|
|
145
|
+
const zLine = consume();
|
|
146
|
+
timezone = (0, time_parser_1.parseTimezoneField)(new scanner_1.Scanner(zLine.value));
|
|
147
|
+
}
|
|
148
|
+
timeDescriptions.push((0, time_parser_1.parseTimeDescription)(timing, repeats, timezone));
|
|
149
|
+
}
|
|
150
|
+
// ========================================================================
|
|
151
|
+
// 4. Parse session-level key and attributes (k=, a=)
|
|
152
|
+
// ========================================================================
|
|
153
|
+
let key;
|
|
154
|
+
const attributes = [];
|
|
155
|
+
// k= key (optional, at most one)
|
|
156
|
+
if (((_o = current()) === null || _o === void 0 ? void 0 : _o.type) === 'k') {
|
|
157
|
+
const kLine = consume();
|
|
158
|
+
key = (0, field_parser_1.parseKeyField)(new scanner_1.Scanner(kLine.value));
|
|
159
|
+
}
|
|
160
|
+
// a= attributes (optional, multiple)
|
|
161
|
+
while (((_p = current()) === null || _p === void 0 ? void 0 : _p.type) === 'a') {
|
|
162
|
+
const aLine = consume();
|
|
163
|
+
attributes.push((0, attribute_parser_1.parseAttributeField)(new scanner_1.Scanner(aLine.value)));
|
|
164
|
+
}
|
|
165
|
+
// ========================================================================
|
|
166
|
+
// 5. Parse media descriptions (optional, zero or more)
|
|
167
|
+
// ========================================================================
|
|
168
|
+
const mediaDescriptions = [];
|
|
169
|
+
// Parse media descriptions until end of input
|
|
170
|
+
while (((_q = current()) === null || _q === void 0 ? void 0 : _q.type) === 'm') {
|
|
171
|
+
const mLine = consume();
|
|
172
|
+
// Collect media-level fields (i=, c=, b=, k=, a=)
|
|
173
|
+
const mediaLines = [];
|
|
174
|
+
while (current() && ['i', 'c', 'b', 'k', 'a'].includes(current().type)) {
|
|
175
|
+
const line = consume();
|
|
176
|
+
mediaLines.push({ type: line.type, value: line.value });
|
|
177
|
+
}
|
|
178
|
+
// Parse media description
|
|
179
|
+
const mediaDescription = (0, media_parser_1.parseMediaDescription)(new scanner_1.Scanner(mLine.value), mediaLines);
|
|
180
|
+
mediaDescriptions.push(mediaDescription);
|
|
181
|
+
}
|
|
182
|
+
// ========================================================================
|
|
183
|
+
// 6. Validate no unexpected fields remain
|
|
184
|
+
// ========================================================================
|
|
185
|
+
if (current()) {
|
|
186
|
+
const line = current();
|
|
187
|
+
throw new errors_1.ParseError(`Unexpected field at line ${line.lineNumber}: ${line.type}=`, line.lineNumber, 0);
|
|
188
|
+
}
|
|
189
|
+
// ========================================================================
|
|
190
|
+
// 7. Create and return session description
|
|
191
|
+
// ========================================================================
|
|
192
|
+
return (0, session_1.createSessionDescription)({
|
|
193
|
+
version,
|
|
194
|
+
origin,
|
|
195
|
+
sessionName,
|
|
196
|
+
sessionInformation,
|
|
197
|
+
uri,
|
|
198
|
+
emails,
|
|
199
|
+
phones,
|
|
200
|
+
connection,
|
|
201
|
+
bandwidths,
|
|
202
|
+
timeDescriptions,
|
|
203
|
+
key,
|
|
204
|
+
attributes,
|
|
205
|
+
mediaDescriptions,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time parsers for SDP (RFC 8866)
|
|
3
|
+
*
|
|
4
|
+
* Parsers for time description fields (t=, r=, z=)
|
|
5
|
+
*/
|
|
6
|
+
import { Scanner } from './scanner';
|
|
7
|
+
import { Timing, Repeat, Timezone, TimeDescription } from '../types/time';
|
|
8
|
+
/**
|
|
9
|
+
* Parse timing field
|
|
10
|
+
*
|
|
11
|
+
* Format: t=<start-time> <stop-time>
|
|
12
|
+
*
|
|
13
|
+
* Both times are NTP timestamps (seconds since Jan 1, 1900).
|
|
14
|
+
* 0 means unbounded/permanent.
|
|
15
|
+
*
|
|
16
|
+
* @param scanner - Scanner instance
|
|
17
|
+
* @returns Timing object
|
|
18
|
+
* @throws ParseError if invalid format
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseTimingField(scanner: Scanner): Timing;
|
|
21
|
+
/**
|
|
22
|
+
* Parse repeat field
|
|
23
|
+
*
|
|
24
|
+
* Format: r=<repeat-interval> <active-duration> <offsets-from-start-time>
|
|
25
|
+
*
|
|
26
|
+
* repeat-interval: Time between repetitions (typed time)
|
|
27
|
+
* active-duration: Duration of each session (typed time)
|
|
28
|
+
* offsets-from-start-time: One or more offsets (typed times)
|
|
29
|
+
*
|
|
30
|
+
* Example: r=7d 1h 0 25h
|
|
31
|
+
* - Repeats weekly (7 days)
|
|
32
|
+
* - Each session is 1 hour
|
|
33
|
+
* - Sessions occur at start time (0) and 25 hours after start
|
|
34
|
+
*
|
|
35
|
+
* @param scanner - Scanner instance
|
|
36
|
+
* @returns Repeat object
|
|
37
|
+
* @throws ParseError if invalid format
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseRepeatField(scanner: Scanner): Repeat;
|
|
40
|
+
/**
|
|
41
|
+
* Parse timezone field
|
|
42
|
+
*
|
|
43
|
+
* Format: z=<adjustment-time> <offset> [<adjustment-time> <offset> ...]
|
|
44
|
+
*
|
|
45
|
+
* adjustment-time: NTP timestamp when adjustment takes effect
|
|
46
|
+
* offset: Typed time offset to apply
|
|
47
|
+
*
|
|
48
|
+
* Example: z=2882844526 -1h 2898848070 0
|
|
49
|
+
* - At time 2882844526, apply -1h offset (fall back)
|
|
50
|
+
* - At time 2898848070, apply 0 offset (return to standard)
|
|
51
|
+
*
|
|
52
|
+
* @param scanner - Scanner instance
|
|
53
|
+
* @returns Timezone object
|
|
54
|
+
* @throws ParseError if invalid format
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseTimezoneField(scanner: Scanner): Timezone;
|
|
57
|
+
/**
|
|
58
|
+
* Parse a time description
|
|
59
|
+
*
|
|
60
|
+
* A time description consists of:
|
|
61
|
+
* - One timing field (t=) [required]
|
|
62
|
+
* - Zero or more repeat fields (r=) [optional]
|
|
63
|
+
* - Zero or one timezone field (z=) [optional]
|
|
64
|
+
*
|
|
65
|
+
* This is typically used when parsing the time section of an SDP,
|
|
66
|
+
* but the individual field parsers above can be used for line-by-line parsing.
|
|
67
|
+
*
|
|
68
|
+
* @param timing - Timing field
|
|
69
|
+
* @param repeats - Repeat fields (optional)
|
|
70
|
+
* @param timezone - Timezone field (optional)
|
|
71
|
+
* @returns TimeDescription object
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseTimeDescription(timing: Timing, repeats?: Repeat[], timezone?: Timezone): TimeDescription;
|
|
74
|
+
//# sourceMappingURL=time-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-parser.d.ts","sourceRoot":"","sources":["../../../src/parser/time-parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EACL,MAAM,EACN,MAAM,EACN,QAAQ,EAER,eAAe,EAMhB,MAAM,eAAe,CAAC;AAOvB;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAsBzD;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CA2CzD;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAgD7D;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,MAAM,EAAO,EACtB,QAAQ,CAAC,EAAE,QAAQ,GAClB,eAAe,CAEjB"}
|