test(ipsec): покрыть формы ret и print в декодере файла RouterOS
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,65 +1,89 @@
|
|||||||
import assert from "node:assert/strict"
|
import assert from "node:assert/strict"
|
||||||
import { extractRosContentsField } from "./mikrotik.js"
|
import { extractRosContentsField } from "./mikrotik.js"
|
||||||
|
|
||||||
/**
|
/** Экранирование строки ответа RouterOS: только кавычка и обратный слэш. */
|
||||||
* Собирает ответ RouterOS на `/file/get` (.proplist=contents) в том виде, в каком его
|
function escapeRosString(contents: Buffer): Buffer {
|
||||||
* отдаёт устройство: single-byte строка, экранируются только кавычка и обратный слэш.
|
|
||||||
*/
|
|
||||||
function rosFileGetResponse(contents: Buffer): Buffer {
|
|
||||||
const escaped: number[] = []
|
const escaped: number[] = []
|
||||||
for (const byte of contents) {
|
for (const byte of contents) {
|
||||||
if (byte === 0x22 || byte === 0x5c) escaped.push(0x5c, byte)
|
if (byte === 0x22 || byte === 0x5c) escaped.push(0x5c, byte)
|
||||||
else escaped.push(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([
|
return Buffer.concat([
|
||||||
Buffer.from('[{".id":"*A","contents":"', "latin1"),
|
Buffer.from('[{".id":"*A","contents":"', "latin1"),
|
||||||
Buffer.from(escaped),
|
escapeRosString(contents),
|
||||||
Buffer.from('"}]', "latin1"),
|
Buffer.from('"}]', "latin1"),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// ASCII-содержимое
|
// get: ASCII-содержимое
|
||||||
const content = Buffer.from("hello p12", "latin1")
|
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")
|
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"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Пустой файл
|
// print: экранированные кавычки, обратный слэш и \u-escape
|
||||||
assert.equal(extractRosContentsField(rosFileGetResponse(Buffer.alloc(0))).length, 0)
|
assert.deepEqual(
|
||||||
|
extractRosContentsField(Buffer.from('[{"contents":"a\\"b\\\\c\\u0044"}]', "latin1")),
|
||||||
|
Buffer.from('a"b\\cD', "latin1"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Управляющие escape-последовательности
|
// Пустой файл — пустой Buffer
|
||||||
const raw = Buffer.from('{"contents":"a\\nb\\tc\\u0041"}', "latin1")
|
assert.equal(extractRosContentsField(rosGetResponse(Buffer.alloc(0))).length, 0)
|
||||||
assert.deepEqual(extractRosContentsField(raw), Buffer.from("a\nb\tcA", "latin1"))
|
assert.equal(extractRosContentsField(rosPrintResponse(Buffer.alloc(0))).length, 0)
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// contents не первый ключ в объекте
|
|
||||||
const raw = Buffer.from('{".id":"*B","name":"x.p12","contents":"DATA"}', "latin1")
|
|
||||||
assert.deepEqual(extractRosContentsField(raw), Buffer.from("DATA", "latin1"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Все 256 байт: single-byte кодировка не должна терять значения 0x80–0xFF и NUL
|
// Все 256 байт: single-byte кодировка не должна терять значения 0x80–0xFF и NUL
|
||||||
const content = Buffer.from(Array.from({ length: 256 }, (_, i) => i))
|
const content = Buffer.from(Array.from({ length: 256 }, (_, i) => i))
|
||||||
const decoded = extractRosContentsField(rosFileGetResponse(content))
|
for (const raw of [rosGetResponse(content), rosPrintResponse(content)]) {
|
||||||
assert.equal(decoded.length, 256)
|
const decoded = extractRosContentsField(raw)
|
||||||
assert.deepEqual(decoded, content)
|
assert.equal(decoded.length, 256)
|
||||||
|
assert.deepEqual(decoded, content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Нет поля contents — понятная ошибка
|
// Нет полей ret/contents — понятная ошибка с превью ответа
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => extractRosContentsField(Buffer.from('{".id":"*A"}', "latin1")),
|
() => extractRosContentsField(Buffer.from('{".id":"*A"}', "latin1")),
|
||||||
/не содержит поле contents/,
|
/не содержит поле ret\/contents/,
|
||||||
|
)
|
||||||
|
assert.throws(
|
||||||
|
() => extractRosContentsField(Buffer.from('{"error":400,"detail":"Bad Request"}', "latin1")),
|
||||||
|
/Bad Request/,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user