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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
const fs = require('fs');
const path = require('path');
const { parseAndPrintData } = require('./index');
const {
  extractFramesFromText,
  extractFramesFromUtf16leBuffer,
  guessTextEncodingFromBuffer,
} = require('./framing');
 
function printUsageAndExit(code) {
  // eslint-disable-next-line no-console
  console.log(`\n用法:\n  node .\\parse_raw.js <file> [--hex|--b64|--bin] [--utf16le|--utf8]\n  node .\\parse_raw.js --demo\n\n说明:\n- <file> 可以是二进制抓包文件,或包含 hex/base64 的文本文件。\n- 默认会尝试自动判断:hex/base64/二进制,以及 utf16le/utf8。\n`);
  process.exit(code);
}
 
function stripNonHex(text) {
  return text
    .replace(/0x/gi, '')
    .replace(/\\x/gi, '')
    .replace(/[^0-9a-fA-F]/g, '');
}
 
function looksLikeHex(text) {
  const cleaned = stripNonHex(text);
  return cleaned.length >= 40 && cleaned.length % 2 === 0;
}
 
function decodeHexText(text) {
  let cleaned = stripNonHex(text);
  if (cleaned.length % 2 !== 0) {
    // eslint-disable-next-line no-console
    console.warn('⚠️ hex 长度不是偶数(可能复制/截断),已丢弃最后 1 个半字节继续尝试解析');
    cleaned = cleaned.slice(0, -1);
  }
  return Buffer.from(cleaned, 'hex');
}
 
function looksLikeBase64(text) {
  const cleaned = text.replace(/\s+/g, '');
  if (cleaned.length < 40) return false;
  if (cleaned.length % 4 !== 0) return false;
  return /^[A-Za-z0-9+/]+={0,2}$/.test(cleaned);
}
 
function decodeBase64Text(text) {
  const cleaned = text.replace(/\s+/g, '');
  return Buffer.from(cleaned, 'base64');
}
 
function autoDecodeFileToBytes(filePath) {
  const buf = fs.readFileSync(filePath);
 
  // Try treat as UTF-8 text containing hex/base64.
  const asText = buf.toString('utf8');
  if (looksLikeHex(asText)) {
    return { bytes: decodeHexText(asText), source: 'hex' };
  }
  if (looksLikeBase64(asText)) {
    return { bytes: decodeBase64Text(asText), source: 'base64' };
  }
 
  return { bytes: buf, source: 'binary' };
}
 
function runParse(bytes, forcedEncoding) {
  const encoding = forcedEncoding || guessTextEncodingFromBuffer(bytes);
 
  if (encoding === 'utf16le') {
    const { frames } = extractFramesFromUtf16leBuffer(bytes);
    if (frames.length === 0) {
      // 兜底:抓包/复制时常见“消息截断”,导致找不到 </ORU_R31>,但前面可能已经包含完整 OBX 段。
      const iconv = require('iconv-lite');
      let work = bytes;
      if (work.length % 2 === 1) work = work.slice(0, work.length - 1);
      const text = iconv.decode(work, 'utf16le').replace(/^\uFEFF/, '');
      const hasRoot = text.includes('<ORU_R31') || text.includes('<ORF_R04');
      const obxBlocks = text.match(/<OBX>[\s\S]*?<\/OBX>/g) || [];
 
      if (hasRoot && obxBlocks.length > 0) {
        // eslint-disable-next-line no-console
        console.log(`⚠️ 未找到完整帧(疑似截断),尝试从片段提取 ${obxBlocks.length} 个 OBX 解析`);
        const wrapped = `<?xml version="1.0" encoding="UTF-8"?>\n<ORU_R31>\n  <ORU_R31.OBSERVATION>\n${obxBlocks.join('\n')}\n  </ORU_R31.OBSERVATION>\n</ORU_R31>`;
        parseAndPrintData(wrapped);
        return;
      }
 
      // eslint-disable-next-line no-console
      console.log('未在 UTF-16LE 流中找到 ORU_R31/ORF_R04 XML 完整帧');
      return;
    }
    // eslint-disable-next-line no-console
    console.log(`找到 ${frames.length} 条 XML 帧 (encoding=utf16le)`);
    frames.forEach((xml, idx) => {
      // eslint-disable-next-line no-console
      console.log(`\n===== Frame #${idx + 1} =====`);
      parseAndPrintData(xml);
    });
    return;
  }
 
  // utf8
  const text = bytes.toString('utf8');
  const { frames } = extractFramesFromText(text);
  if (frames.length === 0) {
    const hasRoot = text.includes('<ORU_R31') || text.includes('<ORF_R04');
    const obxBlocks = text.match(/<OBX>[\s\S]*?<\/OBX>/g) || [];
    if (hasRoot && obxBlocks.length > 0) {
      // eslint-disable-next-line no-console
      console.log(`⚠️ 未找到完整帧(疑似截断),尝试从片段提取 ${obxBlocks.length} 个 OBX 解析`);
      const wrapped = `<?xml version="1.0" encoding="UTF-8"?>\n<ORU_R31>\n  <ORU_R31.OBSERVATION>\n${obxBlocks.join('\n')}\n  </ORU_R31.OBSERVATION>\n</ORU_R31>`;
      parseAndPrintData(wrapped);
      return;
    }
 
    // eslint-disable-next-line no-console
    console.log('未在 UTF-8 文本中找到 ORU_R31/ORF_R04 XML 完整帧');
    return;
  }
  // eslint-disable-next-line no-console
  console.log(`找到 ${frames.length} 条 XML 帧 (encoding=utf8)`);
  frames.forEach((xml, idx) => {
    // eslint-disable-next-line no-console
    console.log(`\n===== Frame #${idx + 1} =====`);
    parseAndPrintData(xml);
  });
}
 
