1/**
2 * URL-safe Base64 encoding and decoding helpers.
3 *
4 * @since 4.0.0
5 */
6import * as Result from "../Result.ts"
7import * as Base64 from "./Base64.ts"
8import { EncodingError } from "./EncodingError.ts"
9
10/**
11 * Encodes the given value into an unpadded URL-safe Base64 string.
12 *
13 * **When to use**
14 *
15 * Use to encode text or bytes in contexts that require the URL-safe alphabet.
16 *
17 * **Details**
18 *
19 * String inputs are encoded as UTF-8 bytes before Base64Url encoding.
20 * `Uint8Array` inputs are encoded directly. The output removes `=` padding
21 * and replaces `+` with `-` and `/` with `_`.
22 *
23 * **Example** (Encoding URL-safe Base64)
24 *
25 * ```ts import.meta.vitest
26 * import { Base64Url } from "effect/encoding"
27 *
28 * Base64Url.encode("hello?") // => "aGVsbG8_"
29 *
30 * const bytes = new Uint8Array([72, 101, 108, 108, 111, 63])
31 * Base64Url.encode(bytes) // => "SGVsbG8_"
32 * ```
33 *
34 * @see {@link decode} for decoding URL-safe Base64 to bytes
35 * @see {@link decodeString} for decoding URL-safe Base64 to UTF-8 text
36 *
37 * @category encoding
38 * @since 4.0.0
39 */
40export const encode: (input: Uint8Array | string) => string = (input) =>
41 Base64.encode(input).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
42
43/**
44 * Decodes a padded or unpadded URL-safe Base64 string into bytes safely.
45 *
46 * **When to use**
47 *
48 * Use to decode Base64Url text into bytes without throwing on invalid input.
49 *
50 * **Details**
51 *
52 * Returns `Result.succeed` with a `Uint8Array` when decoding succeeds, or
53 * `Result.fail` with an `EncodingError` when the input is not valid
54 * Base64Url. Both padded and unpadded forms are accepted when otherwise valid.
55 *
56 * **Example** (Decoding URL-safe Base64 bytes)
57 *
58 * ```ts import.meta.vitest
59 * import { Result } from "effect"
60 * import { Base64Url } from "effect/encoding"
61 *
62 * Base64Url.decode("SGVsbG8_") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111, 63]))
63 * ```
64 *
65 * @category decoding
66 * @since 4.0.0
67 */
68export const decode = (str: string): Result.Result<Uint8Array, EncodingError> => {
69 const stripped = str.replace(/[\n\r]/g, "")
70 const length = stripped.length
71 if (length % 4 === 1) {
72 return Result.fail(
73 new EncodingError({
74 module: "Base64Url",
75 kind: "Decode",
76 input: stripped,
77 message: `Length should be a multiple of 4, but is ${length}`
78 })
79 )
80 }
81 if (!/^[-_A-Z0-9]*?={0,2}$/i.test(stripped)) {
82 return Result.fail(
83 new EncodingError({
84 module: "Base64Url",
85 kind: "Decode",
86 input: stripped,
87 message: "Invalid input"
88 })
89 )
90 }
91 let sanitized = length % 4 === 2 ? `${stripped}==` : length % 4 === 3 ? `${stripped}=` : stripped
92 sanitized = sanitized.replace(/-/g, "+").replace(/_/g, "/")
93 return Base64.decode(sanitized)
94}
95
96/**
97 * Decodes a padded or unpadded URL-safe Base64 string into UTF-8 text safely.
98 *
99 * **When to use**
100 *
101 * Use to decode Base64Url text into UTF-8 text without throwing on invalid
102 * input.
103 *
104 * **Details**
105 *
106 * Returns `Result.succeed` with the decoded text when decoding succeeds, or
107 * `Result.fail` with an `EncodingError` when the input is not valid
108 * Base64Url.
109 *
110 * **Example** (Decoding URL-safe Base64 strings)
111 *
112 * ```ts import.meta.vitest
113 * import { Result } from "effect"
114 * import { Base64Url } from "effect/encoding"
115 *
116 * Base64Url.decodeString("aGVsbG8_") // => Result.succeed("hello?")
117 * ```
118 *
119 * @category decoding
120 * @since 4.0.0
121 */
122export const decodeString = (str: string) => Result.map(decode(str), (_) => decoder.decode(_))
123
124const decoder = new TextDecoder()