const iconv = require('iconv-lite');
|
|
// ============================================================================
|
// framing.js 负责“分帧”而不是“解析”
|
// - 输入:连续文本/字节流(可能粘包、半包、多帧)
|
// - 输出:完整 XML 帧数组 + 未完成尾部余量
|
// 解析工作交给 index.js 的 parseAndPrintData。
|
// ============================================================================
|
|
const ROOT_SPECS = [
|
{ open: '<ORU_R31', close: '</ORU_R31>' },
|
{ open: '<ORF_R04', close: '</ORF_R04>' },
|
];
|
|
function findNextRoot(text, fromIndex) {
|
// 从当前位置向后找下一个根节点起点(ORU_R31 / ORF_R04),取最早出现者。
|
let best = null;
|
for (const spec of ROOT_SPECS) {
|
const idx = text.indexOf(spec.open, fromIndex);
|
if (idx === -1) continue;
|
if (!best || idx < best.idx) best = { idx, spec };
|
}
|
return best;
|
}
|
|
function maybeIncludeXmlDecl(text, frameStart, searchFloor) {
|
// 若根节点前存在同一帧的 XML 声明 `<?xml ...?>`,则把声明一起纳入帧起点。
|
const idxXml = text.lastIndexOf('<?xml', frameStart);
|
if (idxXml < searchFloor) return frameStart;
|
|
const endDecl = text.indexOf('?>', idxXml);
|
if (endDecl === -1) return frameStart;
|
if (endDecl >= frameStart) return frameStart;
|
|
return idxXml;
|
}
|
|
function extractFramesFromText(text) {
|
// 文本分帧:按根节点和闭合标签切片,找不到闭合则保留余量等待下次补齐。
|
const frames = [];
|
let cursor = 0;
|
|
while (cursor < text.length) {
|
const next = findNextRoot(text, cursor);
|
if (!next) break;
|
|
let start = next.idx;
|
start = maybeIncludeXmlDecl(text, start, cursor);
|
|
const endClose = text.indexOf(next.spec.close, next.idx);
|
if (endClose === -1) {
|
// Incomplete frame: keep from `start` as remainder.
|
return { frames, remainderText: text.slice(start) };
|
}
|
|
const end = endClose + next.spec.close.length;
|
frames.push(text.slice(start, end));
|
cursor = end;
|
}
|
|
return { frames, remainderText: text.slice(cursor) };
|
}
|
|
function extractFramesFromUtf16leBuffer(buffer) {
|
// 字节分帧:先按 UTF-16LE 解码成文本分帧,再把 remainder 映射回字节余量。
|
const frames = [];
|
|
// UTF-16LE requires even bytes; keep a tail byte if needed.
|
let oddTail = Buffer.alloc(0);
|
let work = buffer;
|
if (work.length % 2 === 1) {
|
oddTail = work.slice(work.length - 1);
|
work = work.slice(0, work.length - 1);
|
}
|
|
const text = iconv.decode(work, 'utf16le');
|
const { frames: found, remainderText } = extractFramesFromText(text);
|
frames.push(...found);
|
|
// Convert remainderText length (code units) back to bytes.
|
// This assumes the decoded text maps 1:1 to UTF-16 code units, which is valid here.
|
const consumedChars = text.length - remainderText.length;
|
const consumedBytes = consumedChars * 2;
|
|
const remainderBuffer = Buffer.concat([work.slice(consumedBytes), oddTail]);
|
return { frames, remainderBuffer };
|
}
|
|
function guessTextEncodingFromBuffer(buffer) {
|
// 编码猜测:透析机报文常见 UTF-16LE(奇数字节位置大量 0x00)。
|
if (!buffer || buffer.length < 4) return 'utf16le';
|
|
// Heuristic: UTF-16LE XML often looks like 3C 00 4F 00 ... (lots of zeros on odd bytes)
|
const sampleLen = Math.min(buffer.length, 512);
|
let oddPositions = 0;
|
let oddZeros = 0;
|
for (let i = 1; i < sampleLen; i += 2) {
|
oddPositions++;
|
if (buffer[i] === 0x00) oddZeros++;
|
}
|
const ratio = oddPositions ? oddZeros / oddPositions : 0;
|
return ratio >= 0.25 ? 'utf16le' : 'utf8';
|
}
|
|
module.exports = {
|
extractFramesFromText,
|
extractFramesFromUtf16leBuffer,
|
guessTextEncodingFromBuffer,
|
};
|