chenyc
2026-03-22 d23dc3235324e6bbe62e507eae807435d77dfc6d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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,
};