stringDecoder.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. // based on Joyent's StringDecoder
  2. // https://github.com/nodejs/string_decoder/blob/master/lib/string_decoder.js
  3. export class StringDecoder {
  4. lastNeed: number
  5. lastTotal: number
  6. lastChar: Buffer
  7. constructor () {
  8. this.lastNeed = 0
  9. this.lastTotal = 0
  10. this.lastChar = Buffer.allocUnsafe(4)
  11. }
  12. write (buf: Buffer): Buffer {
  13. if (buf.length === 0) {
  14. return buf
  15. }
  16. let r: Buffer|undefined = undefined
  17. let i = 0
  18. if (this.lastNeed) {
  19. r = this.fillLast(buf)
  20. if (r === undefined) {
  21. return Buffer.from('')
  22. }
  23. i = this.lastNeed
  24. this.lastNeed = 0
  25. }
  26. if (i < buf.length) {
  27. return r ? Buffer.concat([r, this.text(buf, i)]) : this.text(buf, i)
  28. }
  29. return r
  30. }
  31. // For UTF-8, a replacement character is added when ending on a partial
  32. // character.
  33. end (buf?: Buffer): Buffer {
  34. const r = buf?.length ? this.write(buf) : Buffer.from('')
  35. if (this.lastNeed) {
  36. console.log('end', r)
  37. return Buffer.concat([r, Buffer.from('\ufffd')])
  38. }
  39. return r
  40. }
  41. // Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a
  42. // partial character, the character's bytes are buffered until the required
  43. // number of bytes are available.
  44. private text (buf: Buffer, i: number) {
  45. const total = this.utf8CheckIncomplete(buf, i)
  46. if (!this.lastNeed) {
  47. return buf.slice(i)
  48. }
  49. this.lastTotal = total
  50. const end = buf.length - (total - this.lastNeed)
  51. buf.copy(this.lastChar, 0, end)
  52. return buf.slice(i, end)
  53. }
  54. // Attempts to complete a partial non-UTF-8 character using bytes from a Buffer
  55. private fillLast (buf: Buffer): Buffer|undefined {
  56. if (this.lastNeed <= buf.length) {
  57. buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed)
  58. return this.lastChar.slice(0, this.lastTotal)
  59. }
  60. buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length)
  61. this.lastNeed -= buf.length
  62. return undefined
  63. }
  64. // Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a
  65. // continuation byte. If an invalid byte is detected, -2 is returned.
  66. private utf8CheckByte (byte) {
  67. if (byte <= 0x7F) {return 0} else if (byte >> 5 === 0x06) {return 2} else if (byte >> 4 === 0x0E) {return 3} else if (byte >> 3 === 0x1E) {return 4}
  68. return byte >> 6 === 0x02 ? -1 : -2
  69. }
  70. // Checks at most 3 bytes at the end of a Buffer in order to detect an
  71. // incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4)
  72. // needed to complete the UTF-8 character (if applicable) are returned.
  73. private utf8CheckIncomplete (buf, i) {
  74. let j = buf.length - 1
  75. if (j < i) {return 0}
  76. let nb = this.utf8CheckByte(buf[j])
  77. if (nb >= 0) {
  78. if (nb > 0) {this.lastNeed = nb - 1}
  79. return nb
  80. }
  81. if (--j < i || nb === -2) {return 0}
  82. nb = this.utf8CheckByte(buf[j])
  83. if (nb >= 0) {
  84. if (nb > 0) {this.lastNeed = nb - 2}
  85. return nb
  86. }
  87. if (--j < i || nb === -2) {return 0}
  88. nb = this.utf8CheckByte(buf[j])
  89. if (nb >= 0) {
  90. if (nb > 0) {
  91. if (nb === 2) {nb = 0} else {this.lastNeed = nb - 3}
  92. }
  93. return nb
  94. }
  95. return 0
  96. }
  97. }
  98. // // Validates as many continuation bytes for a multi-byte UTF-8 character as
  99. // // needed or are available. If we see a non-continuation byte where we expect
  100. // // one, we "replace" the validated continuation bytes we've seen so far with
  101. // // a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding
  102. // // behavior. The continuation byte check is included three times in the case
  103. // // where all of the continuation bytes for a character exist in the same buffer.
  104. // // It is also done this way as a slight performance increase instead of using a
  105. // // loop.
  106. // function utf8CheckExtraBytes (self, buf, p) {
  107. // if ((buf[0] & 0xC0) !== 0x80) {
  108. // self.lastNeed = 0
  109. // return '\ufffd'
  110. // }
  111. // if (self.lastNeed > 1 && buf.length > 1) {
  112. // if ((buf[1] & 0xC0) !== 0x80) {
  113. // self.lastNeed = 1
  114. // return '\ufffd'
  115. // }
  116. // if (self.lastNeed > 2 && buf.length > 2) {
  117. // if ((buf[2] & 0xC0) !== 0x80) {
  118. // self.lastNeed = 2
  119. // return '\ufffd'
  120. // }
  121. // }
  122. // }
  123. // }
  124. // // Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer.
  125. // function utf8FillLast (buf) {
  126. // const p = this.lastTotal - this.lastNeed
  127. // const r = utf8CheckExtraBytes(this, buf, p)
  128. // if (r !== undefined) {return r}
  129. // if (this.lastNeed <= buf.length) {
  130. // buf.copy(this.lastChar, p, 0, this.lastNeed)
  131. // return this.lastChar.toString(this.encoding, 0, this.lastTotal)
  132. // }
  133. // buf.copy(this.lastChar, p, 0, buf.length)
  134. // this.lastNeed -= buf.length
  135. // }