From 0fd6da1542ae83a4907948f75e7e7ed5e3d9f1d5 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 1 Sep 2026 21:37:19 +0200 Subject: [PATCH 1/2] Separate a receipt's fields on any whitespace, not only a space Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Csos3Vfm66NrAywQUe3e4V --- src/dlr.ts | 9 +++++---- test/dlr.test.ts | 13 ++++++++++--- todo.md | 5 ----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/dlr.ts b/src/dlr.ts index adb2829..6931594 100644 --- a/src/dlr.ts +++ b/src/dlr.ts @@ -56,17 +56,18 @@ export type Dlr = { statusMsg: MessageState; }; -const field = (name: string) => new RegExp(`\\b${name}:([^ ]*)`, 'i'); +const field = (name: string) => new RegExp(`\\b${name}:(\\S*)`, 'i'); const patterns = { dlvrd: field('dlvrd'), - doneDate: /\bdone date:([^ ]*)/i, + doneDate: field('done date'), err: field('err'), id: field('id'), stat: field('stat'), sub: field('sub'), - submitDate: /\bsubmit date:([^ ]*)/i, - text: /\btext:(.*)$/i, + submitDate: field('submit date'), + // The last field, and the only one that may hold a space: it carries the message's own start. + text: /\btext:([^\r\n]*)/i, }; function toNumber(value: string | undefined): number | undefined { diff --git a/test/dlr.test.ts b/test/dlr.test.ts index 2dc499c..73e0b2a 100644 --- a/test/dlr.test.ts +++ b/test/dlr.test.ts @@ -5,7 +5,7 @@ import { dlrFromPdu, parseReceipt, receiptCodes } from '../src/dlr.ts'; import { objToPdu, pduToObj } from '../src/pdu.ts'; import type { PduObject, TlvInput } from '../src/pdu.ts'; -const receiptText = 'id:0195f0c7 sub:001 dlvrd:001 submit date:2508251430 done date:2508251431 stat:DELIVRD err:000 text:hello'; +const receiptText = 'id:0195f0c7 sub:001 dlvrd:001 submit date:2508251430 done date:2508251431 stat:DELIVRD err:000 text:hello there'; function deliverSm( message: Buffer | string, @@ -44,7 +44,14 @@ describe('parseReceipt()', () => { assert.equal(receipt.doneDate, '2508251431'); assert.equal(receipt.stat, 'DELIVRD'); assert.equal(receipt.err, '000'); - assert.equal(receipt.text, 'hello'); + assert.equal(receipt.text, 'hello there'); + }); + + test('takes any whitespace between the fields, not only a space', () => { + const wrapped = 'id:0195f0c7\r\nsub:001\ndlvrd:001\tsubmit date:2508251430\r\ndone date:2508251431' + + '\r\nstat:DELIVRD\r\nerr:000\r\ntext:hello there\r\n'; + + assert.deepEqual(parseReceipt(wrapped), parseReceipt(receiptText)); }); test('does not confuse "done date" with "submit date"', () => { @@ -204,7 +211,7 @@ describe('dlrFromPdu()', () => { assert.ok(dlr?.receipt); assert.equal(dlr.receipt.sub, 1); - assert.equal(dlr.receipt.text, 'hello'); + assert.equal(dlr.receipt.text, 'hello there'); }); test('reads the id in the notation the peer writes receipts in', () => { diff --git a/todo.md b/todo.md index 1f1cacb..8353602 100644 --- a/todo.md +++ b/todo.md @@ -170,11 +170,6 @@ session message is a change to every call site. - [ ] **Coverage reporting.** `node --test --experimental-test-coverage` works today; nothing publishes the numbers. -- [ ] **A receipt whose fields are separated by anything but a space reads as one field.** - `parseReceipt()` takes `id:` as everything up to the next space, so a peer writing CRLF - between fields yields an id of `1a2b\r\nstat:DELIVRD` — one nothing correlates and no - notation can read. Every field pattern has the same shape. Raised by review, 2026-08-30. - - [ ] **An `onReceipt` hook.** Receipt text is only loosely specified and operators disagree on it, but `dlrFromPdu()` is wired into `IncomingRequests` with no seam of its own: an application facing a format we do not parse has to take the whole PDU on `onRequest` and reimplement the From f10c8e58cb931fbf97d1a85d46e05a8b2b33f857 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 1 Sep 2026 21:48:07 +0200 Subject: [PATCH 2/2] Read a valueless count as unstated, and pin a CRLF receipt end to end Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Csos3Vfm66NrAywQUe3e4V --- src/dlr.ts | 5 +++-- test/dlr.test.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dlr.ts b/src/dlr.ts index 6931594..6b90bef 100644 --- a/src/dlr.ts +++ b/src/dlr.ts @@ -66,12 +66,13 @@ const patterns = { stat: field('stat'), sub: field('sub'), submitDate: field('submit date'), - // The last field, and the only one that may hold a space: it carries the message's own start. + // The one field that may hold a space, carrying the message's own start, so it ends at its line. text: /\btext:([^\r\n]*)/i, }; function toNumber(value: string | undefined): number | undefined { - if (value === undefined) return undefined; + // Number('') is 0, which would report a receipt that stated no count as one that stated none sent. + if (value === undefined || value === '') return undefined; const parsed = Number(value); diff --git a/test/dlr.test.ts b/test/dlr.test.ts index 73e0b2a..8aef21e 100644 --- a/test/dlr.test.ts +++ b/test/dlr.test.ts @@ -61,7 +61,7 @@ describe('parseReceipt()', () => { }); test('leaves absent fields undefined rather than guessing', () => { - const receipt = parseReceipt('id:abc stat:UNDELIV'); + const receipt = parseReceipt('id:abc sub: stat:UNDELIV'); assert.equal(receipt.id, 'abc'); assert.equal(receipt.stat, 'UNDELIV'); @@ -94,6 +94,7 @@ describe('dlrFromPdu()', () => { assert.equal(dlr.statusId, 2); assert.equal(dlr.errorCode, '000'); assert.equal(dlr.doneDate?.toISOString(), '2025-08-25T14:31:00.000Z'); + assert.equal(dlrFromPdu(deliverSm('id:beef-1\r\nstat:DELIVRD'))?.smsId, 'beef-1'); }); test('maps every spec status code back to its message state and id', () => {