@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/src/builder/media-builder.d.ts +221 -0
  4. package/dist/src/builder/media-builder.d.ts.map +1 -0
  5. package/dist/src/builder/media-builder.js +385 -0
  6. package/dist/src/builder/session-builder.d.ts +195 -0
  7. package/dist/src/builder/session-builder.d.ts.map +1 -0
  8. package/dist/src/builder/session-builder.js +366 -0
  9. package/dist/src/index.d.ts +67 -0
  10. package/dist/src/index.d.ts.map +1 -0
  11. package/dist/src/index.js +250 -0
  12. package/dist/src/parser/attribute-parser.d.ts +100 -0
  13. package/dist/src/parser/attribute-parser.d.ts.map +1 -0
  14. package/dist/src/parser/attribute-parser.js +217 -0
  15. package/dist/src/parser/field-parser.d.ts +124 -0
  16. package/dist/src/parser/field-parser.d.ts.map +1 -0
  17. package/dist/src/parser/field-parser.js +335 -0
  18. package/dist/src/parser/media-parser.d.ts +45 -0
  19. package/dist/src/parser/media-parser.d.ts.map +1 -0
  20. package/dist/src/parser/media-parser.js +157 -0
  21. package/dist/src/parser/primitive-parser.d.ts +138 -0
  22. package/dist/src/parser/primitive-parser.d.ts.map +1 -0
  23. package/dist/src/parser/primitive-parser.js +316 -0
  24. package/dist/src/parser/scanner.d.ts +142 -0
  25. package/dist/src/parser/scanner.d.ts.map +1 -0
  26. package/dist/src/parser/scanner.js +284 -0
  27. package/dist/src/parser/session-parser.d.ts +35 -0
  28. package/dist/src/parser/session-parser.d.ts.map +1 -0
  29. package/dist/src/parser/session-parser.js +207 -0
  30. package/dist/src/parser/time-parser.d.ts +74 -0
  31. package/dist/src/parser/time-parser.d.ts.map +1 -0
  32. package/dist/src/parser/time-parser.js +168 -0
  33. package/dist/src/serializer/attribute-serializer.d.ts +18 -0
  34. package/dist/src/serializer/attribute-serializer.d.ts.map +1 -0
  35. package/dist/src/serializer/attribute-serializer.js +34 -0
  36. package/dist/src/serializer/field-serializer.d.ts +112 -0
  37. package/dist/src/serializer/field-serializer.d.ts.map +1 -0
  38. package/dist/src/serializer/field-serializer.js +212 -0
  39. package/dist/src/serializer/media-serializer.d.ts +31 -0
  40. package/dist/src/serializer/media-serializer.d.ts.map +1 -0
  41. package/dist/src/serializer/media-serializer.js +83 -0
  42. package/dist/src/serializer/session-serializer.d.ts +29 -0
  43. package/dist/src/serializer/session-serializer.d.ts.map +1 -0
  44. package/dist/src/serializer/session-serializer.js +99 -0
  45. package/dist/src/serializer/time-serializer.d.ts +46 -0
  46. package/dist/src/serializer/time-serializer.d.ts.map +1 -0
  47. package/dist/src/serializer/time-serializer.js +86 -0
  48. package/dist/src/types/attributes.d.ts +318 -0
  49. package/dist/src/types/attributes.d.ts.map +1 -0
  50. package/dist/src/types/attributes.js +225 -0
  51. package/dist/src/types/errors.d.ts +129 -0
  52. package/dist/src/types/errors.d.ts.map +1 -0
  53. package/dist/src/types/errors.js +186 -0
  54. package/dist/src/types/fields.d.ts +100 -0
  55. package/dist/src/types/fields.d.ts.map +1 -0
  56. package/dist/src/types/fields.js +48 -0
  57. package/dist/src/types/media.d.ts +148 -0
  58. package/dist/src/types/media.d.ts.map +1 -0
  59. package/dist/src/types/media.js +137 -0
  60. package/dist/src/types/network.d.ts +136 -0
  61. package/dist/src/types/network.d.ts.map +1 -0
  62. package/dist/src/types/network.js +130 -0
  63. package/dist/src/types/primitives.d.ts +193 -0
  64. package/dist/src/types/primitives.d.ts.map +1 -0
  65. package/dist/src/types/primitives.js +195 -0
  66. package/dist/src/types/session.d.ts +122 -0
  67. package/dist/src/types/session.d.ts.map +1 -0
  68. package/dist/src/types/session.js +81 -0
  69. package/dist/src/types/time.d.ts +129 -0
  70. package/dist/src/types/time.d.ts.map +1 -0
  71. package/dist/src/types/time.js +84 -0
  72. package/dist/src/utils/address-parser.d.ts +100 -0
  73. package/dist/src/utils/address-parser.d.ts.map +1 -0
  74. package/dist/src/utils/address-parser.js +338 -0
  75. package/dist/src/utils/format-validators.d.ts +77 -0
  76. package/dist/src/utils/format-validators.d.ts.map +1 -0
  77. package/dist/src/utils/format-validators.js +504 -0
  78. package/dist/src/utils/line-reader.d.ts +84 -0
  79. package/dist/src/utils/line-reader.d.ts.map +1 -0
  80. package/dist/src/utils/line-reader.js +169 -0
  81. package/dist/src/utils/time-converter.d.ts +99 -0
  82. package/dist/src/utils/time-converter.d.ts.map +1 -0
  83. package/dist/src/utils/time-converter.js +195 -0
  84. package/dist/src/validator/media-validator.d.ts +27 -0
  85. package/dist/src/validator/media-validator.d.ts.map +1 -0
  86. package/dist/src/validator/media-validator.js +241 -0
  87. package/dist/src/validator/semantic-validator.d.ts +47 -0
  88. package/dist/src/validator/semantic-validator.d.ts.map +1 -0
  89. package/dist/src/validator/semantic-validator.js +207 -0
  90. package/dist/src/validator/session-validator.d.ts +36 -0
  91. package/dist/src/validator/session-validator.d.ts.map +1 -0
  92. package/dist/src/validator/session-validator.js +280 -0
  93. package/dist/tests/integration/round-trip.test.d.ts +5 -0
  94. package/dist/tests/integration/round-trip.test.d.ts.map +1 -0
  95. package/dist/tests/integration/round-trip.test.js +320 -0
  96. package/dist/tests/integration/voip-examples.test.d.ts +5 -0
  97. package/dist/tests/integration/voip-examples.test.d.ts.map +1 -0
  98. package/dist/tests/integration/voip-examples.test.js +361 -0
  99. package/dist/tests/unit/builder/media-builder.test.d.ts +5 -0
  100. package/dist/tests/unit/builder/media-builder.test.d.ts.map +1 -0
  101. package/dist/tests/unit/builder/media-builder.test.js +524 -0
  102. package/dist/tests/unit/builder/session-builder.test.d.ts +5 -0
  103. package/dist/tests/unit/builder/session-builder.test.d.ts.map +1 -0
  104. package/dist/tests/unit/builder/session-builder.test.js +367 -0
  105. package/dist/tests/unit/parser/attribute-parser.test.d.ts +5 -0
  106. package/dist/tests/unit/parser/attribute-parser.test.d.ts.map +1 -0
  107. package/dist/tests/unit/parser/attribute-parser.test.js +319 -0
  108. package/dist/tests/unit/parser/field-parser.test.d.ts +5 -0
  109. package/dist/tests/unit/parser/field-parser.test.d.ts.map +1 -0
  110. package/dist/tests/unit/parser/field-parser.test.js +355 -0
  111. package/dist/tests/unit/parser/media-parser.test.d.ts +5 -0
  112. package/dist/tests/unit/parser/media-parser.test.d.ts.map +1 -0
  113. package/dist/tests/unit/parser/media-parser.test.js +241 -0
  114. package/dist/tests/unit/parser/primitive-parser.test.d.ts +5 -0
  115. package/dist/tests/unit/parser/primitive-parser.test.d.ts.map +1 -0
  116. package/dist/tests/unit/parser/primitive-parser.test.js +261 -0
  117. package/dist/tests/unit/parser/scanner.test.d.ts +5 -0
  118. package/dist/tests/unit/parser/scanner.test.d.ts.map +1 -0
  119. package/dist/tests/unit/parser/scanner.test.js +241 -0
  120. package/dist/tests/unit/parser/session-parser.test.d.ts +5 -0
  121. package/dist/tests/unit/parser/session-parser.test.d.ts.map +1 -0
  122. package/dist/tests/unit/parser/session-parser.test.js +346 -0
  123. package/dist/tests/unit/parser/time-parser.test.d.ts +5 -0
  124. package/dist/tests/unit/parser/time-parser.test.d.ts.map +1 -0
  125. package/dist/tests/unit/parser/time-parser.test.js +173 -0
  126. package/dist/tests/unit/serializer/attribute-serializer.test.d.ts +5 -0
  127. package/dist/tests/unit/serializer/attribute-serializer.test.d.ts.map +1 -0
  128. package/dist/tests/unit/serializer/attribute-serializer.test.js +78 -0
  129. package/dist/tests/unit/serializer/field-serializer.test.d.ts +5 -0
  130. package/dist/tests/unit/serializer/field-serializer.test.d.ts.map +1 -0
  131. package/dist/tests/unit/serializer/field-serializer.test.js +159 -0
  132. package/dist/tests/unit/serializer/media-serializer.test.d.ts +5 -0
  133. package/dist/tests/unit/serializer/media-serializer.test.d.ts.map +1 -0
  134. package/dist/tests/unit/serializer/media-serializer.test.js +155 -0
  135. package/dist/tests/unit/serializer/session-serializer.test.d.ts +5 -0
  136. package/dist/tests/unit/serializer/session-serializer.test.d.ts.map +1 -0
  137. package/dist/tests/unit/serializer/session-serializer.test.js +317 -0
  138. package/dist/tests/unit/serializer/time-serializer.test.d.ts +5 -0
  139. package/dist/tests/unit/serializer/time-serializer.test.d.ts.map +1 -0
  140. package/dist/tests/unit/serializer/time-serializer.test.js +115 -0
  141. package/dist/tests/unit/types/errors.test.d.ts +5 -0
  142. package/dist/tests/unit/types/errors.test.d.ts.map +1 -0
  143. package/dist/tests/unit/types/errors.test.js +127 -0
  144. package/dist/tests/unit/types/network.test.d.ts +5 -0
  145. package/dist/tests/unit/types/network.test.d.ts.map +1 -0
  146. package/dist/tests/unit/types/network.test.js +132 -0
  147. package/dist/tests/unit/types/primitives.test.d.ts +5 -0
  148. package/dist/tests/unit/types/primitives.test.d.ts.map +1 -0
  149. package/dist/tests/unit/types/primitives.test.js +108 -0
  150. package/dist/tests/unit/utils/address-parser.test.d.ts +5 -0
  151. package/dist/tests/unit/utils/address-parser.test.d.ts.map +1 -0
  152. package/dist/tests/unit/utils/address-parser.test.js +203 -0
  153. package/dist/tests/unit/utils/format-validators.test.d.ts +5 -0
  154. package/dist/tests/unit/utils/format-validators.test.d.ts.map +1 -0
  155. package/dist/tests/unit/utils/format-validators.test.js +224 -0
  156. package/dist/tests/unit/utils/line-reader.test.d.ts +5 -0
  157. package/dist/tests/unit/utils/line-reader.test.d.ts.map +1 -0
  158. package/dist/tests/unit/utils/line-reader.test.js +157 -0
  159. package/dist/tests/unit/utils/time-converter.test.d.ts +5 -0
  160. package/dist/tests/unit/utils/time-converter.test.d.ts.map +1 -0
  161. package/dist/tests/unit/utils/time-converter.test.js +190 -0
  162. package/dist/tests/unit/validator/media-validator.test.d.ts +5 -0
  163. package/dist/tests/unit/validator/media-validator.test.d.ts.map +1 -0
  164. package/dist/tests/unit/validator/media-validator.test.js +313 -0
  165. package/dist/tests/unit/validator/semantic-validator.test.d.ts +5 -0
  166. package/dist/tests/unit/validator/semantic-validator.test.d.ts.map +1 -0
  167. package/dist/tests/unit/validator/semantic-validator.test.js +262 -0
  168. package/dist/tests/unit/validator/session-validator.test.d.ts +5 -0
  169. package/dist/tests/unit/validator/session-validator.test.d.ts.map +1 -0
  170. package/dist/tests/unit/validator/session-validator.test.js +447 -0
  171. 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"}