@tomgiee/tsdp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/src/builder/media-builder.d.ts +221 -0
- package/dist/src/builder/media-builder.d.ts.map +1 -0
- package/dist/src/builder/media-builder.js +385 -0
- package/dist/src/builder/session-builder.d.ts +195 -0
- package/dist/src/builder/session-builder.d.ts.map +1 -0
- package/dist/src/builder/session-builder.js +366 -0
- package/dist/src/index.d.ts +67 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +250 -0
- package/dist/src/parser/attribute-parser.d.ts +100 -0
- package/dist/src/parser/attribute-parser.d.ts.map +1 -0
- package/dist/src/parser/attribute-parser.js +217 -0
- package/dist/src/parser/field-parser.d.ts +124 -0
- package/dist/src/parser/field-parser.d.ts.map +1 -0
- package/dist/src/parser/field-parser.js +335 -0
- package/dist/src/parser/media-parser.d.ts +45 -0
- package/dist/src/parser/media-parser.d.ts.map +1 -0
- package/dist/src/parser/media-parser.js +157 -0
- package/dist/src/parser/primitive-parser.d.ts +138 -0
- package/dist/src/parser/primitive-parser.d.ts.map +1 -0
- package/dist/src/parser/primitive-parser.js +316 -0
- package/dist/src/parser/scanner.d.ts +142 -0
- package/dist/src/parser/scanner.d.ts.map +1 -0
- package/dist/src/parser/scanner.js +284 -0
- package/dist/src/parser/session-parser.d.ts +35 -0
- package/dist/src/parser/session-parser.d.ts.map +1 -0
- package/dist/src/parser/session-parser.js +207 -0
- package/dist/src/parser/time-parser.d.ts +74 -0
- package/dist/src/parser/time-parser.d.ts.map +1 -0
- package/dist/src/parser/time-parser.js +168 -0
- package/dist/src/serializer/attribute-serializer.d.ts +18 -0
- package/dist/src/serializer/attribute-serializer.d.ts.map +1 -0
- package/dist/src/serializer/attribute-serializer.js +34 -0
- package/dist/src/serializer/field-serializer.d.ts +112 -0
- package/dist/src/serializer/field-serializer.d.ts.map +1 -0
- package/dist/src/serializer/field-serializer.js +212 -0
- package/dist/src/serializer/media-serializer.d.ts +31 -0
- package/dist/src/serializer/media-serializer.d.ts.map +1 -0
- package/dist/src/serializer/media-serializer.js +83 -0
- package/dist/src/serializer/session-serializer.d.ts +29 -0
- package/dist/src/serializer/session-serializer.d.ts.map +1 -0
- package/dist/src/serializer/session-serializer.js +99 -0
- package/dist/src/serializer/time-serializer.d.ts +46 -0
- package/dist/src/serializer/time-serializer.d.ts.map +1 -0
- package/dist/src/serializer/time-serializer.js +86 -0
- package/dist/src/types/attributes.d.ts +318 -0
- package/dist/src/types/attributes.d.ts.map +1 -0
- package/dist/src/types/attributes.js +225 -0
- package/dist/src/types/errors.d.ts +129 -0
- package/dist/src/types/errors.d.ts.map +1 -0
- package/dist/src/types/errors.js +186 -0
- package/dist/src/types/fields.d.ts +100 -0
- package/dist/src/types/fields.d.ts.map +1 -0
- package/dist/src/types/fields.js +48 -0
- package/dist/src/types/media.d.ts +148 -0
- package/dist/src/types/media.d.ts.map +1 -0
- package/dist/src/types/media.js +137 -0
- package/dist/src/types/network.d.ts +136 -0
- package/dist/src/types/network.d.ts.map +1 -0
- package/dist/src/types/network.js +130 -0
- package/dist/src/types/primitives.d.ts +193 -0
- package/dist/src/types/primitives.d.ts.map +1 -0
- package/dist/src/types/primitives.js +195 -0
- package/dist/src/types/session.d.ts +122 -0
- package/dist/src/types/session.d.ts.map +1 -0
- package/dist/src/types/session.js +81 -0
- package/dist/src/types/time.d.ts +129 -0
- package/dist/src/types/time.d.ts.map +1 -0
- package/dist/src/types/time.js +84 -0
- package/dist/src/utils/address-parser.d.ts +100 -0
- package/dist/src/utils/address-parser.d.ts.map +1 -0
- package/dist/src/utils/address-parser.js +338 -0
- package/dist/src/utils/format-validators.d.ts +77 -0
- package/dist/src/utils/format-validators.d.ts.map +1 -0
- package/dist/src/utils/format-validators.js +504 -0
- package/dist/src/utils/line-reader.d.ts +84 -0
- package/dist/src/utils/line-reader.d.ts.map +1 -0
- package/dist/src/utils/line-reader.js +169 -0
- package/dist/src/utils/time-converter.d.ts +99 -0
- package/dist/src/utils/time-converter.d.ts.map +1 -0
- package/dist/src/utils/time-converter.js +195 -0
- package/dist/src/validator/media-validator.d.ts +27 -0
- package/dist/src/validator/media-validator.d.ts.map +1 -0
- package/dist/src/validator/media-validator.js +241 -0
- package/dist/src/validator/semantic-validator.d.ts +47 -0
- package/dist/src/validator/semantic-validator.d.ts.map +1 -0
- package/dist/src/validator/semantic-validator.js +207 -0
- package/dist/src/validator/session-validator.d.ts +36 -0
- package/dist/src/validator/session-validator.d.ts.map +1 -0
- package/dist/src/validator/session-validator.js +280 -0
- package/dist/tests/integration/round-trip.test.d.ts +5 -0
- package/dist/tests/integration/round-trip.test.d.ts.map +1 -0
- package/dist/tests/integration/round-trip.test.js +320 -0
- package/dist/tests/integration/voip-examples.test.d.ts +5 -0
- package/dist/tests/integration/voip-examples.test.d.ts.map +1 -0
- package/dist/tests/integration/voip-examples.test.js +361 -0
- package/dist/tests/unit/builder/media-builder.test.d.ts +5 -0
- package/dist/tests/unit/builder/media-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/builder/media-builder.test.js +524 -0
- package/dist/tests/unit/builder/session-builder.test.d.ts +5 -0
- package/dist/tests/unit/builder/session-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/builder/session-builder.test.js +367 -0
- package/dist/tests/unit/parser/attribute-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/attribute-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/attribute-parser.test.js +319 -0
- package/dist/tests/unit/parser/field-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/field-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/field-parser.test.js +355 -0
- package/dist/tests/unit/parser/media-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/media-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/media-parser.test.js +241 -0
- package/dist/tests/unit/parser/primitive-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/primitive-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/primitive-parser.test.js +261 -0
- package/dist/tests/unit/parser/scanner.test.d.ts +5 -0
- package/dist/tests/unit/parser/scanner.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/scanner.test.js +241 -0
- package/dist/tests/unit/parser/session-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/session-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/session-parser.test.js +346 -0
- package/dist/tests/unit/parser/time-parser.test.d.ts +5 -0
- package/dist/tests/unit/parser/time-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/parser/time-parser.test.js +173 -0
- package/dist/tests/unit/serializer/attribute-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/attribute-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/attribute-serializer.test.js +78 -0
- package/dist/tests/unit/serializer/field-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/field-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/field-serializer.test.js +159 -0
- package/dist/tests/unit/serializer/media-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/media-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/media-serializer.test.js +155 -0
- package/dist/tests/unit/serializer/session-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/session-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/session-serializer.test.js +317 -0
- package/dist/tests/unit/serializer/time-serializer.test.d.ts +5 -0
- package/dist/tests/unit/serializer/time-serializer.test.d.ts.map +1 -0
- package/dist/tests/unit/serializer/time-serializer.test.js +115 -0
- package/dist/tests/unit/types/errors.test.d.ts +5 -0
- package/dist/tests/unit/types/errors.test.d.ts.map +1 -0
- package/dist/tests/unit/types/errors.test.js +127 -0
- package/dist/tests/unit/types/network.test.d.ts +5 -0
- package/dist/tests/unit/types/network.test.d.ts.map +1 -0
- package/dist/tests/unit/types/network.test.js +132 -0
- package/dist/tests/unit/types/primitives.test.d.ts +5 -0
- package/dist/tests/unit/types/primitives.test.d.ts.map +1 -0
- package/dist/tests/unit/types/primitives.test.js +108 -0
- package/dist/tests/unit/utils/address-parser.test.d.ts +5 -0
- package/dist/tests/unit/utils/address-parser.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/address-parser.test.js +203 -0
- package/dist/tests/unit/utils/format-validators.test.d.ts +5 -0
- package/dist/tests/unit/utils/format-validators.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/format-validators.test.js +224 -0
- package/dist/tests/unit/utils/line-reader.test.d.ts +5 -0
- package/dist/tests/unit/utils/line-reader.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/line-reader.test.js +157 -0
- package/dist/tests/unit/utils/time-converter.test.d.ts +5 -0
- package/dist/tests/unit/utils/time-converter.test.d.ts.map +1 -0
- package/dist/tests/unit/utils/time-converter.test.js +190 -0
- package/dist/tests/unit/validator/media-validator.test.d.ts +5 -0
- package/dist/tests/unit/validator/media-validator.test.d.ts.map +1 -0
- package/dist/tests/unit/validator/media-validator.test.js +313 -0
- package/dist/tests/unit/validator/semantic-validator.test.d.ts +5 -0
- package/dist/tests/unit/validator/semantic-validator.test.d.ts.map +1 -0
- package/dist/tests/unit/validator/semantic-validator.test.js +262 -0
- package/dist/tests/unit/validator/session-validator.test.d.ts +5 -0
- package/dist/tests/unit/validator/session-validator.test.d.ts.map +1 -0
- package/dist/tests/unit/validator/session-validator.test.js +447 -0
- package/package.json +50 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Media validators for SDP (RFC 8866)
|
|
4
|
+
*
|
|
5
|
+
* Validates media descriptions and media-level constraints per RFC 8866.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.validateMediaDescription = validateMediaDescription;
|
|
9
|
+
exports.validateAllMediaDescriptions = validateAllMediaDescriptions;
|
|
10
|
+
const errors_1 = require("../types/errors");
|
|
11
|
+
const session_validator_1 = require("./session-validator");
|
|
12
|
+
const attributes_1 = require("../types/attributes");
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Media Description Validator
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Validate a media description
|
|
18
|
+
*
|
|
19
|
+
* Checks RFC 8866 Section 5.14 (m=) and media-level field requirements.
|
|
20
|
+
*
|
|
21
|
+
* @param media - Media description to validate
|
|
22
|
+
* @param index - Index of media description (for error messages)
|
|
23
|
+
* @param hasSessionConnection - Whether session-level connection exists
|
|
24
|
+
* @returns ValidationResult with any errors found
|
|
25
|
+
*/
|
|
26
|
+
function validateMediaDescription(media, index, hasSessionConnection) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const prefix = `mediaDescriptions[${index}]`;
|
|
29
|
+
// 1. Validate media field (RFC 8866 Section 5.14)
|
|
30
|
+
validateMediaField(media, prefix, errors);
|
|
31
|
+
// 2. Validate port (RFC 8866 Section 5.14)
|
|
32
|
+
validatePort(media, prefix, errors);
|
|
33
|
+
// 3. Validate formats (RFC 8866 Section 5.14)
|
|
34
|
+
validateFormats(media, prefix, errors);
|
|
35
|
+
// 4. Validate connection coverage (RFC 8866 Section 5.7)
|
|
36
|
+
if (!hasSessionConnection) {
|
|
37
|
+
validateMediaConnection(media, prefix, errors);
|
|
38
|
+
}
|
|
39
|
+
// 5. Validate bandwidths (RFC 8866 Section 5.8)
|
|
40
|
+
validateMediaBandwidths(media, prefix, errors);
|
|
41
|
+
// 6. Validate attributes (RFC 8866 Section 6)
|
|
42
|
+
validateMediaAttributes(media, prefix, errors);
|
|
43
|
+
return errors.length === 0 ? (0, session_validator_1.validResult)() : (0, session_validator_1.invalidResult)(errors);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate media field
|
|
47
|
+
*
|
|
48
|
+
* RFC 8866 Section 5.14: Media type and protocol are required
|
|
49
|
+
*/
|
|
50
|
+
function validateMediaField(media, prefix, errors) {
|
|
51
|
+
// Validate media type
|
|
52
|
+
if (!media.media.type || media.media.type.length === 0) {
|
|
53
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.type`, media.media.type, 'Media type cannot be empty', 'RFC8866:5.14'));
|
|
54
|
+
}
|
|
55
|
+
// Validate protocol
|
|
56
|
+
if (!media.media.proto || media.media.proto.length === 0) {
|
|
57
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.proto`, media.media.proto, 'Protocol cannot be empty', 'RFC8866:5.14'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate port
|
|
62
|
+
*
|
|
63
|
+
* RFC 8866 Section 5.14: Port must be 0-65535
|
|
64
|
+
*/
|
|
65
|
+
function validatePort(media, prefix, errors) {
|
|
66
|
+
const port = media.media.port;
|
|
67
|
+
if (port.kind === 'simple') {
|
|
68
|
+
if (port.value < 0 || port.value > 65535) {
|
|
69
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.port`, port.value, 'Port must be 0-65535', 'RFC8866:5.14'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (port.kind === 'range') {
|
|
73
|
+
if (port.base < 0 || port.base > 65535) {
|
|
74
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.port.base`, port.base, 'Port must be 0-65535', 'RFC8866:5.14'));
|
|
75
|
+
}
|
|
76
|
+
if (port.count < 1) {
|
|
77
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.media.port.count`, port.count, 'Port count must be at least 1', 'RFC8866:5.14'));
|
|
78
|
+
}
|
|
79
|
+
// Check that port range doesn't exceed 65535
|
|
80
|
+
if (port.base + port.count - 1 > 65535) {
|
|
81
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: port range ${port.base}/${port.count} exceeds 65535`, 'RFC8866:5.14'));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validate formats
|
|
87
|
+
*
|
|
88
|
+
* RFC 8866 Section 5.14: At least one format is required
|
|
89
|
+
*/
|
|
90
|
+
function validateFormats(media, prefix, errors) {
|
|
91
|
+
if (!media.media.formats || media.media.formats.length === 0) {
|
|
92
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: at least one media format is required`, 'RFC8866:5.14'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate media-level connection
|
|
97
|
+
*
|
|
98
|
+
* RFC 8866 Section 5.7: Connection required if no session-level connection
|
|
99
|
+
*/
|
|
100
|
+
function validateMediaConnection(media, prefix, errors) {
|
|
101
|
+
if (!media.connections || media.connections.length === 0) {
|
|
102
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: connection information required when no session-level connection`, 'RFC8866:5.7'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Validate media-level bandwidths
|
|
107
|
+
*
|
|
108
|
+
* RFC 8866 Section 5.8: Bandwidth values must be non-negative
|
|
109
|
+
*/
|
|
110
|
+
function validateMediaBandwidths(media, prefix, errors) {
|
|
111
|
+
for (let i = 0; i < media.bandwidths.length; i++) {
|
|
112
|
+
const bw = media.bandwidths[i];
|
|
113
|
+
if (bw.value < 0) {
|
|
114
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.bandwidths[${i}].value`, bw.value, 'Bandwidth value must be non-negative', 'RFC8866:5.8'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validate media-level attributes
|
|
120
|
+
*
|
|
121
|
+
* RFC 8866 Section 6: Various attribute constraints including:
|
|
122
|
+
* - Only one direction attribute allowed
|
|
123
|
+
* - Session-only attributes cannot appear at media level
|
|
124
|
+
* - At most one rtpmap per payload type (RFC 8866 Section 6.6)
|
|
125
|
+
* - At most one fmtp per format (RFC 8866 Section 6.15)
|
|
126
|
+
* - ptime, maxptime, quality, framerate value constraints
|
|
127
|
+
*/
|
|
128
|
+
function validateMediaAttributes(media, prefix, errors) {
|
|
129
|
+
const directionAttrs = ['sendrecv', 'sendonly', 'recvonly', 'inactive'];
|
|
130
|
+
let directionCount = 0;
|
|
131
|
+
// Track rtpmap payload types for duplicate detection and format validation
|
|
132
|
+
const rtpmapPayloadTypes = new Set();
|
|
133
|
+
const rtpmapPayloadTypeDuplicates = new Set();
|
|
134
|
+
// Track fmtp formats for duplicate detection and format validation
|
|
135
|
+
const fmtpFormats = new Set();
|
|
136
|
+
const fmtpFormatDuplicates = new Set();
|
|
137
|
+
for (const attr of media.attributes) {
|
|
138
|
+
// Check for multiple direction attributes (only one allowed)
|
|
139
|
+
if (attr.kind === 'property' && directionAttrs.includes(attr.name)) {
|
|
140
|
+
directionCount++;
|
|
141
|
+
}
|
|
142
|
+
// Check for session-only attributes at media level (RFC 8866 Section 6)
|
|
143
|
+
if (attributes_1.SESSION_ONLY_ATTRIBUTES.includes(attr.name)) {
|
|
144
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: attribute '${attr.name}' is only valid at session level, not media level`, 'RFC8866:6'));
|
|
145
|
+
}
|
|
146
|
+
// Check rtpmap payload types (should match format list, at most one per payload type)
|
|
147
|
+
if (attr.kind === 'value' && attr.name === 'rtpmap') {
|
|
148
|
+
const match = attr.value.match(/^(\d+)\s/);
|
|
149
|
+
if (match) {
|
|
150
|
+
const payloadType = match[1];
|
|
151
|
+
if (rtpmapPayloadTypes.has(payloadType)) {
|
|
152
|
+
rtpmapPayloadTypeDuplicates.add(payloadType);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
rtpmapPayloadTypes.add(payloadType);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Check fmtp formats (should have corresponding rtpmap or be in format list, at most one per format)
|
|
160
|
+
if (attr.kind === 'value' && attr.name === 'fmtp') {
|
|
161
|
+
const match = attr.value.match(/^(\S+)\s/);
|
|
162
|
+
if (match) {
|
|
163
|
+
const format = match[1];
|
|
164
|
+
if (fmtpFormats.has(format)) {
|
|
165
|
+
fmtpFormatDuplicates.add(format);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
fmtpFormats.add(format);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Validate ptime is positive
|
|
173
|
+
if (attr.kind === 'value' && attr.name === 'ptime') {
|
|
174
|
+
const ptime = parseFloat(attr.value);
|
|
175
|
+
if (isNaN(ptime) || ptime <= 0) {
|
|
176
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.ptime`, attr.value, 'ptime must be a positive number', 'RFC8866:6.4'));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Validate maxptime is positive
|
|
180
|
+
if (attr.kind === 'value' && attr.name === 'maxptime') {
|
|
181
|
+
const maxptime = parseFloat(attr.value);
|
|
182
|
+
if (isNaN(maxptime) || maxptime <= 0) {
|
|
183
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.maxptime`, attr.value, 'maxptime must be a positive number', 'RFC8866:6.5'));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Validate quality is 0-10
|
|
187
|
+
if (attr.kind === 'value' && attr.name === 'quality') {
|
|
188
|
+
const quality = parseInt(attr.value, 10);
|
|
189
|
+
if (isNaN(quality) || quality < 0 || quality > 10) {
|
|
190
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.quality`, attr.value, 'quality must be 0-10', 'RFC8866:6.14'));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Validate framerate is positive
|
|
194
|
+
if (attr.kind === 'value' && attr.name === 'framerate') {
|
|
195
|
+
const framerate = parseFloat(attr.value);
|
|
196
|
+
if (isNaN(framerate) || framerate <= 0) {
|
|
197
|
+
errors.push(errors_1.ValidationError.invalidValue(`${prefix}.attributes.framerate`, attr.value, 'framerate must be a positive number', 'RFC8866:6.13'));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (directionCount > 1) {
|
|
202
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: only one direction attribute (sendrecv/sendonly/recvonly/inactive) allowed`, 'RFC8866:6.7'));
|
|
203
|
+
}
|
|
204
|
+
// Validate no duplicate rtpmap entries for same payload type (RFC 8866 Section 6.6)
|
|
205
|
+
for (const pt of rtpmapPayloadTypeDuplicates) {
|
|
206
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: duplicate rtpmap for payload type ${pt} (at most one rtpmap per format allowed)`, 'RFC8866:6.6'));
|
|
207
|
+
}
|
|
208
|
+
// Validate no duplicate fmtp entries for same format (RFC 8866 Section 6.15)
|
|
209
|
+
for (const fmt of fmtpFormatDuplicates) {
|
|
210
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: duplicate fmtp for format ${fmt} (at most one fmtp per format allowed)`, 'RFC8866:6.15'));
|
|
211
|
+
}
|
|
212
|
+
// Validate rtpmap payload types are in format list
|
|
213
|
+
// Cast formats to strings for comparison since Format is a branded string
|
|
214
|
+
const formatStrings = media.media.formats.map((f) => f);
|
|
215
|
+
for (const pt of rtpmapPayloadTypes) {
|
|
216
|
+
if (!formatStrings.includes(pt)) {
|
|
217
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: rtpmap payload type ${pt} not in format list`, 'RFC8866:6.6'));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Validate fmtp formats are in format list or have rtpmap
|
|
221
|
+
for (const fmt of fmtpFormats) {
|
|
222
|
+
if (!formatStrings.includes(fmt) && !rtpmapPayloadTypes.has(fmt)) {
|
|
223
|
+
errors.push(errors_1.ValidationError.constraintViolation(`${prefix}: fmtp format ${fmt} not in format list`, 'RFC8866:6.15'));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Validate all media descriptions in a session
|
|
229
|
+
*
|
|
230
|
+
* @param mediaDescriptions - Array of media descriptions
|
|
231
|
+
* @param hasSessionConnection - Whether session-level connection exists
|
|
232
|
+
* @returns ValidationResult with any errors found
|
|
233
|
+
*/
|
|
234
|
+
function validateAllMediaDescriptions(mediaDescriptions, hasSessionConnection) {
|
|
235
|
+
const allErrors = [];
|
|
236
|
+
for (let i = 0; i < mediaDescriptions.length; i++) {
|
|
237
|
+
const result = validateMediaDescription(mediaDescriptions[i], i, hasSessionConnection);
|
|
238
|
+
allErrors.push(...result.errors);
|
|
239
|
+
}
|
|
240
|
+
return allErrors.length === 0 ? (0, session_validator_1.validResult)() : (0, session_validator_1.invalidResult)(allErrors);
|
|
241
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic validators for SDP (RFC 8866)
|
|
3
|
+
*
|
|
4
|
+
* Cross-field semantic validation that checks relationships between fields.
|
|
5
|
+
*/
|
|
6
|
+
import { SessionDescription } from '../types/session';
|
|
7
|
+
import { ValidationResult } from './session-validator';
|
|
8
|
+
/**
|
|
9
|
+
* Options for customizing validation behavior
|
|
10
|
+
*/
|
|
11
|
+
export interface ValidationOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Whether to skip semantic validation (cross-field checks)
|
|
14
|
+
*/
|
|
15
|
+
skipSemanticValidation?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether to allow deprecated k= field without warning
|
|
18
|
+
*/
|
|
19
|
+
allowDeprecatedKeyField?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to require connection at session level
|
|
22
|
+
*/
|
|
23
|
+
requireSessionConnection?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Perform complete semantic validation of a session description
|
|
27
|
+
*
|
|
28
|
+
* This is the main validation entry point that checks:
|
|
29
|
+
* 1. Session-level validation
|
|
30
|
+
* 2. Media-level validation
|
|
31
|
+
* 3. Cross-field semantic constraints
|
|
32
|
+
*
|
|
33
|
+
* @param session - Session description to validate
|
|
34
|
+
* @param options - Optional validation options
|
|
35
|
+
* @returns ValidationResult with all errors found
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateSdp(session: SessionDescription, options?: ValidationOptions): ValidationResult;
|
|
38
|
+
/**
|
|
39
|
+
* Validate cross-field semantic constraints
|
|
40
|
+
*
|
|
41
|
+
* Checks relationships between different parts of the SDP.
|
|
42
|
+
*
|
|
43
|
+
* @param session - Session description to validate
|
|
44
|
+
* @returns ValidationResult with any errors found
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateSemanticConstraints(session: SessionDescription): ValidationResult;
|
|
47
|
+
//# sourceMappingURL=semantic-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-validator.d.ts","sourceRoot":"","sources":["../../../src/validator/semantic-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EACL,gBAAgB,EAIjB,MAAM,qBAAqB,CAAC;AAO7B;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC;;OAEG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CA0ClB;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,kBAAkB,GAAG,gBAAgB,CAmBzF"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Semantic validators for SDP (RFC 8866)
|
|
4
|
+
*
|
|
5
|
+
* Cross-field semantic validation that checks relationships between fields.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.validateSdp = validateSdp;
|
|
9
|
+
exports.validateSemanticConstraints = validateSemanticConstraints;
|
|
10
|
+
const errors_1 = require("../types/errors");
|
|
11
|
+
const session_validator_1 = require("./session-validator");
|
|
12
|
+
const media_validator_1 = require("./media-validator");
|
|
13
|
+
/**
|
|
14
|
+
* Perform complete semantic validation of a session description
|
|
15
|
+
*
|
|
16
|
+
* This is the main validation entry point that checks:
|
|
17
|
+
* 1. Session-level validation
|
|
18
|
+
* 2. Media-level validation
|
|
19
|
+
* 3. Cross-field semantic constraints
|
|
20
|
+
*
|
|
21
|
+
* @param session - Session description to validate
|
|
22
|
+
* @param options - Optional validation options
|
|
23
|
+
* @returns ValidationResult with all errors found
|
|
24
|
+
*/
|
|
25
|
+
function validateSdp(session, options = {}) {
|
|
26
|
+
const allErrors = [];
|
|
27
|
+
// 1. Session-level validation
|
|
28
|
+
const sessionResult = (0, session_validator_1.validateSessionDescription)(session);
|
|
29
|
+
allErrors.push(...sessionResult.errors);
|
|
30
|
+
// 2. Check for deprecated key field
|
|
31
|
+
if (!options.allowDeprecatedKeyField && session.key !== undefined) {
|
|
32
|
+
allErrors.push(errors_1.ValidationError.constraintViolation('The k= field is deprecated per RFC 8866', 'RFC8866:5.12'));
|
|
33
|
+
}
|
|
34
|
+
// 3. Require session-level connection if specified
|
|
35
|
+
if (options.requireSessionConnection && session.connection === undefined) {
|
|
36
|
+
allErrors.push(errors_1.ValidationError.constraintViolation('Session-level connection information is required', 'RFC8866:5.7'));
|
|
37
|
+
}
|
|
38
|
+
// 4. Media-level validation
|
|
39
|
+
const hasSessionConnection = session.connection !== undefined;
|
|
40
|
+
const mediaResult = (0, media_validator_1.validateAllMediaDescriptions)(session.mediaDescriptions, hasSessionConnection);
|
|
41
|
+
allErrors.push(...mediaResult.errors);
|
|
42
|
+
// 5. Cross-field semantic validation (if not skipped)
|
|
43
|
+
if (!options.skipSemanticValidation) {
|
|
44
|
+
const semanticResult = validateSemanticConstraints(session);
|
|
45
|
+
allErrors.push(...semanticResult.errors);
|
|
46
|
+
}
|
|
47
|
+
return allErrors.length === 0 ? (0, session_validator_1.validResult)() : (0, session_validator_1.invalidResult)(allErrors);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validate cross-field semantic constraints
|
|
51
|
+
*
|
|
52
|
+
* Checks relationships between different parts of the SDP.
|
|
53
|
+
*
|
|
54
|
+
* @param session - Session description to validate
|
|
55
|
+
* @returns ValidationResult with any errors found
|
|
56
|
+
*/
|
|
57
|
+
function validateSemanticConstraints(session) {
|
|
58
|
+
const errors = [];
|
|
59
|
+
// 1. Validate session vs media direction consistency
|
|
60
|
+
validateDirectionConsistency(session, errors);
|
|
61
|
+
// 2. Validate bandwidth constraints
|
|
62
|
+
validateBandwidthConsistency(session, errors);
|
|
63
|
+
// 3. Validate address type consistency
|
|
64
|
+
validateAddressTypeConsistency(session, errors);
|
|
65
|
+
// 4. Validate ptime/maxptime relationship
|
|
66
|
+
validatePtimeConsistency(session, errors);
|
|
67
|
+
// 5. Validate time description constraints
|
|
68
|
+
validateTimeConsistency(session, errors);
|
|
69
|
+
return errors.length === 0 ? (0, session_validator_1.validResult)() : (0, session_validator_1.invalidResult)(errors);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate direction attribute consistency
|
|
73
|
+
*
|
|
74
|
+
* If session has a direction attribute, media can override but should be compatible.
|
|
75
|
+
*/
|
|
76
|
+
function validateDirectionConsistency(session, errors) {
|
|
77
|
+
const directionAttrs = ['sendrecv', 'sendonly', 'recvonly', 'inactive'];
|
|
78
|
+
// Find session-level direction
|
|
79
|
+
let sessionDirection;
|
|
80
|
+
for (const attr of session.attributes) {
|
|
81
|
+
if (attr.kind === 'property' && directionAttrs.includes(attr.name)) {
|
|
82
|
+
sessionDirection = attr.name;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// If session is inactive, warn if any media has active direction
|
|
87
|
+
if (sessionDirection === 'inactive') {
|
|
88
|
+
for (let i = 0; i < session.mediaDescriptions.length; i++) {
|
|
89
|
+
const md = session.mediaDescriptions[i];
|
|
90
|
+
for (const attr of md.attributes) {
|
|
91
|
+
if (attr.kind === 'property' &&
|
|
92
|
+
(attr.name === 'sendrecv' || attr.name === 'sendonly' || attr.name === 'recvonly')) {
|
|
93
|
+
// This is a warning, not an error - media can override
|
|
94
|
+
// But we'll note it for completeness
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate bandwidth consistency
|
|
102
|
+
*
|
|
103
|
+
* CT (Conference Total) should be >= sum of AS (Application Specific) for all media.
|
|
104
|
+
*/
|
|
105
|
+
function validateBandwidthConsistency(session, errors) {
|
|
106
|
+
// Find session-level CT bandwidth
|
|
107
|
+
let sessionCT;
|
|
108
|
+
for (const bw of session.bandwidths) {
|
|
109
|
+
if (bw.type === 'CT') {
|
|
110
|
+
sessionCT = bw.value;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (sessionCT === undefined) {
|
|
115
|
+
return; // No CT to validate against
|
|
116
|
+
}
|
|
117
|
+
// Sum all AS bandwidths from media
|
|
118
|
+
let totalAS = 0;
|
|
119
|
+
for (const md of session.mediaDescriptions) {
|
|
120
|
+
for (const bw of md.bandwidths) {
|
|
121
|
+
if (bw.type === 'AS') {
|
|
122
|
+
totalAS += bw.value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Also add session-level AS
|
|
127
|
+
for (const bw of session.bandwidths) {
|
|
128
|
+
if (bw.type === 'AS') {
|
|
129
|
+
totalAS += bw.value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (totalAS > sessionCT) {
|
|
133
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Sum of AS bandwidths (${totalAS}) exceeds CT bandwidth (${sessionCT})`, 'RFC8866:5.8'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Validate address type consistency
|
|
138
|
+
*
|
|
139
|
+
* Origin address type should be compatible with connection address types.
|
|
140
|
+
*/
|
|
141
|
+
function validateAddressTypeConsistency(session, errors) {
|
|
142
|
+
const originAddrType = session.origin.addrType;
|
|
143
|
+
// Check session-level connection
|
|
144
|
+
if (session.connection) {
|
|
145
|
+
const connAddrType = session.connection.addrType;
|
|
146
|
+
// Different address types are allowed, but we could warn
|
|
147
|
+
// For now, we don't enforce strict consistency
|
|
148
|
+
}
|
|
149
|
+
// Check media-level connections
|
|
150
|
+
for (let i = 0; i < session.mediaDescriptions.length; i++) {
|
|
151
|
+
const md = session.mediaDescriptions[i];
|
|
152
|
+
for (let j = 0; j < md.connections.length; j++) {
|
|
153
|
+
const conn = md.connections[j];
|
|
154
|
+
// Different address types are allowed per RFC
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Validate ptime/maxptime relationship
|
|
160
|
+
*
|
|
161
|
+
* If both ptime and maxptime are specified, ptime should be <= maxptime.
|
|
162
|
+
*
|
|
163
|
+
* RFC 8866 Sections 6.4-6.5: Both values use non-zero-int-or-real,
|
|
164
|
+
* meaning they can be decimal values.
|
|
165
|
+
*/
|
|
166
|
+
function validatePtimeConsistency(session, errors) {
|
|
167
|
+
for (let i = 0; i < session.mediaDescriptions.length; i++) {
|
|
168
|
+
const md = session.mediaDescriptions[i];
|
|
169
|
+
let ptime;
|
|
170
|
+
let maxptime;
|
|
171
|
+
for (const attr of md.attributes) {
|
|
172
|
+
if (attr.kind === 'value') {
|
|
173
|
+
if (attr.name === 'ptime') {
|
|
174
|
+
ptime = parseFloat(attr.value);
|
|
175
|
+
}
|
|
176
|
+
else if (attr.name === 'maxptime') {
|
|
177
|
+
maxptime = parseFloat(attr.value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (ptime !== undefined && maxptime !== undefined && ptime > maxptime) {
|
|
182
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Media ${i}: ptime (${ptime}) exceeds maxptime (${maxptime})`, 'RFC8866:6.4-6.5'));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Validate time description constraints
|
|
188
|
+
*
|
|
189
|
+
* Check that repeat times and timezone adjustments are consistent.
|
|
190
|
+
*/
|
|
191
|
+
function validateTimeConsistency(session, errors) {
|
|
192
|
+
for (let i = 0; i < session.timeDescriptions.length; i++) {
|
|
193
|
+
const td = session.timeDescriptions[i];
|
|
194
|
+
// Repeat times only make sense for bounded sessions
|
|
195
|
+
if (td.repeats.length > 0) {
|
|
196
|
+
const { startTime, stopTime } = td.timing;
|
|
197
|
+
if (startTime === 0 && stopTime === 0) {
|
|
198
|
+
// Repeat with unbounded session - technically valid but unusual
|
|
199
|
+
// Could add a warning here
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Timezone adjustments only make sense for bounded sessions with repeats
|
|
203
|
+
if (td.timezone && td.repeats.length === 0) {
|
|
204
|
+
errors.push(errors_1.ValidationError.constraintViolation(`Time description ${i}: timezone adjustment specified without repeat times`, 'RFC8866:5.11'));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session validators for SDP (RFC 8866)
|
|
3
|
+
*
|
|
4
|
+
* Validates session-level fields and constraints per RFC 8866.
|
|
5
|
+
*/
|
|
6
|
+
import { SessionDescription } from '../types/session';
|
|
7
|
+
import { ValidationError } from '../types/errors';
|
|
8
|
+
/**
|
|
9
|
+
* Result of validation - either valid or contains errors
|
|
10
|
+
*/
|
|
11
|
+
export interface ValidationResult {
|
|
12
|
+
readonly valid: boolean;
|
|
13
|
+
readonly errors: readonly ValidationError[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a valid result
|
|
17
|
+
*/
|
|
18
|
+
export declare function validResult(): ValidationResult;
|
|
19
|
+
/**
|
|
20
|
+
* Create an invalid result with errors
|
|
21
|
+
*/
|
|
22
|
+
export declare function invalidResult(errors: ValidationError[]): ValidationResult;
|
|
23
|
+
/**
|
|
24
|
+
* Validate a complete session description
|
|
25
|
+
*
|
|
26
|
+
* Checks RFC 8866 Section 5 requirements:
|
|
27
|
+
* - Required fields are present (v, o, s, t)
|
|
28
|
+
* - Version is 0
|
|
29
|
+
* - At least one time description
|
|
30
|
+
* - Connection coverage (session or all media)
|
|
31
|
+
*
|
|
32
|
+
* @param session - Session description to validate
|
|
33
|
+
* @returns ValidationResult with any errors found
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateSessionDescription(session: SessionDescription): ValidationResult;
|
|
36
|
+
//# sourceMappingURL=session-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-validator.d.ts","sourceRoot":"","sources":["../../../src/validator/session-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AASlD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;CAC7C;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,CAE9C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,gBAAgB,CAEzE;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,kBAAkB,GAAG,gBAAgB,CAkCxF"}
|