/lib/base64.js

https://github.com/darobin/jsdom · JavaScript · 175 lines · 102 code · 5 blank · 68 comment · 27 complexity · e4a6947c126fa85ed8d84c83d2ebc5bd MD5 · raw file

  1. "use strict";
  2. // Originally from https://github.com/w3c/web-platform-tests/blob/master/html/webappapis/atob/base64.html
  3. /**
  4. * btoa() as defined by the HTML5 spec, which mostly just references RFC4648.
  5. */
  6. function btoa(s) {
  7. // String conversion as required by WebIDL.
  8. s = String(s);
  9. // "The btoa() method must throw an INVALID_CHARACTER_ERR exception if the
  10. // method's first argument contains any character whose code point is
  11. // greater than U+00FF."
  12. for (let i = 0; i < s.length; i++) {
  13. if (s.charCodeAt(i) > 255) {
  14. return null;
  15. }
  16. }
  17. let out = "";
  18. for (let i = 0; i < s.length; i += 3) {
  19. const groupsOfSix = [undefined, undefined, undefined, undefined];
  20. groupsOfSix[0] = s.charCodeAt(i) >> 2;
  21. groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4;
  22. if (s.length > i + 1) {
  23. groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4;
  24. groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2;
  25. }
  26. if (s.length > i + 2) {
  27. groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6;
  28. groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f;
  29. }
  30. for (let j = 0; j < groupsOfSix.length; j++) {
  31. if (groupsOfSix[j] === undefined) {
  32. out += "=";
  33. } else {
  34. out += btoaLookup(groupsOfSix[j]);
  35. }
  36. }
  37. }
  38. return out;
  39. }
  40. /**
  41. * Lookup table for btoa(), which converts a six-bit number into the
  42. * corresponding ASCII character.
  43. */
  44. function btoaLookup(idx) {
  45. if (idx < 26) {
  46. return String.fromCharCode(idx + "A".charCodeAt(0));
  47. }
  48. if (idx < 52) {
  49. return String.fromCharCode(idx - 26 + "a".charCodeAt(0));
  50. }
  51. if (idx < 62) {
  52. return String.fromCharCode(idx - 52 + "0".charCodeAt(0));
  53. }
  54. if (idx === 62) {
  55. return "+";
  56. }
  57. if (idx === 63) {
  58. return "/";
  59. }
  60. // Throw INVALID_CHARACTER_ERR exception here -- won't be hit in the tests.
  61. }
  62. /**
  63. * Implementation of atob() according to the HTML spec, except that instead of
  64. * throwing INVALID_CHARACTER_ERR we return null.
  65. */
  66. function atob(input) {
  67. // WebIDL requires DOMStrings to just be converted using ECMAScript
  68. // ToString, which in our case amounts to calling String().
  69. input = String(input);
  70. // "Remove all space characters from input."
  71. input = input.replace(/[ \t\n\f\r]/g, "");
  72. // "If the length of input divides by 4 leaving no remainder, then: if
  73. // input ends with one or two U+003D EQUALS SIGN (=) characters, remove
  74. // them from input."
  75. if (input.length % 4 === 0 && /==?$/.test(input)) {
  76. input = input.replace(/==?$/, "");
  77. }
  78. // "If the length of input divides by 4 leaving a remainder of 1, throw an
  79. // INVALID_CHARACTER_ERR exception and abort these steps."
  80. //
  81. // "If input contains a character that is not in the following list of
  82. // characters and character ranges, throw an INVALID_CHARACTER_ERR
  83. // exception and abort these steps:
  84. //
  85. // U+002B PLUS SIGN (+)
  86. // U+002F SOLIDUS (/)
  87. // U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)
  88. // U+0041 LATIN CAPITAL LETTER A to U+005A LATIN CAPITAL LETTER Z
  89. // U+0061 LATIN SMALL LETTER A to U+007A LATIN SMALL LETTER Z"
  90. if (input.length % 4 === 1 || !/^[+/0-9A-Za-z]*$/.test(input)) {
  91. return null;
  92. }
  93. // "Let output be a string, initially empty."
  94. let output = "";
  95. // "Let buffer be a buffer that can have bits appended to it, initially
  96. // empty."
  97. //
  98. // We append bits via left-shift and or. accumulatedBits is used to track
  99. // when we've gotten to 24 bits.
  100. let buffer = 0;
  101. let accumulatedBits = 0;
  102. // "While position does not point past the end of input, run these
  103. // substeps:"
  104. for (let i = 0; i < input.length; i++) {
  105. // "Find the character pointed to by position in the first column of
  106. // the following table. Let n be the number given in the second cell of
  107. // the same row."
  108. //
  109. // "Append to buffer the six bits corresponding to number, most
  110. // significant bit first."
  111. //
  112. // atobLookup() implements the table from the spec.
  113. buffer <<= 6;
  114. buffer |= atobLookup(input[i]);
  115. // "If buffer has accumulated 24 bits, interpret them as three 8-bit
  116. // big-endian numbers. Append the three characters with code points
  117. // equal to those numbers to output, in the same order, and then empty
  118. // buffer."
  119. accumulatedBits += 6;
  120. if (accumulatedBits === 24) {
  121. output += String.fromCharCode((buffer & 0xff0000) >> 16);
  122. output += String.fromCharCode((buffer & 0xff00) >> 8);
  123. output += String.fromCharCode(buffer & 0xff);
  124. buffer = accumulatedBits = 0;
  125. }
  126. // "Advance position by one character."
  127. }
  128. // "If buffer is not empty, it contains either 12 or 18 bits. If it
  129. // contains 12 bits, discard the last four and interpret the remaining
  130. // eight as an 8-bit big-endian number. If it contains 18 bits, discard the
  131. // last two and interpret the remaining 16 as two 8-bit big-endian numbers.
  132. // Append the one or two characters with code points equal to those one or
  133. // two numbers to output, in the same order."
  134. if (accumulatedBits === 12) {
  135. buffer >>= 4;
  136. output += String.fromCharCode(buffer);
  137. } else if (accumulatedBits === 18) {
  138. buffer >>= 2;
  139. output += String.fromCharCode((buffer & 0xff00) >> 8);
  140. output += String.fromCharCode(buffer & 0xff);
  141. }
  142. // "Return output."
  143. return output;
  144. }
  145. /**
  146. * A lookup table for atob(), which converts an ASCII character to the
  147. * corresponding six-bit number.
  148. */
  149. function atobLookup(chr) {
  150. if (/[A-Z]/.test(chr)) {
  151. return chr.charCodeAt(0) - "A".charCodeAt(0);
  152. }
  153. if (/[a-z]/.test(chr)) {
  154. return chr.charCodeAt(0) - "a".charCodeAt(0) + 26;
  155. }
  156. if (/[0-9]/.test(chr)) {
  157. return chr.charCodeAt(0) - "0".charCodeAt(0) + 52;
  158. }
  159. if (chr === "+") {
  160. return 62;
  161. }
  162. if (chr === "/") {
  163. return 63;
  164. }
  165. // Throw exception; should not be hit in tests
  166. }
  167. module.exports = {
  168. atob: atob,
  169. btoa: btoa
  170. };