@tomgiee/tsdp 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -39
- package/dist/src/builder/media-builder.d.ts.map +1 -1
- package/dist/src/builder/media-builder.js +5 -12
- package/dist/src/builder/session-builder.d.ts.map +1 -1
- package/dist/src/builder/session-builder.js +4 -14
- package/dist/src/index.d.ts +6 -10
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +37 -49
- package/dist/src/parser/core.d.ts +366 -0
- package/dist/src/parser/core.d.ts.map +1 -0
- package/dist/src/parser/core.js +802 -0
- package/dist/src/parser/field-parser.d.ts +51 -8
- package/dist/src/parser/field-parser.d.ts.map +1 -1
- package/dist/src/parser/field-parser.js +91 -23
- package/dist/src/parser/media-parser.d.ts +1 -1
- package/dist/src/parser/media-parser.d.ts.map +1 -1
- package/dist/src/parser/media-parser.js +9 -15
- package/dist/src/parser/session-parser.d.ts.map +1 -1
- package/dist/src/parser/session-parser.js +16 -17
- package/dist/src/parser/time-parser.d.ts +1 -1
- package/dist/src/parser/time-parser.d.ts.map +1 -1
- package/dist/src/parser/time-parser.js +8 -8
- package/dist/src/serializer/fields.d.ts +167 -0
- package/dist/src/serializer/fields.d.ts.map +1 -0
- package/dist/src/serializer/fields.js +320 -0
- package/dist/src/serializer/media-serializer.js +6 -7
- package/dist/src/serializer/session-serializer.js +13 -15
- package/dist/src/types/attributes.d.ts.map +1 -1
- package/dist/src/types/fields.d.ts +5 -5
- package/dist/src/types/fields.d.ts.map +1 -1
- package/dist/src/types/fields.js +4 -4
- package/dist/src/types/media.d.ts +9 -10
- package/dist/src/types/media.d.ts.map +1 -1
- package/dist/src/types/media.js +2 -4
- package/dist/src/types/network.d.ts +15 -56
- package/dist/src/types/network.d.ts.map +1 -1
- package/dist/src/types/network.js +3 -34
- package/dist/src/types/primitives.d.ts +3 -147
- package/dist/src/types/primitives.d.ts.map +1 -1
- package/dist/src/types/primitives.js +2 -171
- package/dist/src/types/session.d.ts +14 -14
- package/dist/src/types/session.d.ts.map +1 -1
- package/dist/src/types/time.d.ts +4 -4
- package/dist/src/types/time.d.ts.map +1 -1
- package/dist/src/types/time.js +8 -6
- package/dist/src/utils/address-parser.d.ts +4 -4
- package/dist/src/utils/address-parser.d.ts.map +1 -1
- package/dist/src/utils/address-parser.js +9 -16
- package/dist/src/validator/validator.d.ts +94 -0
- package/dist/src/validator/validator.d.ts.map +1 -0
- package/dist/src/validator/validator.js +573 -0
- package/dist/tests/unit/parser/attribute-parser.test.js +106 -107
- package/dist/tests/unit/parser/field-parser.test.js +66 -66
- package/dist/tests/unit/parser/media-parser.test.js +38 -38
- package/dist/tests/unit/parser/primitive-parser.test.js +89 -90
- package/dist/tests/unit/parser/scanner.test.js +32 -32
- package/dist/tests/unit/parser/time-parser.test.js +22 -22
- package/dist/tests/unit/serializer/attribute-serializer.test.js +22 -22
- package/dist/tests/unit/serializer/field-serializer.test.js +57 -57
- package/dist/tests/unit/serializer/media-serializer.test.js +5 -6
- package/dist/tests/unit/serializer/session-serializer.test.js +24 -24
- package/dist/tests/unit/serializer/time-serializer.test.js +16 -16
- package/dist/tests/unit/types/network.test.js +21 -56
- package/dist/tests/unit/types/primitives.test.js +0 -39
- package/dist/tests/unit/validator/media-validator.test.js +34 -35
- package/dist/tests/unit/validator/semantic-validator.test.js +36 -37
- package/dist/tests/unit/validator/session-validator.test.js +54 -54
- package/package.json +1 -1
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validators for SDP (RFC 8866)
|
|
4
|
+
*
|
|
5
|
+
* This module consolidates:
|
|
6
|
+
* - Session-level validation
|
|
7
|
+
* - Media description validation
|
|
8
|
+
* - Cross-field semantic validation
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.validResult = validResult;
|
|
12
|
+
exports.invalidResult = invalidResult;
|
|
13
|
+
exports.validateSdp = validateSdp;
|
|
14
|
+
exports.validateSessionDescription = validateSessionDescription;
|
|
15
|
+
exports.validateMediaDescription = validateMediaDescription;
|
|
16
|
+
exports.validateAllMediaDescriptions = validateAllMediaDescriptions;
|
|
17
|
+
exports.validateSemanticConstraints = validateSemanticConstraints;
|
|
18
|
+
const errors_1 = require("../types/errors");
|
|
19
|
+
const primitives_1 = require("../types/primitives");
|
|
20
|
+
const attributes_1 = require("../types/attributes");
|
|
21
|
+
const format_validators_1 = require("../utils/format-validators");
|
|
22
|
+
/**
|
|
23
|
+
* Create a valid result
|
|
24
|
+
*/
|
|
25
|
+
function validResult() {
|
|
26
|
+
return { valid: true, errors: [] };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create an invalid result with errors
|
|
30
|
+
*/
|
|
31
|
+
function invalidResult(errors) {
|
|
32
|
+
return { valid: false, errors };
|
|
33
|
+
}
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Main Validation Entry Point
|
|
36
|
+
// ============================================================================
|
|
37
|
+
/**
|
|
38
|
+
* Perform complete semantic validation of a session description
|
|
39
|
+
*
|
|
40
|
+
* This is the main validation entry point that checks:
|
|
41
|
+
* 1. Session-level validation
|
|
42
|
+
* 2. Media-level validation
|
|
43
|
+
* 3. Cross-field semantic constraints
|
|
44
|
+
*
|
|
45
|
+
* @param session - Session description to validate
|
|
46
|
+
* @param options - Optional validation options
|
|
47
|
+
* @returns ValidationResult with all errors found
|
|
48
|
+
*/
|
|
49
|
+
function validateSdp(session, options = {}) {
|
|
50
|
+
const allErrors = [];
|
|
51
|
+
// 1. Session-level validation
|
|
52
|
+
const sessionResult = validateSessionDescription(session);
|
|
53
|
+
allErrors.push(...sessionResult.errors);
|
|
54
|
+
// 2. Check for deprecated key field
|
|
55
|
+
if (!options.allowDeprecatedKeyField && session.key !== undefined) {
|
|
56
|
+
allErrors.push(errors_1.ValidationError.constraintViolation('The k= field is deprecated per RFC 8866', 'RFC8866:5.12'));
|
|
57
|
+
}
|
|
58
|
+
// 3. Require session-level connection if specified
|
|
59
|
+
if (options.requireSessionConnection && session.connection === undefined) {
|
|
60
|
+
allErrors.push(errors_1.ValidationError.constraintViolation('Session-level connection information is required', 'RFC8866:5.7'));
|
|
61
|
+
}
|
|
62
|
+
// 4. Media-level validation
|
|
63
|
+
const hasSessionConnection = session.connection !== undefined;
|
|
64
|
+
const mediaResult = validateAllMediaDescriptions(session.mediaDescriptions, hasSessionConnection);
|
|
65
|
+
allErrors.push(...mediaResult.errors);
|
|
66
|
+
// 5. Cross-field semantic validation (if not skipped)
|
|
67
|
+
if (!options.skipSemanticValidation) {
|
|
68
|
+
const semanticResult = validateSemanticConstraints(session);
|
|
69
|
+
allErrors.push(...semanticResult.errors);
|
|
70
|
+
}
|
|
71
|
+
return allErrors.length === 0 ? validResult() : invalidResult(allErrors);
|
|
72
|
+
}
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Session Validator
|
|
75
|
+
// ============================================================================
|
|
76
|
+
/**
|
|
77
|
+
* Validate a complete session description
|
|
78
|
+
*
|
|
79
|
+
* Checks RFC 8866 Section 5 requirements:
|
|
80
|
+
* - Required fields are present (v, o, s, t)
|
|
81
|
+
* - Version is 0
|
|
82
|
+
* - At least one time description
|
|
83
|
+
* - Connection coverage (session or all media)
|
|
84
|
+
*
|
|
85
|
+
* @param session - Session description to validate
|
|
86
|
+
* @returns ValidationResult with any errors found
|
|
87
|
+
*/
|
|
88
|
+
function validateSessionDescription(session) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
// 1. Validate version (RFC 8866 Section 5.1)
|
|
91
|
+
validateVersion(session.version, errors);
|
|
92
|
+
// 2. Validate origin (RFC 8866 Section 5.2)
|
|
93
|
+
validateOrigin(session, errors);
|
|
94
|
+
// 3. Validate session name (RFC 8866 Section 5.3)
|
|
95
|
+
validateSessionName(session, errors);
|
|
96
|
+
// 4. Validate time descriptions (RFC 8866 Section 5.9)
|
|
97
|
+
validateTimeDescriptions(session, errors);
|
|
98
|
+
// 5. Validate connection coverage (RFC 8866 Section 5.7)
|
|
99
|
+
validateConnectionCoverage(session, errors);
|
|
100
|
+
// 6. Validate bandwidths (RFC 8866 Section 5.8)
|
|
101
|
+
validateBandwidths(session, errors);
|
|
102
|
+
// 7. Validate attributes (RFC 8866 Section 6)
|
|
103
|
+
validateSessionAttributes(session, errors);
|
|
104
|
+
// 8. Validate email format (RFC 8866 Section 5.6)
|
|
105
|
+
validateEmails(session, errors);
|
|
106
|
+
// 9. Validate phone format (RFC 8866 Section 5.6)
|
|
107
|
+
validatePhones(session, errors);
|
|
108
|
+
// 10. Validate URI format (RFC 8866 Section 5.5)
|
|
109
|
+
validateUri(session, errors);
|
|
110
|
+
return errors.length === 0 ? validResult() : invalidResult(errors);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Validate version field
|
|
114
|
+
*/
|
|
115
|
+
function validateVersion(version, errors) {
|
|
116
|
+
if (version !== 0) {
|
|
117
|
+
errors.push(errors_1.ValidationError.invalidValue('version', version, 'SDP version must be 0', 'RFC8866:5.1'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate origin field
|
|
122
|
+
*/
|
|
123
|
+
function validateOrigin(session, errors) {
|
|
124
|
+
if (!session.origin) {
|
|
125
|
+
errors.push(errors_1.ValidationError.missingField('origin', 'RFC8866:5.2'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!session.origin.username || session.origin.username.length === 0) {
|
|
129
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.username', session.origin.username, 'Username cannot be empty', 'RFC8866:5.2'));
|
|
130
|
+
}
|
|
131
|
+
if (!session.origin.sessId || session.origin.sessId.length === 0) {
|
|
132
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.sessId', session.origin.sessId, 'Session ID cannot be empty', 'RFC8866:5.2'));
|
|
133
|
+
}
|
|
134
|
+
else if (!(0, primitives_1.isNumericString)(session.origin.sessId)) {
|
|
135
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.sessId', session.origin.sessId, 'Session ID must contain only digits (0-9)', 'RFC8866:5.2'));
|
|
136
|
+
}
|
|
137
|
+
if (!session.origin.sessVersion || session.origin.sessVersion.length === 0) {
|
|
138
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.sessVersion', session.origin.sessVersion, 'Session version cannot be empty', 'RFC8866:5.2'));
|
|
139
|
+
}
|
|
140
|
+
else if (!(0, primitives_1.isNumericString)(session.origin.sessVersion)) {
|
|
141
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.sessVersion', session.origin.sessVersion, 'Session version must contain only digits (0-9)', 'RFC8866:5.2'));
|
|
142
|
+
}
|
|
143
|
+
if (!session.origin.netType || session.origin.netType.length === 0) {
|
|
144
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.netType', session.origin.netType, 'Network type cannot be empty', 'RFC8866:5.2'));
|
|
145
|
+
}
|
|
146
|
+
if (!session.origin.addrType || session.origin.addrType.length === 0) {
|
|
147
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.addrType', session.origin.addrType, 'Address type cannot be empty', 'RFC8866:5.2'));
|
|
148
|
+
}
|
|
149
|
+
if (!session.origin.unicastAddress || session.origin.unicastAddress.length === 0) {
|
|
150
|
+
errors.push(errors_1.ValidationError.invalidValue('origin.unicastAddress', session.origin.unicastAddress, 'Unicast address cannot be empty', 'RFC8866:5.2'));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate session name field
|
|
155
|
+
*/
|
|
156
|
+
function validateSessionName(session, errors) {
|
|
157
|
+
if (session.sessionName === undefined || session.sessionName === null) {
|
|
158
|
+
errors.push(errors_1.ValidationError.missingField('sessionName', 'RFC8866:5.3'));
|
|
159
|
+
}
|
|
160
|
+
else if (session.sessionName.length === 0) {
|
|
161
|
+
errors.push(errors_1.ValidationError.invalidValue('sessionName', session.sessionName, 'Session name cannot be empty (use " " if no meaningful name)', 'RFC8866:5.3'));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validate time descriptions
|
|
166
|
+
*/
|
|
167
|
+
function validateTimeDescriptions(session, errors) {
|
|
168
|
+
if (!session.timeDescriptions || session.timeDescriptions.length === 0) {
|
|
169
|
+
errors.push(errors_1.ValidationError.constraintViolation('At least one time description (t=) is required', 'RFC8866:5.9'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (let i = 0; i < session.timeDescriptions.length; i++) {
|
|
173
|
+
const td = session.timeDescriptions[i];
|
|
174
|
+
if (!td.timing) {
|
|
175
|
+
errors.push(errors_1.ValidationError.missingField(`timeDescriptions[${i}].timing`, 'RFC8866:5.9'));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const { startTime, stopTime } = td.timing;
|
|
179
|
+
if (startTime !== 0 && stopTime !== 0 && startTime > stopTime) {
|
|
180
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Time description ${i}: start time (${startTime}) must be before or equal to stop time (${stopTime})`, 'RFC8866:5.9'));
|
|
181
|
+
}
|
|
182
|
+
for (let j = 0; j < td.repeats.length; j++) {
|
|
183
|
+
const repeat = td.repeats[j];
|
|
184
|
+
if (!repeat.offsets || repeat.offsets.length === 0) {
|
|
185
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Time description ${i}, repeat ${j}: at least one offset is required`, 'RFC8866:5.10'));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (td.timezone) {
|
|
189
|
+
if (!td.timezone.adjustments || td.timezone.adjustments.length === 0) {
|
|
190
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Time description ${i}: timezone field requires at least one adjustment`, 'RFC8866:5.11'));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Validate connection coverage
|
|
197
|
+
*/
|
|
198
|
+
function validateConnectionCoverage(session, errors) {
|
|
199
|
+
var _a;
|
|
200
|
+
const hasSessionConnection = session.connection !== undefined;
|
|
201
|
+
const mediaDescriptions = (_a = session.mediaDescriptions) !== null && _a !== void 0 ? _a : [];
|
|
202
|
+
if (mediaDescriptions.length === 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (hasSessionConnection) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (let i = 0; i < mediaDescriptions.length; i++) {
|
|
209
|
+
const md = mediaDescriptions[i];
|
|
210
|
+
if (!md.connections || md.connections.length === 0) {
|
|
211
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Media description ${i} (${md.media.type}): connection information required when no session-level connection is present`, 'RFC8866:5.7'));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Validate bandwidth fields
|
|
217
|
+
*/
|
|
218
|
+
function validateBandwidths(session, errors) {
|
|
219
|
+
for (let i = 0; i < session.bandwidths.length; i++) {
|
|
220
|
+
const bw = session.bandwidths[i];
|
|
221
|
+
if (bw.value < 0) {
|
|
222
|
+
errors.push(errors_1.ValidationError.invalidValue(`bandwidths[${i}].value`, bw.value, 'Bandwidth value must be non-negative', 'RFC8866:5.8'));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Validate session-level attributes
|
|
228
|
+
*/
|
|
229
|
+
function validateSessionAttributes(session, errors) {
|
|
230
|
+
const directionAttrs = ['sendrecv', 'sendonly', 'recvonly', 'inactive'];
|
|
231
|
+
let directionCount = 0;
|
|
232
|
+
for (const attr of session.attributes) {
|
|
233
|
+
if (attr.kind === 'property' && directionAttrs.includes(attr.name)) {
|
|
234
|
+
directionCount++;
|
|
235
|
+
}
|
|
236
|
+
if (attributes_1.MEDIA_ONLY_ATTRIBUTES.includes(attr.name)) {
|
|
237
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Attribute '${attr.name}' is only valid at media level, not session level`, 'RFC8866:6'));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (directionCount > 1) {
|
|
241
|
+
errors.push(errors_1.ValidationError.constraintViolation('Only one direction attribute (sendrecv/sendonly/recvonly/inactive) allowed at session level', 'RFC8866:6.7'));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate email addresses
|
|
246
|
+
*/
|
|
247
|
+
function validateEmails(session, errors) {
|
|
248
|
+
if (!session.emails || session.emails.length === 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (let i = 0; i < session.emails.length; i++) {
|
|
252
|
+
const email = session.emails[i];
|
|
253
|
+
if (!(0, format_validators_1.isValidEmailFormat)(email)) {
|
|
254
|
+
errors.push(errors_1.ValidationError.invalidValue(`emails[${i}]`, email, 'Invalid email format. Expected: addr-spec, addr-spec (comment), or Display Name <addr-spec>', 'RFC8866:5.6'));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Validate phone numbers
|
|
260
|
+
*/
|
|
261
|
+
function validatePhones(session, errors) {
|
|
262
|
+
if (!session.phones || session.phones.length === 0) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
for (let i = 0; i < session.phones.length; i++) {
|
|
266
|
+
const phone = session.phones[i];
|
|
267
|
+
if (!(0, format_validators_1.isValidPhoneFormat)(phone)) {
|
|
268
|
+
errors.push(errors_1.ValidationError.invalidValue(`phones[${i}]`, phone, 'Invalid phone format. Expected: phone, phone (comment), or Display Name <phone>', 'RFC8866:5.6'));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Validate URI
|
|
274
|
+
*/
|
|
275
|
+
function validateUri(session, errors) {
|
|
276
|
+
if (!session.uri) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!(0, format_validators_1.isValidURIReference)(session.uri)) {
|
|
280
|
+
errors.push(errors_1.ValidationError.invalidValue('uri', session.uri, 'Invalid URI format per RFC 3986', 'RFC8866:5.5'));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Media Description Validator
|
|
285
|
+
// ============================================================================
|
|
286
|
+
/**
|
|
287
|
+
* Validate a media description
|
|
288
|
+
*
|
|
289
|
+
* @param media - Media description to validate
|
|
290
|
+
* @param index - Index of media description (for error messages)
|
|
291
|
+
* @param hasSessionConnection - Whether session-level connection exists
|
|
292
|
+
* @returns ValidationResult with any errors found
|
|
293
|
+
*/
|
|
294
|
+
function validateMediaDescription(media, index, hasSessionConnection) {
|
|
295
|
+
const errors = [];
|
|
296
|
+
const prefix = `mediaDescriptions[${index}]`;
|
|
297
|
+
validateMediaField(media, prefix, errors);
|
|
298
|
+
validatePort(media, prefix, errors);
|
|
299
|
+
validateFormats(media, prefix, errors);
|
|
300
|
+
if (!hasSessionConnection) {
|
|
301
|
+
validateMediaConnection(media, prefix, errors);
|
|
302
|
+
}
|
|
303
|
+
validateMediaBandwidths(media, prefix, errors);
|
|
304
|
+
validateMediaAttributes(media, prefix, errors);
|
|
305
|
+
return errors.length === 0 ? validResult() : invalidResult(errors);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Validate media field
|
|
309
|
+
*/
|
|
310
|
+
function validateMediaField(media, prefix, errors) {
|
|
311
|
+
if (!media.media.type || media.media.type.length === 0) {
|
|
312
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.type`, media.media.type, 'Media type cannot be empty', 'RFC8866:5.14'));
|
|
313
|
+
}
|
|
314
|
+
if (!media.media.proto || media.media.proto.length === 0) {
|
|
315
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.proto`, media.media.proto, 'Protocol cannot be empty', 'RFC8866:5.14'));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Validate port
|
|
320
|
+
*/
|
|
321
|
+
function validatePort(media, prefix, errors) {
|
|
322
|
+
const port = media.media.port;
|
|
323
|
+
if (port.kind === 'simple') {
|
|
324
|
+
if (port.value < 0 || port.value > 65535) {
|
|
325
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.port`, port.value, 'Port must be 0-65535', 'RFC8866:5.14'));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (port.kind === 'range') {
|
|
329
|
+
if (port.base < 0 || port.base > 65535) {
|
|
330
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.port.base`, port.base, 'Port must be 0-65535', 'RFC8866:5.14'));
|
|
331
|
+
}
|
|
332
|
+
if (port.count < 1) {
|
|
333
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.port.count`, port.count, 'Port count must be at least 1', 'RFC8866:5.14'));
|
|
334
|
+
}
|
|
335
|
+
if (port.base + port.count - 1 > 65535) {
|
|
336
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: port range ${port.base}/${port.count} exceeds 65535`, 'RFC8866:5.14'));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Validate formats
|
|
342
|
+
*/
|
|
343
|
+
function validateFormats(media, prefix, errors) {
|
|
344
|
+
if (!media.media.formats || media.media.formats.length === 0) {
|
|
345
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: at least one media format is required`, 'RFC8866:5.14'));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Validate media-level connection
|
|
350
|
+
*/
|
|
351
|
+
function validateMediaConnection(media, prefix, errors) {
|
|
352
|
+
if (!media.connections || media.connections.length === 0) {
|
|
353
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: connection information required when no session-level connection`, 'RFC8866:5.7'));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Validate media-level bandwidths
|
|
358
|
+
*/
|
|
359
|
+
function validateMediaBandwidths(media, prefix, errors) {
|
|
360
|
+
for (let i = 0; i < media.bandwidths.length; i++) {
|
|
361
|
+
const bw = media.bandwidths[i];
|
|
362
|
+
if (bw.value < 0) {
|
|
363
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.bandwidths[${i}].value`, bw.value, 'Bandwidth value must be non-negative', 'RFC8866:5.8'));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Validate media-level attributes
|
|
369
|
+
*/
|
|
370
|
+
function validateMediaAttributes(media, prefix, errors) {
|
|
371
|
+
const directionAttrs = ['sendrecv', 'sendonly', 'recvonly', 'inactive'];
|
|
372
|
+
let directionCount = 0;
|
|
373
|
+
const rtpmapPayloadTypes = new Set();
|
|
374
|
+
const rtpmapPayloadTypeDuplicates = new Set();
|
|
375
|
+
const fmtpFormats = new Set();
|
|
376
|
+
const fmtpFormatDuplicates = new Set();
|
|
377
|
+
for (const attr of media.attributes) {
|
|
378
|
+
if (attr.kind === 'property' && directionAttrs.includes(attr.name)) {
|
|
379
|
+
directionCount++;
|
|
380
|
+
}
|
|
381
|
+
if (attributes_1.SESSION_ONLY_ATTRIBUTES.includes(attr.name)) {
|
|
382
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: attribute '${attr.name}' is only valid at session level, not media level`, 'RFC8866:6'));
|
|
383
|
+
}
|
|
384
|
+
if (attr.kind === 'value' && attr.name === 'rtpmap') {
|
|
385
|
+
const match = attr.value.match(/^(\d+)\s/);
|
|
386
|
+
if (match) {
|
|
387
|
+
const payloadType = match[1];
|
|
388
|
+
if (rtpmapPayloadTypes.has(payloadType)) {
|
|
389
|
+
rtpmapPayloadTypeDuplicates.add(payloadType);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
rtpmapPayloadTypes.add(payloadType);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (attr.kind === 'value' && attr.name === 'fmtp') {
|
|
397
|
+
const match = attr.value.match(/^(\S+)\s/);
|
|
398
|
+
if (match) {
|
|
399
|
+
const format = match[1];
|
|
400
|
+
if (fmtpFormats.has(format)) {
|
|
401
|
+
fmtpFormatDuplicates.add(format);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
fmtpFormats.add(format);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (attr.kind === 'value' && attr.name === 'ptime') {
|
|
409
|
+
const ptime = parseFloat(attr.value);
|
|
410
|
+
if (isNaN(ptime) || ptime <= 0) {
|
|
411
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.ptime`, attr.value, 'ptime must be a positive number', 'RFC8866:6.4'));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (attr.kind === 'value' && attr.name === 'maxptime') {
|
|
415
|
+
const maxptime = parseFloat(attr.value);
|
|
416
|
+
if (isNaN(maxptime) || maxptime <= 0) {
|
|
417
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.maxptime`, attr.value, 'maxptime must be a positive number', 'RFC8866:6.5'));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (attr.kind === 'value' && attr.name === 'quality') {
|
|
421
|
+
const quality = parseInt(attr.value, 10);
|
|
422
|
+
if (isNaN(quality) || quality < 0 || quality > 10) {
|
|
423
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.quality`, attr.value, 'quality must be 0-10', 'RFC8866:6.14'));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (attr.kind === 'value' && attr.name === 'framerate') {
|
|
427
|
+
const framerate = parseFloat(attr.value);
|
|
428
|
+
if (isNaN(framerate) || framerate <= 0) {
|
|
429
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.framerate`, attr.value, 'framerate must be a positive number', 'RFC8866:6.13'));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (directionCount > 1) {
|
|
434
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: only one direction attribute (sendrecv/sendonly/recvonly/inactive) allowed`, 'RFC8866:6.7'));
|
|
435
|
+
}
|
|
436
|
+
for (const pt of rtpmapPayloadTypeDuplicates) {
|
|
437
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: duplicate rtpmap for payload type ${pt} (at most one rtpmap per format allowed)`, 'RFC8866:6.6'));
|
|
438
|
+
}
|
|
439
|
+
for (const fmt of fmtpFormatDuplicates) {
|
|
440
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: duplicate fmtp for format ${fmt} (at most one fmtp per format allowed)`, 'RFC8866:6.15'));
|
|
441
|
+
}
|
|
442
|
+
const formatStrings = media.media.formats.map((f) => f);
|
|
443
|
+
for (const pt of rtpmapPayloadTypes) {
|
|
444
|
+
if (!formatStrings.includes(pt)) {
|
|
445
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: rtpmap payload type ${pt} not in format list`, 'RFC8866:6.6'));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const fmt of fmtpFormats) {
|
|
449
|
+
if (!formatStrings.includes(fmt) && !rtpmapPayloadTypes.has(fmt)) {
|
|
450
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: fmtp format ${fmt} not in format list`, 'RFC8866:6.15'));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Validate all media descriptions in a session
|
|
456
|
+
*
|
|
457
|
+
* @param mediaDescriptions - Array of media descriptions
|
|
458
|
+
* @param hasSessionConnection - Whether session-level connection exists
|
|
459
|
+
* @returns ValidationResult with any errors found
|
|
460
|
+
*/
|
|
461
|
+
function validateAllMediaDescriptions(mediaDescriptions, hasSessionConnection) {
|
|
462
|
+
const allErrors = [];
|
|
463
|
+
for (let i = 0; i < mediaDescriptions.length; i++) {
|
|
464
|
+
const result = validateMediaDescription(mediaDescriptions[i], i, hasSessionConnection);
|
|
465
|
+
allErrors.push(...result.errors);
|
|
466
|
+
}
|
|
467
|
+
return allErrors.length === 0 ? validResult() : invalidResult(allErrors);
|
|
468
|
+
}
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// Semantic Validation
|
|
471
|
+
// ============================================================================
|
|
472
|
+
/**
|
|
473
|
+
* Validate cross-field semantic constraints
|
|
474
|
+
*
|
|
475
|
+
* @param session - Session description to validate
|
|
476
|
+
* @returns ValidationResult with any errors found
|
|
477
|
+
*/
|
|
478
|
+
function validateSemanticConstraints(session) {
|
|
479
|
+
const errors = [];
|
|
480
|
+
validateDirectionConsistency(session, errors);
|
|
481
|
+
validateBandwidthConsistency(session, errors);
|
|
482
|
+
validateAddressTypeConsistency(session, errors);
|
|
483
|
+
validatePtimeConsistency(session, errors);
|
|
484
|
+
validateTimeConsistency(session, errors);
|
|
485
|
+
return errors.length === 0 ? validResult() : invalidResult(errors);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Validate direction attribute consistency
|
|
489
|
+
*/
|
|
490
|
+
function validateDirectionConsistency(session, errors) {
|
|
491
|
+
const directionAttrs = ['sendrecv', 'sendonly', 'recvonly', 'inactive'];
|
|
492
|
+
let sessionDirection;
|
|
493
|
+
for (const attr of session.attributes) {
|
|
494
|
+
if (attr.kind === 'property' && directionAttrs.includes(attr.name)) {
|
|
495
|
+
sessionDirection = attr.name;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// If session is inactive, we could warn if any media has active direction
|
|
500
|
+
// but RFC allows media to override session-level direction
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Validate bandwidth consistency
|
|
504
|
+
*/
|
|
505
|
+
function validateBandwidthConsistency(session, errors) {
|
|
506
|
+
let sessionCT;
|
|
507
|
+
for (const bw of session.bandwidths) {
|
|
508
|
+
if (bw.type === 'CT') {
|
|
509
|
+
sessionCT = bw.value;
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (sessionCT === undefined) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
let totalAS = 0;
|
|
517
|
+
for (const md of session.mediaDescriptions) {
|
|
518
|
+
for (const bw of md.bandwidths) {
|
|
519
|
+
if (bw.type === 'AS') {
|
|
520
|
+
totalAS += bw.value;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (const bw of session.bandwidths) {
|
|
525
|
+
if (bw.type === 'AS') {
|
|
526
|
+
totalAS += bw.value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (totalAS > sessionCT) {
|
|
530
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Sum of AS bandwidths (${totalAS}) exceeds CT bandwidth (${sessionCT})`, 'RFC8866:5.8'));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Validate address type consistency
|
|
535
|
+
*/
|
|
536
|
+
function validateAddressTypeConsistency(session, errors) {
|
|
537
|
+
// Different address types are allowed per RFC
|
|
538
|
+
// This function is a placeholder for additional consistency checks if needed
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Validate ptime/maxptime relationship
|
|
542
|
+
*/
|
|
543
|
+
function validatePtimeConsistency(session, errors) {
|
|
544
|
+
for (let i = 0; i < session.mediaDescriptions.length; i++) {
|
|
545
|
+
const md = session.mediaDescriptions[i];
|
|
546
|
+
let ptime;
|
|
547
|
+
let maxptime;
|
|
548
|
+
for (const attr of md.attributes) {
|
|
549
|
+
if (attr.kind === 'value') {
|
|
550
|
+
if (attr.name === 'ptime') {
|
|
551
|
+
ptime = parseFloat(attr.value);
|
|
552
|
+
}
|
|
553
|
+
else if (attr.name === 'maxptime') {
|
|
554
|
+
maxptime = parseFloat(attr.value);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (ptime !== undefined && maxptime !== undefined && ptime > maxptime) {
|
|
559
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Media ${i}: ptime (${ptime}) exceeds maxptime (${maxptime})`, 'RFC8866:6.4-6.5'));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Validate time description constraints
|
|
565
|
+
*/
|
|
566
|
+
function validateTimeConsistency(session, errors) {
|
|
567
|
+
for (let i = 0; i < session.timeDescriptions.length; i++) {
|
|
568
|
+
const td = session.timeDescriptions[i];
|
|
569
|
+
if (td.timezone && td.repeats.length === 0) {
|
|
570
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Time description ${i}: timezone adjustment specified without repeat times`, 'RFC8866:5.11'));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|