diff --git a/backend/src/services/ros-file-contents.test.ts b/backend/src/services/ros-file-contents.test.ts index 6376d26..b376481 100644 --- a/backend/src/services/ros-file-contents.test.ts +++ b/backend/src/services/ros-file-contents.test.ts @@ -1,65 +1,89 @@ import assert from "node:assert/strict" import { extractRosContentsField } from "./mikrotik.js" -/** - * Собирает ответ RouterOS на `/file/get` (.proplist=contents) в том виде, в каком его - * отдаёт устройство: single-byte строка, экранируются только кавычка и обратный слэш. - */ -function rosFileGetResponse(contents: Buffer): Buffer { +/** Экранирование строки ответа RouterOS: только кавычка и обратный слэш. */ +function escapeRosString(contents: Buffer): Buffer { const escaped: number[] = [] for (const byte of contents) { if (byte === 0x22 || byte === 0x5c) escaped.push(0x5c, byte) else escaped.push(byte) } + return Buffer.from(escaped) +} + +/** Ответ команды get: `{"ret":"<содержимое>"}` (!done с данными). */ +function rosGetResponse(contents: Buffer): Buffer { + return Buffer.concat([ + Buffer.from('{"ret":"', "latin1"), + escapeRosString(contents), + Buffer.from('"}', "latin1"), + ]) +} + +/** Ответ команды print: `[{".id":"*A","contents":"<содержимое>"}]` (!re-запись). */ +function rosPrintResponse(contents: Buffer): Buffer { return Buffer.concat([ Buffer.from('[{".id":"*A","contents":"', "latin1"), - Buffer.from(escaped), + escapeRosString(contents), Buffer.from('"}]', "latin1"), ]) } { - // ASCII-содержимое + // get: ASCII-содержимое const content = Buffer.from("hello p12", "latin1") - assert.deepEqual(extractRosContentsField(rosFileGetResponse(content)), content) + assert.deepEqual(extractRosContentsField(rosGetResponse(content)), content) } { - // Экранированные кавычки и обратный слэш + // print: ASCII-содержимое (обратная совместимость) + const content = Buffer.from("hello p12", "latin1") + assert.deepEqual(extractRosContentsField(rosPrintResponse(content)), content) +} + +{ + // get: экранированные кавычки, обратный слэш и \u-escape const content = Buffer.from('a"b\\c"d', "latin1") - assert.deepEqual(extractRosContentsField(rosFileGetResponse(content)), content) + assert.deepEqual(extractRosContentsField(rosGetResponse(content)), content) + assert.deepEqual( + extractRosContentsField(Buffer.from('{"ret":"a\\nb\\u0041"}', "latin1")), + Buffer.from("a\nbA", "latin1"), + ) } { - // Пустой файл - assert.equal(extractRosContentsField(rosFileGetResponse(Buffer.alloc(0))).length, 0) + // print: экранированные кавычки, обратный слэш и \u-escape + assert.deepEqual( + extractRosContentsField(Buffer.from('[{"contents":"a\\"b\\\\c\\u0044"}]', "latin1")), + Buffer.from('a"b\\cD', "latin1"), + ) } { - // Управляющие escape-последовательности - const raw = Buffer.from('{"contents":"a\\nb\\tc\\u0041"}', "latin1") - assert.deepEqual(extractRosContentsField(raw), Buffer.from("a\nb\tcA", "latin1")) -} - -{ - // contents не первый ключ в объекте - const raw = Buffer.from('{".id":"*B","name":"x.p12","contents":"DATA"}', "latin1") - assert.deepEqual(extractRosContentsField(raw), Buffer.from("DATA", "latin1")) + // Пустой файл — пустой Buffer + assert.equal(extractRosContentsField(rosGetResponse(Buffer.alloc(0))).length, 0) + assert.equal(extractRosContentsField(rosPrintResponse(Buffer.alloc(0))).length, 0) } { // Все 256 байт: single-byte кодировка не должна терять значения 0x80–0xFF и NUL const content = Buffer.from(Array.from({ length: 256 }, (_, i) => i)) - const decoded = extractRosContentsField(rosFileGetResponse(content)) - assert.equal(decoded.length, 256) - assert.deepEqual(decoded, content) + for (const raw of [rosGetResponse(content), rosPrintResponse(content)]) { + const decoded = extractRosContentsField(raw) + assert.equal(decoded.length, 256) + assert.deepEqual(decoded, content) + } } { - // Нет поля contents — понятная ошибка + // Нет полей ret/contents — понятная ошибка с превью ответа assert.throws( () => extractRosContentsField(Buffer.from('{".id":"*A"}', "latin1")), - /не содержит поле contents/, + /не содержит поле ret\/contents/, + ) + assert.throws( + () => extractRosContentsField(Buffer.from('{"error":400,"detail":"Bad Request"}', "latin1")), + /Bad Request/, ) }