@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,77 @@
1
+ /**
2
+ * Format validators for SDP (RFC 8866)
3
+ *
4
+ * Validates email, phone, and URI formats per RFC 8866 Section 5.5, 5.6 and Section 9 (Grammar)
5
+ */
6
+ /**
7
+ * Validate email address format per RFC 8866 Section 9
8
+ *
9
+ * email-address = address-and-comment / dispname-and-address / addr-spec
10
+ * address-and-comment = addr-spec 1*SP "(" 1*email-safe ")"
11
+ * dispname-and-address = 1*email-safe 1*SP "<" addr-spec ">"
12
+ *
13
+ * @param input - Email field value
14
+ * @returns true if format is valid
15
+ */
16
+ export declare function isValidEmailFormat(input: string): boolean;
17
+ /**
18
+ * Parse email components
19
+ *
20
+ * @param input - Email field value
21
+ * @returns Parsed components or null if invalid
22
+ */
23
+ export declare function parseEmailComponents(input: string): {
24
+ email: string;
25
+ displayName?: string;
26
+ } | null;
27
+ /**
28
+ * Validate phone number format per RFC 8866 Section 9
29
+ *
30
+ * phone-number = phone *SP "(" 1*email-safe ")" /
31
+ * 1*email-safe "<" phone ">" /
32
+ * phone
33
+ *
34
+ * @param input - Phone field value
35
+ * @returns true if format is valid
36
+ */
37
+ export declare function isValidPhoneFormat(input: string): boolean;
38
+ /**
39
+ * Parse phone components
40
+ *
41
+ * @param input - Phone field value
42
+ * @returns Parsed components or null if invalid
43
+ */
44
+ export declare function parsePhoneComponents(input: string): {
45
+ phone: string;
46
+ displayName?: string;
47
+ } | null;
48
+ /**
49
+ * Validate URI-reference format per RFC 3986
50
+ *
51
+ * URI-reference = URI / relative-ref
52
+ * URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
53
+ * relative-ref = relative-part [ "?" query ] [ "#" fragment ]
54
+ *
55
+ * This is a simplified validation that checks:
56
+ * 1. All characters are valid URI characters
57
+ * 2. If there's a scheme, it's properly formatted
58
+ * 3. Percent-encoding is valid (% followed by two hex digits)
59
+ * 4. No control characters or spaces
60
+ *
61
+ * @param input - URI field value
62
+ * @returns true if format is valid
63
+ */
64
+ export declare function isValidURIReference(input: string): boolean;
65
+ /**
66
+ * Parse URI components
67
+ *
68
+ * Extracts scheme and the rest of the URI if present
69
+ *
70
+ * @param input - URI field value
71
+ * @returns Parsed components or null if invalid
72
+ */
73
+ export declare function parseURIComponents(input: string): {
74
+ scheme?: string;
75
+ rest: string;
76
+ } | null;
77
+ //# sourceMappingURL=format-validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-validators.d.ts","sourceRoot":"","sources":["../../../src/utils/format-validators.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgJH;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAmCzD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,GACZ;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAkChD;AAiDD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAqBzD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,GACZ;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAgChD;AAuHD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CA2C1D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,GACZ;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAuB1C"}
@@ -0,0 +1,504 @@
1
+ "use strict";
2
+ /**
3
+ * Format validators for SDP (RFC 8866)
4
+ *
5
+ * Validates email, phone, and URI formats per RFC 8866 Section 5.5, 5.6 and Section 9 (Grammar)
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.isValidEmailFormat = isValidEmailFormat;
9
+ exports.parseEmailComponents = parseEmailComponents;
10
+ exports.isValidPhoneFormat = isValidPhoneFormat;
11
+ exports.parsePhoneComponents = parsePhoneComponents;
12
+ exports.isValidURIReference = isValidURIReference;
13
+ exports.parseURIComponents = parseURIComponents;
14
+ // ============================================================================
15
+ // Email-safe Character Validation
16
+ // ============================================================================
17
+ /**
18
+ * Check if a character is email-safe per RFC 8866 Section 9
19
+ *
20
+ * email-safe = %x01-09/%x0B-0C/%x0E-27/%x2A-3B/%x3D/%x3F-FF
21
+ * (any byte except NUL, CR, LF, or the quoting characters ()<>)
22
+ *
23
+ * @param ch - Character to check
24
+ * @returns true if character is email-safe
25
+ */
26
+ function isEmailSafe(ch) {
27
+ const code = ch.charCodeAt(0);
28
+ return ((code >= 0x01 && code <= 0x09) || // %x01-09
29
+ (code >= 0x0b && code <= 0x0c) || // %x0B-0C
30
+ (code >= 0x0e && code <= 0x27) || // %x0E-27 (includes most printable chars except ()
31
+ (code >= 0x2a && code <= 0x3b) || // %x2A-3B (includes *, +, -, ., digits, :, ;)
32
+ code === 0x3d || // %x3D (=)
33
+ (code >= 0x3f && code <= 0xff) // %x3F-FF (includes ?, @, A-Z, a-z, etc.)
34
+ );
35
+ }
36
+ /**
37
+ * Check if an entire string is email-safe
38
+ */
39
+ function isEmailSafeString(str) {
40
+ for (const ch of str) {
41
+ if (!isEmailSafe(ch)) {
42
+ return false;
43
+ }
44
+ }
45
+ return str.length > 0;
46
+ }
47
+ // ============================================================================
48
+ // ReDoS-safe Format Parsing Helpers
49
+ // ============================================================================
50
+ /**
51
+ * Parse "display <content>" format without regex to avoid ReDoS
52
+ * @param input - String to parse
53
+ * @param requireSpaceBeforeOpen - Whether space is required before <
54
+ * @returns Parsed parts or null if invalid format
55
+ */
56
+ function parseAngleBracketFormat(input, requireSpaceBeforeOpen) {
57
+ // Must end with >
58
+ if (!input.endsWith('>'))
59
+ return null;
60
+ // Find the last < (in case display name contains <, though it shouldn't)
61
+ const openIdx = input.lastIndexOf('<');
62
+ if (openIdx === -1 || openIdx === 0)
63
+ return null;
64
+ const content = input.substring(openIdx + 1, input.length - 1);
65
+ if (content.length === 0)
66
+ return null;
67
+ const beforeOpen = input.substring(0, openIdx);
68
+ const display = beforeOpen.trimEnd();
69
+ if (display.length === 0)
70
+ return null;
71
+ // Check if there's whitespace before < when required
72
+ if (requireSpaceBeforeOpen) {
73
+ if (beforeOpen.length === display.length) {
74
+ // No trailing whitespace found
75
+ return null;
76
+ }
77
+ }
78
+ return { display, content };
79
+ }
80
+ /**
81
+ * Parse "content (comment)" format without regex to avoid ReDoS
82
+ * @param input - String to parse
83
+ * @returns Parsed parts or null if invalid format
84
+ */
85
+ function parseParenthesisFormat(input) {
86
+ // Must end with )
87
+ if (!input.endsWith(')'))
88
+ return null;
89
+ // Find the last (
90
+ const openIdx = input.lastIndexOf('(');
91
+ if (openIdx === -1 || openIdx === 0)
92
+ return null;
93
+ const comment = input.substring(openIdx + 1, input.length - 1);
94
+ if (comment.length === 0)
95
+ return null;
96
+ const content = input.substring(0, openIdx).trimEnd();
97
+ if (content.length === 0)
98
+ return null;
99
+ return { content, comment };
100
+ }
101
+ // ============================================================================
102
+ // Email Format Validation (RFC 8866 Section 5.6, Section 9)
103
+ // ============================================================================
104
+ /**
105
+ * Basic addr-spec validation
106
+ *
107
+ * This is a simplified validation for RFC 5322 addr-spec.
108
+ * Full RFC 5322 addr-spec parsing is complex; this validates the common pattern.
109
+ *
110
+ * @param email - Email address to validate
111
+ * @returns true if appears to be a valid email addr-spec
112
+ */
113
+ function isValidAddrSpec(email) {
114
+ // Basic pattern: local-part@domain
115
+ // Must have exactly one @ not at start or end
116
+ const atIndex = email.indexOf('@');
117
+ if (atIndex <= 0 || atIndex === email.length - 1) {
118
+ return false;
119
+ }
120
+ // Check no second @
121
+ if (email.indexOf('@', atIndex + 1) !== -1) {
122
+ return false;
123
+ }
124
+ const localPart = email.substring(0, atIndex);
125
+ const domain = email.substring(atIndex + 1);
126
+ // Local part and domain must not be empty
127
+ if (localPart.length === 0 || domain.length === 0) {
128
+ return false;
129
+ }
130
+ // Domain must have at least one character and no leading/trailing dots
131
+ if (domain.startsWith('.') || domain.endsWith('.')) {
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+ /**
137
+ * Validate email address format per RFC 8866 Section 9
138
+ *
139
+ * email-address = address-and-comment / dispname-and-address / addr-spec
140
+ * address-and-comment = addr-spec 1*SP "(" 1*email-safe ")"
141
+ * dispname-and-address = 1*email-safe 1*SP "<" addr-spec ">"
142
+ *
143
+ * @param input - Email field value
144
+ * @returns true if format is valid
145
+ */
146
+ function isValidEmailFormat(input) {
147
+ const trimmed = input.trim();
148
+ if (trimmed.length === 0) {
149
+ return false;
150
+ }
151
+ // Check for dispname-and-address format: Display Name <email@domain>
152
+ // Must have at least one character before the angle brackets (1*email-safe)
153
+ const angleParsed = parseAngleBracketFormat(trimmed, true);
154
+ if (angleParsed) {
155
+ return isEmailSafeString(angleParsed.display) && isValidAddrSpec(angleParsed.content);
156
+ }
157
+ // Reject angle brackets without proper format (e.g., "<email@domain>" without display name)
158
+ if (trimmed.includes('<') || trimmed.includes('>')) {
159
+ return false;
160
+ }
161
+ // Check for address-and-comment format: email@domain (Display Name)
162
+ const parenParsed = parseParenthesisFormat(trimmed);
163
+ if (parenParsed) {
164
+ // Content must not contain whitespace (it's an addr-spec)
165
+ if (/\s/.test(parenParsed.content)) {
166
+ return false;
167
+ }
168
+ return isValidAddrSpec(parenParsed.content) && isEmailSafeString(parenParsed.comment);
169
+ }
170
+ // Reject unclosed or empty parentheses
171
+ if (trimmed.includes('(') || trimmed.includes(')')) {
172
+ return false;
173
+ }
174
+ // Plain addr-spec format: email@domain
175
+ return isValidAddrSpec(trimmed);
176
+ }
177
+ /**
178
+ * Parse email components
179
+ *
180
+ * @param input - Email field value
181
+ * @returns Parsed components or null if invalid
182
+ */
183
+ function parseEmailComponents(input) {
184
+ const trimmed = input.trim();
185
+ if (trimmed.length === 0) {
186
+ return null;
187
+ }
188
+ // Check for dispname-and-address format: Display Name <email@domain>
189
+ const angleParsed = parseAngleBracketFormat(trimmed, true);
190
+ if (angleParsed) {
191
+ if (isEmailSafeString(angleParsed.display) && isValidAddrSpec(angleParsed.content)) {
192
+ return { email: angleParsed.content, displayName: angleParsed.display };
193
+ }
194
+ return null;
195
+ }
196
+ // Check for address-and-comment format: email@domain (Display Name)
197
+ const parenParsed = parseParenthesisFormat(trimmed);
198
+ if (parenParsed) {
199
+ // Content must not contain whitespace (it's an addr-spec)
200
+ if (/\s/.test(parenParsed.content)) {
201
+ return null;
202
+ }
203
+ if (isValidAddrSpec(parenParsed.content) && isEmailSafeString(parenParsed.comment)) {
204
+ return { email: parenParsed.content, displayName: parenParsed.comment };
205
+ }
206
+ return null;
207
+ }
208
+ // Plain addr-spec format: email@domain
209
+ if (isValidAddrSpec(trimmed)) {
210
+ return { email: trimmed };
211
+ }
212
+ return null;
213
+ }
214
+ // ============================================================================
215
+ // Phone Format Validation (RFC 8866 Section 5.6, Section 9)
216
+ // ============================================================================
217
+ /**
218
+ * Validate phone number format per RFC 8866 Section 9
219
+ *
220
+ * phone = ["+"] DIGIT 1*(SP / "-" / DIGIT)
221
+ *
222
+ * A phone must:
223
+ * - Optionally start with "+"
224
+ * - Have at least one digit after optional "+"
225
+ * - Contain only digits, spaces, and hyphens after the first digit
226
+ *
227
+ * @param phone - Phone number to validate
228
+ * @returns true if format is valid
229
+ */
230
+ function isValidPhone(phone) {
231
+ if (phone.length === 0) {
232
+ return false;
233
+ }
234
+ let index = 0;
235
+ // Optional leading "+"
236
+ if (phone[index] === '+') {
237
+ index++;
238
+ }
239
+ // Must have at least one digit
240
+ if (index >= phone.length || !/[0-9]/.test(phone[index])) {
241
+ return false;
242
+ }
243
+ index++;
244
+ // Rest must be digits, spaces, or hyphens (1*(SP / "-" / DIGIT))
245
+ while (index < phone.length) {
246
+ const ch = phone[index];
247
+ if (ch !== ' ' && ch !== '-' && !/[0-9]/.test(ch)) {
248
+ return false;
249
+ }
250
+ index++;
251
+ }
252
+ return true;
253
+ }
254
+ /**
255
+ * Validate phone number format per RFC 8866 Section 9
256
+ *
257
+ * phone-number = phone *SP "(" 1*email-safe ")" /
258
+ * 1*email-safe "<" phone ">" /
259
+ * phone
260
+ *
261
+ * @param input - Phone field value
262
+ * @returns true if format is valid
263
+ */
264
+ function isValidPhoneFormat(input) {
265
+ const trimmed = input.trim();
266
+ if (trimmed.length === 0) {
267
+ return false;
268
+ }
269
+ // Check for dispname-and-phone format: Display Name <+1 555 1234>
270
+ // Space before < is optional for phone format
271
+ const angleParsed = parseAngleBracketFormat(trimmed, false);
272
+ if (angleParsed) {
273
+ return isEmailSafeString(angleParsed.display) && isValidPhone(angleParsed.content.trim());
274
+ }
275
+ // Check for phone-and-comment format: +1 555 1234 (Display Name)
276
+ const parenParsed = parseParenthesisFormat(trimmed);
277
+ if (parenParsed) {
278
+ return isValidPhone(parenParsed.content) && isEmailSafeString(parenParsed.comment);
279
+ }
280
+ // Plain phone format: +1 555 1234
281
+ return isValidPhone(trimmed);
282
+ }
283
+ /**
284
+ * Parse phone components
285
+ *
286
+ * @param input - Phone field value
287
+ * @returns Parsed components or null if invalid
288
+ */
289
+ function parsePhoneComponents(input) {
290
+ const trimmed = input.trim();
291
+ if (trimmed.length === 0) {
292
+ return null;
293
+ }
294
+ // Check for dispname-and-phone format: Display Name <+1 555 1234>
295
+ // Space before < is optional for phone format
296
+ const angleParsed = parseAngleBracketFormat(trimmed, false);
297
+ if (angleParsed) {
298
+ const phone = angleParsed.content.trim();
299
+ if (isEmailSafeString(angleParsed.display) && isValidPhone(phone)) {
300
+ return { phone, displayName: angleParsed.display };
301
+ }
302
+ return null;
303
+ }
304
+ // Check for phone-and-comment format: +1 555 1234 (Display Name)
305
+ const parenParsed = parseParenthesisFormat(trimmed);
306
+ if (parenParsed) {
307
+ if (isValidPhone(parenParsed.content) && isEmailSafeString(parenParsed.comment)) {
308
+ return { phone: parenParsed.content, displayName: parenParsed.comment };
309
+ }
310
+ return null;
311
+ }
312
+ // Plain phone format: +1 555 1234
313
+ if (isValidPhone(trimmed)) {
314
+ return { phone: trimmed };
315
+ }
316
+ return null;
317
+ }
318
+ // ============================================================================
319
+ // URI Format Validation (RFC 8866 Section 5.5, RFC 3986)
320
+ // ============================================================================
321
+ /**
322
+ * Check if a character is valid in a URI per RFC 3986
323
+ *
324
+ * Valid URI characters include:
325
+ * - unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
326
+ * - reserved = gen-delims / sub-delims
327
+ * - gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
328
+ * - sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
329
+ * - pct-encoded = "%" HEXDIG HEXDIG
330
+ *
331
+ * @param ch - Character to check
332
+ * @returns true if character is valid in a URI
333
+ */
334
+ function isURIChar(ch) {
335
+ const code = ch.charCodeAt(0);
336
+ // ALPHA (A-Z, a-z)
337
+ if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)) {
338
+ return true;
339
+ }
340
+ // DIGIT (0-9)
341
+ if (code >= 0x30 && code <= 0x39) {
342
+ return true;
343
+ }
344
+ // unreserved: - . _ ~
345
+ if (ch === '-' || ch === '.' || ch === '_' || ch === '~') {
346
+ return true;
347
+ }
348
+ // gen-delims: : / ? # [ ] @
349
+ if (ch === ':' || ch === '/' || ch === '?' || ch === '#' || ch === '[' || ch === ']' || ch === '@') {
350
+ return true;
351
+ }
352
+ // sub-delims: ! $ & ' ( ) * + , ; =
353
+ if (ch === '!' || ch === '$' || ch === '&' || ch === "'" || ch === '(' || ch === ')' ||
354
+ ch === '*' || ch === '+' || ch === ',' || ch === ';' || ch === '=') {
355
+ return true;
356
+ }
357
+ // percent-encoding marker
358
+ if (ch === '%') {
359
+ return true;
360
+ }
361
+ return false;
362
+ }
363
+ /**
364
+ * Check if a string is a valid URI scheme per RFC 3986
365
+ *
366
+ * scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
367
+ *
368
+ * @param scheme - Potential scheme string
369
+ * @returns true if valid scheme
370
+ */
371
+ function isValidScheme(scheme) {
372
+ if (scheme.length === 0) {
373
+ return false;
374
+ }
375
+ // First character must be ALPHA
376
+ const firstCode = scheme.charCodeAt(0);
377
+ if (!((firstCode >= 0x41 && firstCode <= 0x5a) || (firstCode >= 0x61 && firstCode <= 0x7a))) {
378
+ return false;
379
+ }
380
+ // Rest must be ALPHA / DIGIT / "+" / "-" / "."
381
+ for (let i = 1; i < scheme.length; i++) {
382
+ const code = scheme.charCodeAt(i);
383
+ const ch = scheme[i];
384
+ if (!((code >= 0x41 && code <= 0x5a) || // A-Z
385
+ (code >= 0x61 && code <= 0x7a) || // a-z
386
+ (code >= 0x30 && code <= 0x39) || // 0-9
387
+ ch === '+' || ch === '-' || ch === '.')) {
388
+ return false;
389
+ }
390
+ }
391
+ return true;
392
+ }
393
+ /**
394
+ * Validate percent-encoding in a URI
395
+ *
396
+ * Checks that all % characters are followed by exactly two hex digits
397
+ *
398
+ * @param uri - URI string to check
399
+ * @returns true if all percent-encoding is valid
400
+ */
401
+ function hasValidPercentEncoding(uri) {
402
+ let i = 0;
403
+ while (i < uri.length) {
404
+ if (uri[i] === '%') {
405
+ // Must be followed by exactly two hex digits
406
+ if (i + 2 >= uri.length) {
407
+ return false;
408
+ }
409
+ const hex1 = uri[i + 1];
410
+ const hex2 = uri[i + 2];
411
+ if (!/[0-9A-Fa-f]/.test(hex1) || !/[0-9A-Fa-f]/.test(hex2)) {
412
+ return false;
413
+ }
414
+ i += 3;
415
+ }
416
+ else {
417
+ i++;
418
+ }
419
+ }
420
+ return true;
421
+ }
422
+ /**
423
+ * Validate URI-reference format per RFC 3986
424
+ *
425
+ * URI-reference = URI / relative-ref
426
+ * URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
427
+ * relative-ref = relative-part [ "?" query ] [ "#" fragment ]
428
+ *
429
+ * This is a simplified validation that checks:
430
+ * 1. All characters are valid URI characters
431
+ * 2. If there's a scheme, it's properly formatted
432
+ * 3. Percent-encoding is valid (% followed by two hex digits)
433
+ * 4. No control characters or spaces
434
+ *
435
+ * @param input - URI field value
436
+ * @returns true if format is valid
437
+ */
438
+ function isValidURIReference(input) {
439
+ const trimmed = input.trim();
440
+ if (trimmed.length === 0) {
441
+ return false;
442
+ }
443
+ // Check for control characters or spaces (not allowed)
444
+ for (const ch of trimmed) {
445
+ const code = ch.charCodeAt(0);
446
+ if (code <= 0x20 || code === 0x7f) {
447
+ return false;
448
+ }
449
+ }
450
+ // Check all characters are valid URI characters
451
+ for (const ch of trimmed) {
452
+ if (!isURIChar(ch)) {
453
+ return false;
454
+ }
455
+ }
456
+ // Check percent-encoding is valid
457
+ if (!hasValidPercentEncoding(trimmed)) {
458
+ return false;
459
+ }
460
+ // Check if there's a scheme (look for first ":")
461
+ const colonIndex = trimmed.indexOf(':');
462
+ if (colonIndex > 0) {
463
+ // Potential scheme - validate it
464
+ const potentialScheme = trimmed.substring(0, colonIndex);
465
+ // If the potential scheme contains / or @ or ?, it's not a scheme
466
+ // (it's part of a relative reference like //host or path/to/file)
467
+ if (!potentialScheme.includes('/') && !potentialScheme.includes('@') && !potentialScheme.includes('?')) {
468
+ // This looks like a scheme, validate it
469
+ if (!isValidScheme(potentialScheme)) {
470
+ return false;
471
+ }
472
+ }
473
+ }
474
+ return true;
475
+ }
476
+ /**
477
+ * Parse URI components
478
+ *
479
+ * Extracts scheme and the rest of the URI if present
480
+ *
481
+ * @param input - URI field value
482
+ * @returns Parsed components or null if invalid
483
+ */
484
+ function parseURIComponents(input) {
485
+ if (!isValidURIReference(input)) {
486
+ return null;
487
+ }
488
+ const trimmed = input.trim();
489
+ // Check if there's a scheme
490
+ const colonIndex = trimmed.indexOf(':');
491
+ if (colonIndex > 0) {
492
+ const potentialScheme = trimmed.substring(0, colonIndex);
493
+ if (!potentialScheme.includes('/') && !potentialScheme.includes('@') && !potentialScheme.includes('?')) {
494
+ if (isValidScheme(potentialScheme)) {
495
+ return {
496
+ scheme: potentialScheme,
497
+ rest: trimmed.substring(colonIndex + 1),
498
+ };
499
+ }
500
+ }
501
+ }
502
+ // Relative reference (no scheme)
503
+ return { rest: trimmed };
504
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Line reading utilities for SDP (RFC 8866)
3
+ *
4
+ * RFC 8866 specifies CRLF as the standard line terminator, but parsers
5
+ * SHOULD accept LF alone for robustness. This module handles all three
6
+ * cases: CRLF, LF, and CR.
7
+ */
8
+ /**
9
+ * Parsed line structure
10
+ */
11
+ export interface ParsedLine {
12
+ /** Line type (single character before '=') */
13
+ readonly type: string;
14
+ /** Line value (everything after '=') */
15
+ readonly value: string;
16
+ /** Original line number (1-indexed) */
17
+ readonly lineNumber: number;
18
+ }
19
+ /**
20
+ * Split input text into lines, handling CRLF, LF, and CR
21
+ *
22
+ * RFC 8866 Section 5: Text fields use UTF-8 encoding, line ending is CRLF,
23
+ * but parsers SHOULD accept LF alone for compatibility.
24
+ *
25
+ * @param input - Raw SDP text
26
+ * @returns Array of lines (line endings removed)
27
+ */
28
+ export declare function splitLines(input: string): string[];
29
+ /**
30
+ * Parse a single SDP line into type and value
31
+ *
32
+ * SDP line format: <type>=<value>
33
+ * where <type> is a single character
34
+ *
35
+ * @param line - Single SDP line
36
+ * @returns Parsed line with type and value, or null if invalid
37
+ */
38
+ export declare function parseLine(line: string): {
39
+ type: string;
40
+ value: string;
41
+ } | null;
42
+ /**
43
+ * Parse all lines in SDP input into structured format
44
+ *
45
+ * @param input - Raw SDP text
46
+ * @returns Array of parsed lines (skips empty/invalid lines)
47
+ */
48
+ export declare function parseLines(input: string): ParsedLine[];
49
+ /**
50
+ * Join lines back into SDP text format with CRLF line endings
51
+ *
52
+ * Always outputs standard CRLF line endings per RFC 8866,
53
+ * even if input used LF or CR.
54
+ *
55
+ * @param lines - Array of line strings (without line endings)
56
+ * @returns SDP text with CRLF line endings
57
+ */
58
+ export declare function joinLines(lines: string[]): string;
59
+ /**
60
+ * Format a line in SDP format: <type>=<value>
61
+ *
62
+ * @param type - Single character line type
63
+ * @param value - Line value
64
+ * @returns Formatted SDP line (without line ending)
65
+ */
66
+ export declare function formatLine(type: string, value: string): string;
67
+ /**
68
+ * Validate that a line type is a valid SDP field
69
+ *
70
+ * Valid SDP field types per RFC 8866:
71
+ * v, o, s, i, u, e, p, c, b, t, r, z, k, a, m
72
+ *
73
+ * @param type - Line type character
74
+ * @returns true if valid SDP field type
75
+ */
76
+ export declare function isValidLineType(type: string): boolean;
77
+ /**
78
+ * Get display name for a line type
79
+ *
80
+ * @param type - Line type character
81
+ * @returns Human-readable field name
82
+ */
83
+ export declare function getLineTypeName(type: string): string;
84
+ //# sourceMappingURL=line-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line-reader.d.ts","sourceRoot":"","sources":["../../../src/utils/line-reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+BlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoB9E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAiBtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBpD"}