333
schangxiang@126.com
2025-09-19 18966e02fb573c7e2bb0c6426ed792b38b910940
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
187
188
189
190
'use strict';
 
const debug = require('debug')('serialize-json#JSONDecoder');
 
const TOKEN_TRUE = -1;
const TOKEN_FALSE = -2;
const TOKEN_NULL = -3;
const TOKEN_EMPTY_STRING = -4;
const TOKEN_UNDEFINED = -5;
 
const REG_STR_REPLACER = /\+|%2B|%7C|%5E|%25/g;
const DECODER_REPLACER = {
  '+': ' ',
  '%2B': '+',
  '%7C': '|',
  '%5E': '^',
  '%25': '%',
};
const TOKEN_SET = new Set([ '|', '$', '@', '*', '#', ']' ]);
 
class JSONDecoder {
  constructor() {
    this.dictionary = [];
    this.tokens = [];
    this.tokensIndex = 0;
  }
 
  _decodeString(str) {
    // avoid Parent in (sliced string)
    // https://github.com/nodejs/help/issues/711
    // https://stackoverflow.com/questions/31712808/how-to-force-javascript-to-deep-copy-a-string
    const r = str.replace(REG_STR_REPLACER, a => DECODER_REPLACER[a]);
    return (' ' + r).slice(1);
  }
 
  _decodeDate(str) {
    return new Date(this._base36To10(str));
  }
 
  _base36To10(num) {
    return parseInt(num, 36);
  }
 
  _unpack() {
    const token = this.tokens[this.tokensIndex];
    switch (token) {
      case '@': // array
      {
        debug('--> unpack array begin');
        const arr = [];
        const tokensLen = this.tokens.length;
        for (this.tokensIndex++; this.tokensIndex < tokensLen; this.tokensIndex++) {
          const token = this.tokens[this.tokensIndex];
          if (token === ']') {
            debug('--> unpack array end, %j', arr);
            return arr;
          }
          arr.push(this._unpack());
        }
        return arr;
      }
      case '$': // object
      {
        debug('--> unpack plain object begin');
        const obj = {};
        const tokensLen = this.tokens.length;
        for (this.tokensIndex++; this.tokensIndex < tokensLen; this.tokensIndex++) {
          const token = this.tokens[this.tokensIndex];
          if (token === ']') {
            debug('--> unpack plain object end, %j', obj);
            return obj;
          }
          const key = this._unpack();
          this.tokensIndex++;
          obj[key] = this._unpack();
        }
        return obj;
      }
      case '*': // buffer
      {
        debug('--> unpack buffer begin');
        const arr = [];
        const tokensLen = this.tokens.length;
        for (this.tokensIndex++; this.tokensIndex < tokensLen; this.tokensIndex++) {
          const token = this.tokens[this.tokensIndex];
          if (token === ']') {
            debug('--> unpack buffer end, %j', arr);
            return Buffer.from(arr);
          }
          arr.push(this._unpack());
        }
        return Buffer.from(arr);
      }
      case '#': // error
      {
        debug('--> unpack error begin');
        const obj = {};
        const tokensLen = this.tokens.length;
        for (this.tokensIndex++; this.tokensIndex < tokensLen; this.tokensIndex++) {
          const token = this.tokens[this.tokensIndex];
          if (token === ']') {
            const err = new Error(obj.message);
            Object.assign(err, obj);
            debug('--> unpack error end, %j', err);
            return err;
          }
          const key = this._unpack();
          this.tokensIndex++;
          obj[key] = this._unpack();
        }
        const err = new Error(obj.message);
        Object.assign(err, obj);
        return err;
      }
      case TOKEN_TRUE:
        return true;
      case TOKEN_FALSE:
        return false;
      case TOKEN_NULL:
        return null;
      case TOKEN_EMPTY_STRING:
        return '';
      case TOKEN_UNDEFINED:
        return undefined;
      default:
        return this.dictionary[token];
    }
  }
 
  decode(buf) {
    this.dictionary = [];
    this.tokens = [];
    this.tokensIndex = 0;
 
    const packed = buf.toString();
    const arr = packed.split('^');
 
    if (arr[0]) {
      const strArr = arr[0].split('|');
      for (const str of strArr) {
        this.dictionary.push(this._decodeString(str));
      }
    }
 
    if (arr[1]) {
      const intArr = arr[1].split('|');
      for (const int of intArr) {
        this.dictionary.push(this._base36To10(int));
      }
    }
 
    if (arr[2]) {
      const floatArr = arr[2].split('|');
      for (const float of floatArr) {
        this.dictionary.push(parseFloat(float));
      }
    }
 
    if (arr[3]) {
      const dateArr = arr[3].split('|');
      for (const date of dateArr) {
        this.dictionary.push(this._decodeDate(date));
      }
    }
 
    debug('decode packed json => %s, with dictionary %j', packed, this.dictionary);
 
    let tmp = '';
    for (let i = 0, len = arr[4].length; i < len; ++i) {
      const symbol = arr[4][i];
      if (TOKEN_SET.has(symbol)) {
        if (tmp) {
          this.tokens.push(this._base36To10(tmp));
          tmp = '';
        }
        if (symbol !== '|') {
          this.tokens.push(symbol);
        }
      } else {
        tmp += symbol;
      }
    }
    if (tmp) {
      this.tokens.push(this._base36To10(tmp));
    }
    return this._unpack();
  }
}
 
module.exports = JSONDecoder;