escape-value.js 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. 'use strict'
  2. /**
  3. * Converts an attribute value into an escaped string as described in
  4. * https://www.rfc-editor.org/rfc/rfc4514#section-2.4.
  5. *
  6. * This function supports up to 4 byte unicode characters.
  7. *
  8. * @param {string} value
  9. * @returns {string} The escaped string.
  10. */
  11. module.exports = function escapeValue (value) {
  12. if (typeof value !== 'string') {
  13. throw Error('value must be a string')
  14. }
  15. const toEscape = Buffer.from(value, 'utf8')
  16. const escaped = []
  17. // We will handle the reverse solidus ('\') on its own.
  18. const embeddedReservedChars = [
  19. 0x22, // '"'
  20. 0x2b, // '+'
  21. 0x2c, // ','
  22. 0x3b, // ';'
  23. 0x3c, // '<'
  24. 0x3e // '>'
  25. ]
  26. for (let i = 0; i < toEscape.byteLength;) {
  27. const charHex = toEscape[i]
  28. // Handle leading space or #.
  29. if (i === 0 && (charHex === 0x20 || charHex === 0x23)) {
  30. escaped.push(toEscapedHexString(charHex))
  31. i += 1
  32. continue
  33. }
  34. // Handle trailing space.
  35. if (i === toEscape.byteLength - 1 && charHex === 0x20) {
  36. escaped.push(toEscapedHexString(charHex))
  37. i += 1
  38. continue
  39. }
  40. if (embeddedReservedChars.includes(charHex) === true) {
  41. escaped.push(toEscapedHexString(charHex))
  42. i += 1
  43. continue
  44. }
  45. if (charHex >= 0xc0 && charHex <= 0xdf) {
  46. // Represents the first byte in a 2-byte UTF-8 character.
  47. escaped.push(toEscapedHexString(charHex))
  48. escaped.push(toEscapedHexString(toEscape[i + 1]))
  49. i += 2
  50. continue
  51. }
  52. if (charHex >= 0xe0 && charHex <= 0xef) {
  53. // Represents the first byte in a 3-byte UTF-8 character.
  54. escaped.push(toEscapedHexString(charHex))
  55. escaped.push(toEscapedHexString(toEscape[i + 1]))
  56. escaped.push(toEscapedHexString(toEscape[i + 2]))
  57. i += 3
  58. continue
  59. }
  60. if (charHex >= 0xf0 && charHex <= 0xf7) {
  61. // Represents the first byte in a 4-byte UTF-8 character.
  62. escaped.push(toEscapedHexString(charHex))
  63. escaped.push(toEscapedHexString(toEscape[i + 1]))
  64. escaped.push(toEscapedHexString(toEscape[i + 2]))
  65. escaped.push(toEscapedHexString(toEscape[i + 3]))
  66. i += 4
  67. continue
  68. }
  69. if (charHex <= 31) {
  70. // Represents an ASCII control character.
  71. escaped.push(toEscapedHexString(charHex))
  72. i += 1
  73. continue
  74. }
  75. escaped.push(String.fromCharCode(charHex))
  76. i += 1
  77. continue
  78. }
  79. return escaped.join('')
  80. }
  81. /**
  82. * Given a byte, convert it to an escaped hex string.
  83. *
  84. * @example
  85. * toEscapedHexString(0x20) // '\20'
  86. *
  87. * @param {number} char
  88. * @returns {string}
  89. */
  90. function toEscapedHexString (char) {
  91. return '\\' + char.toString(16).padStart(2, '0')
  92. }