Commit 74bede6 for jssip.net

commit 74bede62efb1ec1c750e2c72afb3b176e1e82d17
Author: José Luis Millán <jmillan@aliax.net>
Date:   Wed Oct 21 11:03:11 2026 +0200

    TS: Parser (#989)

diff --git a/src/Grammar.d.ts b/src/Grammar.d.ts
index 6f69b8c..eab4429 100644
--- a/src/Grammar.d.ts
+++ b/src/Grammar.d.ts
@@ -1,4 +1,5 @@
 import NameAddrHeader = require('./NameAddrHeader');
+import URI = require('./URI');

 /**
  * The result of `parse()`, whose shape depends on the start rule used.
@@ -75,6 +76,32 @@ export type EventHeader = {
  */
 export type ReferToHeader = NameAddrHeader;

+/**
+ * Result of the `Request_Response` start rule when it matches a `Status_Line`.
+ */
+export type StatusLine = {
+	status_code: number;
+	reason_phrase: string;
+};
+
+/**
+ * Result of the `Request_Response` start rule when it matches a `Request_Line`.
+ *
+ * NOTE: `uri` is only set when the Request-URI is a SIP URI. `Request_URI` also
+ * accepts an `absoluteURI`, which the grammar does not turn into a `URI`.
+ */
+export type RequestLine = {
+	method: string;
+	uri?: URI;
+};
+
+/**
+ * Result of the `Request_Response` start rule. Which of the two it is can be
+ * told apart with `'status_code' in parsed`, as the grammar only creates the
+ * properties the matched rule sets.
+ */
+export type RequestResponse = StatusLine | RequestLine;
+
 export function parse(input: string, startRule?: string): Grammar;

 // eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/src/Parser.js b/src/Parser.ts
similarity index 69%
rename from src/Parser.js
rename to src/Parser.ts
index d2fc29a..1ac477d 100644
--- a/src/Parser.js
+++ b/src/Parser.ts
@@ -1,35 +1,41 @@
-const { Logger } = require('./Logger');
-const Grammar = require('./Grammar');
-const { IncomingMessageBuilder } = require('./IncomingMessageBuilder');
+import { Logger } from './Logger';
+import * as Grammar from './Grammar';
+import { IncomingMessageBuilder } from './IncomingMessageBuilder';
+import type { IncomingRequest, IncomingResponse, UALike } from './SIPMessage';

 const logger = new Logger('Parser');

 /**
  * Parse SIP Message
  */
-exports.parseMessage = (data, ua) => {
-	let bodyStart;
+export const parseMessage = (
+	data: string,
+	ua: UALike
+): IncomingRequest | IncomingResponse | undefined => {
+	let bodyStart: number;
 	let headerEnd = data.indexOf('\r\n');

 	if (headerEnd === -1) {
 		logger.warn('parseMessage() | no CRLF found, not a SIP message');

-		return;
+		return undefined;
 	}

 	// Parse first line. Check if it is a Request or a Reply.
 	const firstLine = data.substring(0, headerEnd);
-	const parsed = Grammar.parse(firstLine, 'Request_Response');
+	const parsed: Grammar.RequestResponse | -1 = Grammar.parse(
+		firstLine,
+		'Request_Response'
+	);

 	if (parsed === -1) {
 		logger.warn(
 			`parseMessage() | error parsing first line of SIP message: "${firstLine}"`
 		);

-		return;
+		return undefined;
 	}

-	const isResponse = Boolean(parsed.status_code);
 	const builder = new IncomingMessageBuilder();

 	let headerStart = headerEnd + 2;
@@ -37,6 +43,7 @@ exports.parseMessage = (data, ua) => {
 	/* Loop over every line in data. Detect the end of each header and parse
 	 * it or simply add to the headers collection.
 	 */
+
 	while (true) {
 		headerEnd = getHeader(data, headerStart);

@@ -49,7 +56,7 @@ exports.parseMessage = (data, ua) => {
 		else if (headerEnd === -1) {
 			logger.warn('parseMessage() | malformed message');

-			return;
+			return undefined;
 		}

 		const result = parseHeader(builder, data, headerStart, headerEnd);
@@ -57,7 +64,7 @@ exports.parseMessage = (data, ua) => {
 		if (result !== true) {
 			logger.warn('parseMessage() |', result.error);

-			return;
+			return undefined;
 		}

 		headerStart = headerEnd + 2;
@@ -72,27 +79,38 @@ exports.parseMessage = (data, ua) => {
 	if (builder.hasHeader('content-length')) {
 		const contentLength = builder.getHeader('content-length');

-		body = data.substr(bodyStart, contentLength);
+		body = data.substr(bodyStart, Number(contentLength));
 	} else {
 		body = data.substring(bodyStart);
 	}

-	if (isResponse) {
+	if ('status_code' in parsed) {
 		return builder.buildResponse(
 			data,
 			parsed.status_code,
 			parsed.reason_phrase,
 			body
 		);
-	} else {
-		return builder.buildRequest(ua, data, parsed.method, parsed.uri, body);
 	}
+
+	/* RFC 3261 8.2.2.1. The grammar accepts any absoluteURI as Request-URI but
+	 * only turns a SIP one into a `URI`, so anything else cannot be handled.
+	 */
+	if (!parsed.uri) {
+		logger.warn(
+			`parseMessage() | unsupported Request-URI scheme in "${firstLine}"`
+		);
+
+		return undefined;
+	}
+
+	return builder.buildRequest(ua, data, parsed.method, parsed.uri, body);
 };

 /**
  * Extract and parse every header of a SIP message.
  */
-function getHeader(data, headerStart) {
+function getHeader(data: string, headerStart: number): number {
 	// 'start' position of the header.
 	let start = headerStart;
 	// 'end' position of the header.
@@ -128,7 +146,12 @@ function getHeader(data, headerStart) {
 	return end;
 }

-function parseHeader(builder, data, headerStart, headerEnd) {
+function parseHeader(
+	builder: IncomingMessageBuilder,
+	data: string,
+	headerStart: number,
+	headerEnd: number
+): true | { error: string } {
 	const hcolonIndex = data.indexOf(':', headerStart);
 	const headerName = data.substring(headerStart, hcolonIndex).trim();
 	const headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();
diff --git a/src/test/test-parser.ts b/src/test/test-parser.ts
index eee4e5c..d199744 100644
--- a/src/test/test-parser.ts
+++ b/src/test/test-parser.ts
@@ -4,8 +4,8 @@ import * as consts from './include/consts';
 // eslint-disable-next-line @typescript-eslint/no-require-imports
 const JsSIP = require('../JsSIP.js');
 const { URI, NameAddrHeader, Grammar, WebSocketInterface, UA } = JsSIP;
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Parser = require('../Parser.js');
+
+import * as Parser from '../Parser';

 describe('parser', () => {
 	test('parse URI', () => {
@@ -429,14 +429,18 @@ P-Preferred-Identity: "Cullen Jennings" <sip:fluffy@cisco.com>\r\n\r\n';
 		const ua = new UA(config as ConstructorParameters<typeof UA>[0]);
 		const message = Parser.parseMessage(data, ua);

+		if (!message) {
+			throw new Error('parseMessage() did not return a message');
+		}
+
 		expect(message.hasHeader('P-Preferred-Identity')).toBe(true);

 		const pai = message.getHeader('P-Preferred-Identity');
 		const nameAddress = NameAddrHeader.parse(pai);

 		expect(nameAddress instanceof NameAddrHeader).toBeTruthy();
-		expect(nameAddress!.uri.user).toBe('fluffy');
-		expect(nameAddress!.uri.host).toBe('cisco.com');
+		expect(nameAddress.uri.user).toBe('fluffy');
+		expect(nameAddress.uri.host).toBe('cisco.com');

 		expect(message.hasHeader('Privacy')).toBe(true);
 		expect(message.getHeader('Privacy')).toBe('id');