encode.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { deflate, inflate } from "pako";
  2. // -----------------------------------------------------------------------------
  3. // byte (binary) strings
  4. // -----------------------------------------------------------------------------
  5. // fast, Buffer-compatible implem
  6. export const toByteString = (data: string | Uint8Array): Promise<string> => {
  7. return new Promise((resolve, reject) => {
  8. const blob =
  9. typeof data === "string"
  10. ? new Blob([new TextEncoder().encode(data)])
  11. : new Blob([data]);
  12. const reader = new FileReader();
  13. reader.onload = (event) => {
  14. if (!event.target || typeof event.target.result !== "string") {
  15. return reject(new Error("couldn't convert to byte string"));
  16. }
  17. resolve(event.target.result);
  18. };
  19. reader.readAsBinaryString(blob);
  20. });
  21. };
  22. const byteStringToArrayBuffer = (byteString: string) => {
  23. const buffer = new ArrayBuffer(byteString.length);
  24. const bufferView = new Uint8Array(buffer);
  25. for (let i = 0, len = byteString.length; i < len; i++) {
  26. bufferView[i] = byteString.charCodeAt(i);
  27. }
  28. return buffer;
  29. };
  30. const byteStringToString = (byteString: string) => {
  31. return new TextDecoder("utf-8").decode(byteStringToArrayBuffer(byteString));
  32. };
  33. // -----------------------------------------------------------------------------
  34. // base64
  35. // -----------------------------------------------------------------------------
  36. /**
  37. * @param isByteString set to true if already byte string to prevent bloat
  38. * due to reencoding
  39. */
  40. export const stringToBase64 = async (str: string, isByteString = false) => {
  41. return isByteString ? btoa(str) : btoa(await toByteString(str));
  42. };
  43. // async to align with stringToBase64
  44. export const base64ToString = async (base64: string, isByteString = false) => {
  45. return isByteString ? atob(base64) : byteStringToString(atob(base64));
  46. };
  47. // -----------------------------------------------------------------------------
  48. // text encoding
  49. // -----------------------------------------------------------------------------
  50. type EncodedData = {
  51. encoded: string;
  52. encoding: "bstring";
  53. /** whether text is compressed (zlib) */
  54. compressed: boolean;
  55. /** version for potential migration purposes */
  56. version?: string;
  57. };
  58. /**
  59. * Encodes (and potentially compresses via zlib) text to byte string
  60. */
  61. export const encode = async ({
  62. text,
  63. compress,
  64. }: {
  65. text: string;
  66. /** defaults to `true`. If compression fails, falls back to bstring alone. */
  67. compress?: boolean;
  68. }): Promise<EncodedData> => {
  69. let deflated!: string;
  70. if (compress !== false) {
  71. try {
  72. deflated = await toByteString(deflate(text));
  73. } catch (error) {
  74. console.error("encode: cannot deflate", error);
  75. }
  76. }
  77. return {
  78. version: "1",
  79. encoding: "bstring",
  80. compressed: !!deflated,
  81. encoded: deflated || (await toByteString(text)),
  82. };
  83. };
  84. export const decode = async (data: EncodedData): Promise<string> => {
  85. let decoded: string;
  86. switch (data.encoding) {
  87. case "bstring":
  88. // if compressed, do not double decode the bstring
  89. decoded = data.compressed
  90. ? data.encoded
  91. : await byteStringToString(data.encoded);
  92. break;
  93. default:
  94. throw new Error(`decode: unknown encoding "${data.encoding}"`);
  95. }
  96. if (data.compressed) {
  97. return inflate(new Uint8Array(byteStringToArrayBuffer(decoded)), {
  98. to: "string",
  99. });
  100. }
  101. return decoded;
  102. };