function main() {
  const args = process.argv.slice(2);
  if (args.length === 0) printUsageAndExit(1);
 
  if (args.includes('--help') || args.includes('-h')) printUsageAndExit(0);
 
  if (args.includes('--demo')) {
    const demoXml = `<?xml version="1.0" encoding="UTF-8"?>\n<ORU_R31>\n  <MSH><MSH.18>UNICODE</MSH.18></MSH>\n  <ORU_R31.OBSERVATION>\n    <OBX>\n      <OBX.3><CE.1>7</CE.1><CE.2>Temperature</CE.2></OBX.3>\n      <OBX.5><FN.1>37.500000</FN.1></OBX.5>\n      <OBX.6><CE.1>cel</CE.1></OBX.6>\n    </OBX>\n  </ORU_R31.OBSERVATION>\n</ORU_R31>`;
 
    const iconv = require('iconv-lite');
    const bytes = Buffer.concat([
      iconv.encode(demoXml, 'utf16le'),
      iconv.encode(demoXml, 'utf16le'),
    ]);
 
    runParse(bytes, 'utf16le');
    return;
  }
 
  const fileArg = args.find(a => !a.startsWith('--'));
  if (!fileArg) printUsageAndExit(1);
 
  const filePath = path.resolve(process.cwd(), fileArg);
  if (!fs.existsSync(filePath)) {
    // eslint-disable-next-line no-console
    console.error(`文件不存在: ${filePath}`);
    printUsageAndExit(1);
  }
 
  const forcedEncoding = args.includes('--utf16le') ? 'utf16le' : (args.includes('--utf8') ? 'utf8' : null);
 
  let bytes;
  let source;
  try {
    if (args.includes('--hex')) {
      bytes = decodeHexText(fs.readFileSync(filePath, 'utf8'));
      source = 'hex';
    } else if (args.includes('--b64')) {
      bytes = decodeBase64Text(fs.readFileSync(filePath, 'utf8'));
      source = 'base64';
    } else if (args.includes('--bin')) {
      bytes = fs.readFileSync(filePath);
      source = 'binary';
    } else {
      ({ bytes, source } = autoDecodeFileToBytes(filePath));
    }
  } catch (e) {
    // eslint-disable-next-line no-console
    console.error(`解码失败: ${e && e.message ? e.message : String(e)}`);
    printUsageAndExit(1);
  }
 
  // eslint-disable-next-line no-console
  console.log(`输入: ${filePath} (source=${source}, bytes=${bytes.length})`);
 
  runParse(bytes, forcedEncoding);
}
 
main();