expo 55.0.28 → 55.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ buildscript {
10
10
  }
11
11
 
12
12
  group = 'host.exp.exponent'
13
- version = '55.0.28'
13
+ version = '55.0.29'
14
14
 
15
15
  expoModule {
16
16
  // We can't prebuild the module because it depends on the generated files.
@@ -21,7 +21,7 @@ android {
21
21
  namespace "expo.core"
22
22
  defaultConfig {
23
23
  versionCode 1
24
- versionName "55.0.28"
24
+ versionName "55.0.29"
25
25
  consumerProguardFiles("proguard-rules.pro")
26
26
  }
27
27
  testOptions {
@@ -8,7 +8,9 @@ import expo.modules.kotlin.AppContext
8
8
  import expo.modules.kotlin.sharedobjects.SharedObject
9
9
  import kotlinx.coroutines.CoroutineScope
10
10
  import kotlinx.coroutines.Dispatchers
11
+ import kotlinx.coroutines.NonCancellable
11
12
  import kotlinx.coroutines.launch
13
+ import kotlinx.coroutines.withContext
12
14
  import okhttp3.Call
13
15
  import okhttp3.Callback
14
16
  import okhttp3.Response
@@ -138,10 +140,17 @@ internal class NativeResponse(appContext: AppContext, private val coroutineScope
138
140
  responseInit = createResponseInit(response)
139
141
  state = ResponseState.RESPONSE_RECEIVED
140
142
 
141
- coroutineScope.launch(Dispatchers.IO) {
143
+ coroutineScope.launch {
142
144
  val stream = response.body?.source() ?: return@launch
143
- pumpResponseBodyStream(stream)
144
- response.close()
145
+ try {
146
+ pumpResponseBodyStream(stream)
147
+ } finally {
148
+ // Close off the modules queue so the blocking call doesn't stall it.
149
+ // NonCancellable guarantees the response is released even if the scope is canceled mid-stream.
150
+ withContext(NonCancellable + Dispatchers.IO) {
151
+ response.close()
152
+ }
153
+ }
145
154
 
146
155
  if (this@NativeResponse.state == ResponseState.BODY_STREAMING_STARTED) {
147
156
  emit("didComplete")
@@ -182,9 +191,13 @@ internal class NativeResponse(appContext: AppContext, private val coroutineScope
182
191
  )
183
192
  }
184
193
 
185
- private fun pumpResponseBodyStream(stream: BufferedSource) {
194
+ private suspend fun pumpResponseBodyStream(stream: BufferedSource) {
186
195
  try {
187
- while (!stream.exhausted()) {
196
+ while (true) {
197
+ val data = withContext(Dispatchers.IO) {
198
+ if (stream.exhausted()) null else stream.buffer.readByteArray()
199
+ } ?: break
200
+
188
201
  if (isInvalidState(
189
202
  ResponseState.RESPONSE_RECEIVED,
190
203
  ResponseState.BODY_STREAMING_STARTED,
@@ -194,9 +207,9 @@ internal class NativeResponse(appContext: AppContext, private val coroutineScope
194
207
  break
195
208
  }
196
209
  if (state == ResponseState.RESPONSE_RECEIVED) {
197
- sink.appendBufferBody(stream.buffer.readByteArray())
210
+ sink.appendBufferBody(data)
198
211
  } else if (state == ResponseState.BODY_STREAMING_STARTED) {
199
- emit("didReceiveResponseData", stream.buffer.readByteArray())
212
+ emit("didReceiveResponseData", data)
200
213
  } else {
201
214
  break
202
215
  }
@@ -1,10 +1,5 @@
1
1
  export declare class TextDecoder {
2
- private _encoding;
3
- private _ignoreBOM;
4
- private _errorMode;
5
- private _BOMseen;
6
- private _doNotFlush;
7
- private _decoder;
2
+ private readonly _state;
8
3
  constructor(label?: string, options?: {
9
4
  fatal?: boolean;
10
5
  ignoreBOM?: boolean;
@@ -12,9 +7,8 @@ export declare class TextDecoder {
12
7
  get encoding(): string;
13
8
  get fatal(): boolean;
14
9
  get ignoreBOM(): boolean;
15
- decode(input?: ArrayBuffer | DataView, options?: {
10
+ decode(input?: ArrayBuffer | ArrayBufferView, options?: {
16
11
  stream?: boolean;
17
12
  }): string;
18
- private serializeStream;
19
13
  }
20
14
  //# sourceMappingURL=TextDecoder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextDecoder.d.ts","sourceRoot":"","sources":["../../src/winter/TextDecoder.ts"],"names":[],"mappings":"AAwTA,qBAAa,WAAW;IACtB,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,QAAQ,CAA4B;gBAG1C,KAAK,GAAE,MAAgB,EACvB,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,SAAS,CAAC,EAAE,OAAO,CAAC;KAChB;IAwBR,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,MAAM,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,QAAQ,EAAE,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,MAAM;IAqDlF,OAAO,CAAC,eAAe;CAcxB"}
1
+ {"version":3,"file":"TextDecoder.d.ts","sourceRoot":"","sources":["../../src/winter/TextDecoder.ts"],"names":[],"mappings":"AAqXA,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;gBAE9B,KAAK,GAAE,MAAgB,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO;IAgB3F,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,MAAM,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,eAAe,EAAE,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,MAAM;CAsB1F"}
@@ -16,84 +16,84 @@
16
16
  "expo-analytics-amplitude": "~11.3.0",
17
17
  "expo-app-auth": "~11.1.0",
18
18
  "expo-app-loader-provider": "~8.0.0",
19
- "expo-app-metrics": "~0.2.4",
20
- "expo-apple-authentication": "~55.0.15",
21
- "expo-application": "~55.0.17",
22
- "expo-asset": "~55.0.18",
23
- "expo-audio": "~55.0.16",
24
- "expo-auth-session": "~55.0.17",
25
- "expo-background-fetch": "~55.0.18",
26
- "expo-background-task": "~55.0.20",
27
- "expo-battery": "~55.0.15",
28
- "expo-blur": "~55.0.16",
29
- "expo-brightness": "~55.0.15",
30
- "expo-brownfield": "~55.0.26",
31
- "expo-build-properties": "~55.0.16",
32
- "expo-calendar": "~55.0.17",
33
- "expo-camera": "~55.0.21",
34
- "expo-cellular": "~55.0.15",
19
+ "expo-app-metrics": "~0.2.5",
20
+ "expo-apple-authentication": "~55.0.16",
21
+ "expo-application": "~55.0.18",
22
+ "expo-asset": "~55.0.19",
23
+ "expo-audio": "~55.0.17",
24
+ "expo-auth-session": "~55.0.18",
25
+ "expo-background-fetch": "~55.0.19",
26
+ "expo-background-task": "~55.0.21",
27
+ "expo-battery": "~55.0.16",
28
+ "expo-blur": "~55.0.17",
29
+ "expo-brightness": "~55.0.16",
30
+ "expo-brownfield": "~55.0.27",
31
+ "expo-build-properties": "~55.0.17",
32
+ "expo-calendar": "~55.0.18",
33
+ "expo-camera": "~55.0.22",
34
+ "expo-cellular": "~55.0.16",
35
35
  "expo-checkbox": "~55.0.4",
36
- "expo-clipboard": "~55.0.15",
36
+ "expo-clipboard": "~55.0.16",
37
37
  "expo-constants": "~55.0.17",
38
- "expo-contacts": "~55.0.16",
39
- "expo-crypto": "~55.0.17",
40
- "expo-dev-client": "~55.0.37",
41
- "expo-device": "~55.0.19",
42
- "expo-document-picker": "~55.0.15",
43
- "expo-file-system": "~55.0.24",
38
+ "expo-contacts": "~55.0.17",
39
+ "expo-crypto": "~55.0.18",
40
+ "expo-dev-client": "~55.0.38",
41
+ "expo-device": "~55.0.20",
42
+ "expo-document-picker": "~55.0.16",
43
+ "expo-file-system": "~55.0.25",
44
44
  "expo-font": "~55.0.8",
45
- "expo-gl": "~55.0.16",
45
+ "expo-gl": "~55.0.17",
46
46
  "expo-glass-effect": "~55.0.11",
47
47
  "expo-google-app-auth": "~8.3.0",
48
- "expo-haptics": "~55.0.16",
48
+ "expo-haptics": "~55.0.17",
49
49
  "expo-image": "~55.0.11",
50
50
  "expo-image-loader": "~55.0.1",
51
- "expo-image-manipulator": "~55.0.19",
52
- "expo-image-picker": "~55.0.22",
53
- "expo-intent-launcher": "~55.0.14",
54
- "expo-insights": "~55.0.19",
51
+ "expo-image-manipulator": "~55.0.20",
52
+ "expo-image-picker": "~55.0.23",
53
+ "expo-intent-launcher": "~55.0.15",
54
+ "expo-insights": "~55.0.20",
55
55
  "expo-keep-awake": "~55.0.8",
56
- "expo-linear-gradient": "~55.0.16",
57
- "expo-linking": "~55.0.16",
58
- "expo-local-authentication": "~55.0.16",
59
- "expo-localization": "~55.0.17",
60
- "expo-location": "~55.1.12",
61
- "expo-mail-composer": "~55.0.16",
62
- "expo-manifests": "~55.0.19",
63
- "expo-maps": "~55.0.21",
56
+ "expo-linear-gradient": "~55.0.17",
57
+ "expo-linking": "~55.0.17",
58
+ "expo-local-authentication": "~55.0.17",
59
+ "expo-localization": "~55.0.18",
60
+ "expo-location": "~55.1.13",
61
+ "expo-mail-composer": "~55.0.17",
62
+ "expo-manifests": "~55.0.20",
63
+ "expo-maps": "~55.0.22",
64
64
  "expo-mcp": "~0.2.1",
65
- "expo-media-library": "~55.0.19",
66
- "expo-mesh-gradient": "~55.0.16",
67
- "expo-module-template": "~55.0.23",
65
+ "expo-media-library": "~55.0.20",
66
+ "expo-mesh-gradient": "~55.0.17",
67
+ "expo-module-template": "~55.0.24",
68
68
  "expo-modules-core": "~55.0.25",
69
- "expo-navigation-bar": "~55.0.15",
70
- "expo-network": "~55.0.16",
71
- "expo-notifications": "~55.0.25",
72
- "expo-observe": "~0.2.4",
73
- "expo-print": "~55.0.17",
74
- "expo-live-photo": "~55.0.17",
75
- "expo-router": "~55.0.17",
76
- "expo-screen-capture": "~55.0.16",
77
- "expo-screen-orientation": "~55.0.18",
78
- "expo-secure-store": "~55.0.16",
79
- "expo-sensors": "~55.0.17",
80
- "expo-server": "~55.0.11",
81
- "expo-sharing": "~55.0.22",
82
- "expo-sms": "~55.0.16",
83
- "expo-speech": "~55.0.16",
84
- "expo-splash-screen": "~55.0.23",
85
- "expo-sqlite": "~55.0.18",
69
+ "expo-navigation-bar": "~55.0.16",
70
+ "expo-network": "~55.0.17",
71
+ "expo-notifications": "~55.0.26",
72
+ "expo-observe": "~0.2.5",
73
+ "expo-print": "~55.0.18",
74
+ "expo-live-photo": "~55.0.18",
75
+ "expo-router": "~55.0.18",
76
+ "expo-screen-capture": "~55.0.17",
77
+ "expo-screen-orientation": "~55.0.19",
78
+ "expo-secure-store": "~55.0.17",
79
+ "expo-sensors": "~55.0.18",
80
+ "expo-server": "~55.0.12",
81
+ "expo-sharing": "~55.0.23",
82
+ "expo-sms": "~55.0.17",
83
+ "expo-speech": "~55.0.17",
84
+ "expo-splash-screen": "~55.0.24",
85
+ "expo-sqlite": "~55.0.19",
86
86
  "expo-status-bar": "~55.0.6",
87
- "expo-store-review": "~55.0.16",
87
+ "expo-store-review": "~55.0.17",
88
88
  "expo-symbols": "~55.0.9",
89
- "expo-system-ui": "~55.0.20",
90
- "expo-task-manager": "~55.0.18",
91
- "expo-tracking-transparency": "~55.0.16",
92
- "expo-updates": "~55.0.26",
93
- "expo-video-thumbnails": "~55.0.17",
94
- "expo-video": "~55.0.19",
95
- "expo-web-browser": "~55.0.18",
96
- "jest-expo": "~55.0.20",
89
+ "expo-system-ui": "~55.0.21",
90
+ "expo-task-manager": "~55.0.19",
91
+ "expo-tracking-transparency": "~55.0.17",
92
+ "expo-updates": "~55.0.27",
93
+ "expo-video-thumbnails": "~55.0.18",
94
+ "expo-video": "~55.0.20",
95
+ "expo-web-browser": "~55.0.19",
96
+ "jest-expo": "~55.0.21",
97
97
  "lottie-react-native": "~7.3.4",
98
98
  "react": "19.2.0",
99
99
  "react-dom": "19.2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "55.0.28",
3
+ "version": "55.0.29",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -77,24 +77,24 @@
77
77
  "homepage": "https://github.com/expo/expo/tree/main/packages/expo",
78
78
  "dependencies": {
79
79
  "@babel/runtime": "^7.20.0",
80
- "@expo/cli": "55.0.34",
81
- "@expo/config": "~55.0.19",
80
+ "@expo/cli": "55.0.35",
81
+ "@expo/config": "~55.0.20",
82
82
  "@expo/config-plugins": "~55.0.11",
83
83
  "@expo/devtools": "55.0.3",
84
84
  "@expo/fingerprint": "0.16.8",
85
85
  "@expo/local-build-cache-provider": "55.0.15",
86
86
  "@expo/log-box": "55.0.13",
87
87
  "@expo/metro": "~55.1.1",
88
- "@expo/metro-config": "55.0.25",
88
+ "@expo/metro-config": "55.0.26",
89
89
  "@expo/vector-icons": "^15.0.2",
90
90
  "@ungap/structured-clone": "^1.3.0",
91
91
  "babel-preset-expo": "~55.0.24",
92
- "expo-asset": "~55.0.18",
92
+ "expo-asset": "~55.0.19",
93
93
  "expo-constants": "~55.0.17",
94
- "expo-file-system": "~55.0.24",
94
+ "expo-file-system": "~55.0.25",
95
95
  "expo-font": "~55.0.8",
96
96
  "expo-keep-awake": "~55.0.8",
97
- "expo-modules-autolinking": "55.0.25",
97
+ "expo-modules-autolinking": "55.0.26",
98
98
  "expo-modules-core": "55.0.25",
99
99
  "pretty-format": "^29.7.0",
100
100
  "react-refresh": "^0.14.2",
@@ -130,5 +130,5 @@
130
130
  "optional": true
131
131
  }
132
132
  },
133
- "gitHead": "c8f16914a2713c37fe446c46d613004626b3e6b3"
133
+ "gitHead": "764641f87074c1e49d004790c5a5983f4b5bfc6b"
134
134
  }
@@ -1,429 +1,427 @@
1
- // A fork of text-encoding but with only UTF-8 decoder.
2
- // `TextEncoder` is in Hermes and we only need utf-8 decoder for React Server Components.
3
- //
4
- // https://github.com/inexorabletash/text-encoding/blob/3f330964c0e97e1ed344c2a3e963f4598610a7ad/lib/encoding.js#L1
1
+ // UTF-8-only TextDecoder fallback for runtimes that do not provide one.
5
2
 
6
- /**
7
- * Checks if a number is within a specified range.
8
- * @param a The number to test.
9
- * @param min The minimum value in the range, inclusive.
10
- * @param max The maximum value in the range, inclusive.
11
- * @returns `true` if a passed number is within the specified range.
12
- */
13
- function inRange(a: number, min: number, max: number): boolean {
14
- return min <= a && a <= max;
3
+ const EMPTY_BYTES = new Uint8Array(0);
4
+
5
+ interface TextDecoderState {
6
+ ignoreBOM: boolean;
7
+ fatal: boolean;
8
+ bomSeen: boolean;
9
+ pending: Uint8Array;
10
+ streaming: boolean;
15
11
  }
16
12
 
17
- /**
18
- * Converts an array of code points to a string.
19
- * @param codePoints Array of code points.
20
- * @returns The string representation of given array.
21
- */
22
- function codePointsToString(codePoints: number[]): string {
23
- let s = '';
24
- for (let i = 0; i < codePoints.length; ++i) {
25
- let cp = codePoints[i];
26
- if (cp <= 0xffff) {
27
- s += String.fromCharCode(cp);
28
- } else {
29
- cp -= 0x10000;
30
- s += String.fromCharCode((cp >> 10) + 0xd800, (cp & 0x3ff) + 0xdc00);
31
- }
13
+ function assertValidUTF8Label(label: unknown): void {
14
+ const normalizedLabel = String(label).trim().toLowerCase();
15
+ switch (normalizedLabel) {
16
+ case 'unicode-1-1-utf-8':
17
+ case 'unicode11utf8':
18
+ case 'unicode20utf8':
19
+ case 'utf-8':
20
+ case 'utf8':
21
+ case 'x-unicode20utf8':
22
+ return;
23
+ default:
24
+ throw new RangeError(`Unknown encoding: ${label} (normalized: ${normalizedLabel})`);
32
25
  }
33
- return s;
34
26
  }
35
27
 
36
- function normalizeBytes(input?: ArrayBuffer | DataView): Uint8Array {
37
- if (typeof input === 'object' && input instanceof ArrayBuffer) {
38
- return new Uint8Array(input);
28
+ function normalizeBytes(input: ArrayBuffer | ArrayBufferView | undefined): Uint8Array {
29
+ if (input === undefined) {
30
+ return EMPTY_BYTES;
31
+ } else if (input instanceof Uint8Array) {
32
+ return input;
33
+ } else if (ArrayBuffer.isView(input)) {
34
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
39
35
  } else if (
40
- typeof input === 'object' &&
41
- 'buffer' in input &&
42
- input.buffer instanceof ArrayBuffer
36
+ (input[Symbol.toStringTag] as string) === 'ArrayBuffer' ||
37
+ (input[Symbol.toStringTag] as string) === 'SharedArrayBuffer'
43
38
  ) {
44
- return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
39
+ return new Uint8Array(input as ArrayBuffer);
40
+ } else {
41
+ throw new TypeError('The input must be an ArrayBuffer or ArrayBufferView');
45
42
  }
46
- return new Uint8Array(0);
47
43
  }
48
44
 
49
- /**
50
- * End-of-stream is a special token that signifies no more tokens
51
- * are in the stream.
52
- */
53
- const END_OF_STREAM = -1;
54
-
55
- const FINISHED = -1;
56
-
57
- /**
58
- * A stream represents an ordered sequence of tokens.
59
- *
60
- * @constructor
61
- * @param {!(number[]|Uint8Array)} tokens Array of tokens that provide the stream.
62
- */
63
- class Stream {
64
- private tokens: number[];
65
-
66
- constructor(tokens: number[] | Uint8Array) {
67
- this.tokens = Array.prototype.slice.call(tokens);
68
- // Reversed as push/pop is more efficient than shift/unshift.
69
- this.tokens.reverse();
70
- }
71
-
72
- /**
73
- * @return {boolean} True if end-of-stream has been hit.
74
- */
75
- endOfStream(): boolean {
76
- return !this.tokens.length;
77
- }
78
-
79
- /**
80
- * When a token is read from a stream, the first token in the
81
- * stream must be returned and subsequently removed, and
82
- * end-of-stream must be returned otherwise.
83
- *
84
- * @return {number} Get the next token from the stream, or
85
- * end_of_stream.
86
- */
87
- read(): number {
88
- if (!this.tokens.length) return END_OF_STREAM;
89
- return this.tokens.pop()!;
45
+ function decoderError(state: TextDecoderState): string {
46
+ if (state.fatal) {
47
+ throw new TypeError('Decoder error');
90
48
  }
49
+ return '\ufffd';
50
+ }
91
51
 
92
- /**
93
- * When one or more tokens are prepended to a stream, those tokens
94
- * must be inserted, in given order, before the first token in the
95
- * stream.
96
- *
97
- * @param token The token(s) to prepend to the stream.
98
- */
99
- prepend(token: number | number[]): void {
100
- if (Array.isArray(token)) {
101
- while (token.length) this.tokens.push(token.pop()!);
102
- } else {
103
- this.tokens.push(token);
104
- }
105
- }
106
-
107
- /**
108
- * When one or more tokens are pushed to a stream, those tokens
109
- * must be inserted, in given order, after the last token in the
110
- * stream.
111
- *
112
- * @param token The tokens(s) to push to the stream.
113
- */
114
- push(token: number | number[]): void {
115
- if (Array.isArray(token)) {
116
- while (token.length) this.tokens.unshift(token.shift()!);
117
- } else {
118
- this.tokens.unshift(token);
119
- }
52
+ function appendASCII(output: string, bytes: Uint8Array, start: number, end: number): string {
53
+ // NOTE(@kitten): For longer strings, direct `apply` + `subarray` conversion w/o byte-for-byte copy or spreads is
54
+ // the most efficient by far (3x), leaning on native conversion below the variadic limit
55
+ const HERMES_VARIADIC_ARGUMENT_LIMIT = 4096;
56
+ while (start < end) {
57
+ output += String.fromCharCode.apply(
58
+ null,
59
+ bytes.subarray(
60
+ start,
61
+ Math.min(start + HERMES_VARIADIC_ARGUMENT_LIMIT, end)
62
+ ) as unknown as number[]
63
+ );
64
+ start += HERMES_VARIADIC_ARGUMENT_LIMIT;
120
65
  }
66
+ return output;
121
67
  }
122
68
 
123
- function decoderError(fatal: boolean, opt_code_point?: number) {
124
- if (fatal) throw TypeError('Decoder error');
125
- return opt_code_point || 0xfffd;
69
+ interface DecodeFallback {
70
+ output: string;
71
+ index: number;
126
72
  }
127
73
 
128
- interface Encoding {
129
- name: string;
130
- labels: string[];
74
+ function fallback(output: string, index: number): DecodeFallback {
75
+ return { output, index };
131
76
  }
132
77
 
133
- const LABEL_ENCODING_MAP: { [key: string]: Encoding } = {};
134
-
135
- function getEncoding(label: string): Encoding | null {
136
- label = label.trim().toLowerCase();
137
- if (label in LABEL_ENCODING_MAP) {
138
- return LABEL_ENCODING_MAP[label];
78
+ function decodeUTF8Fast(bytes: Uint8Array, start: number): string | DecodeFallback {
79
+ let output = '';
80
+ let i = start;
81
+ const length = bytes.length;
82
+ while (i < length) {
83
+ const b0 = bytes[i]!;
84
+ if (b0 < 0x80) {
85
+ // NOTE(@kitten): For ASCII text, it's fastest to process the first 32 chars directly
86
+ // After, `appendASCII`'s variadic apply wins out
87
+ const directEnd = Math.min(i + 32, length);
88
+ do {
89
+ output += String.fromCharCode(bytes[i++]!);
90
+ } while (i < directEnd && bytes[i]! < 0x80);
91
+ if (i === directEnd && i < length && bytes[i]! < 0x80) {
92
+ const asciiStart = i;
93
+ do {
94
+ i++;
95
+ } while (i < length && bytes[i]! < 0x80);
96
+ output = appendASCII(output, bytes, asciiStart, i);
97
+ }
98
+ } else if (b0 >= 0xc2 && b0 <= 0xdf) {
99
+ if (i + 1 >= length) return fallback(output, i);
100
+ const b1 = bytes[i + 1]!;
101
+ if ((b1 & 0xc0) !== 0x80) return fallback(output, i);
102
+ output += String.fromCharCode(((b0 & 0x1f) << 6) | (b1 & 0x3f));
103
+ i += 2;
104
+ } else if (b0 >= 0xe0 && b0 <= 0xef) {
105
+ if (i + 2 >= length) return fallback(output, i);
106
+ const b1 = bytes[i + 1]!;
107
+ const b2 = bytes[i + 2]!;
108
+ if (
109
+ b1 < (b0 === 0xe0 ? 0xa0 : 0x80) ||
110
+ b1 > (b0 === 0xed ? 0x9f : 0xbf) ||
111
+ (b2 & 0xc0) !== 0x80
112
+ ) {
113
+ return fallback(output, i);
114
+ }
115
+ output += String.fromCharCode(((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f));
116
+ i += 3;
117
+ } else if (b0 >= 0xf0 && b0 <= 0xf4) {
118
+ if (i + 3 >= length) return fallback(output, i);
119
+ const b1 = bytes[i + 1]!;
120
+ const b2 = bytes[i + 2]!;
121
+ const b3 = bytes[i + 3]!;
122
+ if (
123
+ b1 < (b0 === 0xf0 ? 0x90 : 0x80) ||
124
+ b1 > (b0 === 0xf4 ? 0x8f : 0xbf) ||
125
+ (b2 & 0xc0) !== 0x80 ||
126
+ (b3 & 0xc0) !== 0x80
127
+ ) {
128
+ return fallback(output, i);
129
+ }
130
+ const codePoint =
131
+ (((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f)) - 0x10000;
132
+ output += String.fromCharCode((codePoint >> 10) + 0xd800, (codePoint & 0x3ff) + 0xdc00);
133
+ i += 4;
134
+ } else {
135
+ return fallback(output, i);
136
+ }
139
137
  }
140
- return null;
141
- }
142
-
143
- /** [Encodings table](https://encoding.spec.whatwg.org/encodings.json) (Incomplete as we only need TextDecoder utf8 in Expo RSC. A more complete implementation should be added to Hermes as native code.) */
144
- const ENCODING_MAP: { heading: string; encodings: Encoding[] }[] = [
145
- {
146
- encodings: [
147
- {
148
- labels: [
149
- 'unicode-1-1-utf-8',
150
- 'unicode11utf8',
151
- 'unicode20utf8',
152
- 'utf-8',
153
- 'utf8',
154
- 'x-unicode20utf8',
155
- ],
156
- name: 'UTF-8',
157
- },
158
- ],
159
- heading: 'The Encoding',
160
- },
161
- ];
162
138
 
163
- ENCODING_MAP.forEach((category) => {
164
- category.encodings.forEach((encoding) => {
165
- encoding.labels.forEach((label) => {
166
- LABEL_ENCODING_MAP[label] = encoding;
167
- });
168
- });
169
- });
170
-
171
- // Registry of of encoder/decoder factories, by encoding name.
172
- const DECODERS: { [key: string]: (options: { fatal: boolean }) => UTF8Decoder } = {
173
- 'UTF-8': (options) => new UTF8Decoder(options),
174
- };
175
-
176
- // 9.1.1 utf-8 decoder
177
-
178
- interface Decoder {
179
- handler: (stream: Stream, bite: number) => number | number[] | null | -1;
139
+ return output;
180
140
  }
181
141
 
182
- class UTF8Decoder implements Decoder {
183
- // utf-8's decoder's has an associated utf-8 code point, utf-8
184
- // bytes seen, and utf-8 bytes needed (all initially 0), a utf-8
185
- // lower boundary (initially 0x80), and a utf-8 upper boundary
186
- // (initially 0xBF).
187
- private utf8CodePoint = 0;
188
- private utf8BytesSeen = 0;
189
- private utf8BytesNeeded = 0;
190
- private utf8LowerBoundary = 0x80;
191
- private utf8UpperBoundary = 0xbf;
192
- constructor(private options: { fatal: boolean }) {}
193
- /**
194
- * @param {Stream} stream The stream of bytes being decoded.
195
- * @param {number} bite The next byte read from the stream.
196
- * @return {?(number|!Array.<number>)} The next code point(s)
197
- * decoded, or null if not enough data exists in the input
198
- * stream to decode a complete code point.
199
- */
200
- handler(stream: Stream, bite: number): number | null | -1 {
201
- // 1. If byte is end-of-stream and utf-8 bytes needed is not 0,
202
- // set utf-8 bytes needed to 0 and return error.
203
- if (bite === END_OF_STREAM && this.utf8BytesNeeded !== 0) {
204
- this.utf8BytesNeeded = 0;
205
- return decoderError(this.options.fatal);
142
+ function decodeUTF8General(
143
+ bytes: Uint8Array,
144
+ state: TextDecoderState,
145
+ stream: boolean,
146
+ output = '',
147
+ start = 0
148
+ ): string {
149
+ const { ignoreBOM } = state;
150
+ let { pending, bomSeen } = state;
151
+ let i = start;
152
+ const length = bytes.length;
153
+
154
+ if (pending.length > 0) {
155
+ const b0 = pending[0]!;
156
+ const bytesNeeded = b0 <= 0xdf ? 1 : b0 <= 0xef ? 2 : 3;
157
+ let codePoint = b0 & (bytesNeeded === 1 ? 0x1f : bytesNeeded === 2 ? 0x0f : 0x07);
158
+ let bytesSeen = 0;
159
+
160
+ for (let p = 1; p < pending.length; p++) {
161
+ codePoint = (codePoint << 6) | (pending[p]! & 0x3f);
162
+ bytesSeen++;
206
163
  }
207
164
 
208
- // 2. If byte is end-of-stream, return finished.
209
- if (bite === END_OF_STREAM) return FINISHED;
165
+ while (bytesSeen < bytesNeeded && i < length) {
166
+ const byte = bytes[i]!;
167
+ if (
168
+ (byte & 0xc0) !== 0x80 ||
169
+ (bytesSeen === 0 &&
170
+ ((b0 === 0xe0 && byte < 0xa0) ||
171
+ (b0 === 0xed && byte > 0x9f) ||
172
+ (b0 === 0xf0 && byte < 0x90) ||
173
+ (b0 === 0xf4 && byte > 0x8f)))
174
+ ) {
175
+ pending = EMPTY_BYTES;
176
+ output += decoderError(state);
177
+ bomSeen = true;
178
+ break;
179
+ }
180
+ codePoint = (codePoint << 6) | (byte & 0x3f);
181
+ bytesSeen++;
182
+ i++;
183
+ }
210
184
 
211
- // 3. If utf-8 bytes needed is 0, based on byte:
212
- if (this.utf8BytesNeeded === 0) {
213
- // 0x00 to 0x7F
214
- if (inRange(bite, 0x00, 0x7f)) {
215
- // Return a code point whose value is byte.
216
- return bite;
185
+ if (bytesSeen === bytesNeeded) {
186
+ pending = EMPTY_BYTES;
187
+ if (bomSeen || ignoreBOM || codePoint !== 0xfeff) {
188
+ if (codePoint <= 0xffff) {
189
+ output += String.fromCharCode(codePoint);
190
+ } else {
191
+ codePoint -= 0x10000;
192
+ output += String.fromCharCode((codePoint >> 10) + 0xd800, (codePoint & 0x3ff) + 0xdc00);
193
+ }
217
194
  }
195
+ bomSeen = true;
196
+ } else if (i === length) {
197
+ if (stream && i > 0) {
198
+ const nextPending = new Uint8Array(pending.length + i);
199
+ nextPending.set(pending);
200
+ nextPending.set(bytes.subarray(0, i), pending.length);
201
+ pending = nextPending;
202
+ } else if (!stream) {
203
+ pending = EMPTY_BYTES;
204
+ output += decoderError(state);
205
+ bomSeen = true;
206
+ }
207
+ }
218
208
 
219
- // 0xC2 to 0xDF
220
- else if (inRange(bite, 0xc2, 0xdf)) {
221
- // 1. Set utf-8 bytes needed to 1.
222
- this.utf8BytesNeeded = 1;
209
+ if (pending.length > 0) {
210
+ state.pending = pending;
211
+ state.bomSeen = bomSeen;
212
+ return output;
213
+ }
214
+ }
223
215
 
224
- // 2. Set UTF-8 code point to byte & 0x1F.
225
- this.utf8CodePoint = bite & 0x1f;
216
+ while (i < length) {
217
+ const b0 = bytes[i]!;
218
+ if (b0 < 0x80) {
219
+ bomSeen = true;
220
+ const directEnd = Math.min(i + 32, length);
221
+ do {
222
+ output += String.fromCharCode(bytes[i++]!);
223
+ } while (i < directEnd && bytes[i]! < 0x80);
224
+ if (i === directEnd && i < length && bytes[i]! < 0x80) {
225
+ const asciiStart = i;
226
+ do {
227
+ i++;
228
+ } while (i < length && bytes[i]! < 0x80);
229
+ output = appendASCII(output, bytes, asciiStart, i);
226
230
  }
231
+ continue;
232
+ }
227
233
 
228
- // 0xE0 to 0xEF
229
- else if (inRange(bite, 0xe0, 0xef)) {
230
- // 1. If byte is 0xE0, set utf-8 lower boundary to 0xA0.
231
- if (bite === 0xe0) this.utf8LowerBoundary = 0xa0;
232
- // 2. If byte is 0xED, set utf-8 upper boundary to 0x9F.
233
- if (bite === 0xed) this.utf8UpperBoundary = 0x9f;
234
- // 3. Set utf-8 bytes needed to 2.
235
- this.utf8BytesNeeded = 2;
236
- // 4. Set UTF-8 code point to byte & 0xF.
237
- this.utf8CodePoint = bite & 0xf;
234
+ let codePoint: number;
235
+ const remaining = length - i;
236
+ if (b0 >= 0xc2 && b0 <= 0xdf) {
237
+ if (remaining < 2) {
238
+ if (stream) {
239
+ pending = bytes.slice(i);
240
+ } else {
241
+ output += decoderError(state);
242
+ }
243
+ break;
238
244
  }
239
-
240
- // 0xF0 to 0xF4
241
- else if (inRange(bite, 0xf0, 0xf4)) {
242
- // 1. If byte is 0xF0, set utf-8 lower boundary to 0x90.
243
- if (bite === 0xf0) this.utf8LowerBoundary = 0x90;
244
- // 2. If byte is 0xF4, set utf-8 upper boundary to 0x8F.
245
- if (bite === 0xf4) this.utf8UpperBoundary = 0x8f;
246
- // 3. Set utf-8 bytes needed to 3.
247
- this.utf8BytesNeeded = 3;
248
- // 4. Set UTF-8 code point to byte & 0x7.
249
- this.utf8CodePoint = bite & 0x7;
245
+ const b1 = bytes[i + 1]!;
246
+ if ((b1 & 0xc0) !== 0x80) {
247
+ output += decoderError(state);
248
+ bomSeen = true;
249
+ i++;
250
+ continue;
250
251
  }
251
-
252
- // Otherwise
253
- else {
254
- // Return error.
255
- return decoderError(this.options.fatal);
252
+ codePoint = ((b0 & 0x1f) << 6) | (b1 & 0x3f);
253
+ i += 2;
254
+ } else if (b0 >= 0xe0 && b0 <= 0xef) {
255
+ if (remaining < 2) {
256
+ if (stream) {
257
+ pending = bytes.slice(i);
258
+ } else {
259
+ output += decoderError(state);
260
+ }
261
+ break;
256
262
  }
257
-
258
- // Return continue.
259
- return null;
263
+ const b1 = bytes[i + 1]!;
264
+ if ((b1 & 0xc0) !== 0x80 || (b0 === 0xe0 && b1 < 0xa0) || (b0 === 0xed && b1 > 0x9f)) {
265
+ output += decoderError(state);
266
+ bomSeen = true;
267
+ i++;
268
+ continue;
269
+ } else if (remaining < 3) {
270
+ if (stream) {
271
+ pending = bytes.slice(i);
272
+ } else {
273
+ output += decoderError(state);
274
+ }
275
+ break;
276
+ }
277
+ const b2 = bytes[i + 2]!;
278
+ if ((b2 & 0xc0) !== 0x80) {
279
+ output += decoderError(state);
280
+ bomSeen = true;
281
+ i += 2;
282
+ continue;
283
+ }
284
+ codePoint = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f);
285
+ i += 3;
286
+ } else if (b0 >= 0xf0 && b0 <= 0xf4) {
287
+ if (remaining < 2) {
288
+ if (stream) {
289
+ pending = bytes.slice(i);
290
+ } else {
291
+ output += decoderError(state);
292
+ }
293
+ break;
294
+ }
295
+ const b1 = bytes[i + 1]!;
296
+ if ((b1 & 0xc0) !== 0x80 || (b0 === 0xf0 && b1 < 0x90) || (b0 === 0xf4 && b1 > 0x8f)) {
297
+ output += decoderError(state);
298
+ bomSeen = true;
299
+ i++;
300
+ continue;
301
+ } else if (remaining < 3) {
302
+ if (stream) {
303
+ pending = bytes.slice(i);
304
+ } else {
305
+ output += decoderError(state);
306
+ }
307
+ break;
308
+ }
309
+ const b2 = bytes[i + 2]!;
310
+ if ((b2 & 0xc0) !== 0x80) {
311
+ output += decoderError(state);
312
+ bomSeen = true;
313
+ i += 2;
314
+ continue;
315
+ } else if (remaining < 4) {
316
+ if (stream) {
317
+ pending = bytes.slice(i);
318
+ } else {
319
+ output += decoderError(state);
320
+ }
321
+ break;
322
+ }
323
+ const b3 = bytes[i + 3]!;
324
+ if ((b3 & 0xc0) !== 0x80) {
325
+ output += decoderError(state);
326
+ bomSeen = true;
327
+ i += 3;
328
+ continue;
329
+ }
330
+ codePoint = ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
331
+ i += 4;
332
+ } else {
333
+ output += decoderError(state);
334
+ bomSeen = true;
335
+ i++;
336
+ continue;
260
337
  }
261
338
 
262
- // 4. If byte is not in the range utf-8 lower boundary to utf-8
263
- // upper boundary, inclusive, run these substeps:
264
- if (!inRange(bite, this.utf8LowerBoundary, this.utf8UpperBoundary)) {
265
- // 1. Set utf-8 code point, utf-8 bytes needed, and utf-8
266
- // bytes seen to 0, set utf-8 lower boundary to 0x80, and set
267
- // utf-8 upper boundary to 0xBF.
268
- this.utf8CodePoint = 0;
269
- this.utf8BytesNeeded = 0;
270
- this.utf8BytesSeen = 0;
271
- this.utf8LowerBoundary = 0x80;
272
- this.utf8UpperBoundary = 0xbf;
273
-
274
- // 2. Prepend byte to stream.
275
- stream.prepend(bite);
276
-
277
- // 3. Return error.
278
- return decoderError(this.options.fatal);
339
+ if (bomSeen || ignoreBOM || codePoint !== 0xfeff) {
340
+ if (codePoint <= 0xffff) {
341
+ output += String.fromCharCode(codePoint);
342
+ } else {
343
+ codePoint -= 0x10000;
344
+ output += String.fromCharCode((codePoint >> 10) + 0xd800, (codePoint & 0x3ff) + 0xdc00);
345
+ }
279
346
  }
347
+ bomSeen = true;
348
+ }
280
349
 
281
- // 5. Set utf-8 lower boundary to 0x80 and utf-8 upper boundary
282
- // to 0xBF.
283
- this.utf8LowerBoundary = 0x80;
284
- this.utf8UpperBoundary = 0xbf;
285
-
286
- // 6. Set UTF-8 code point to (UTF-8 code point << 6) | (byte &
287
- // 0x3F)
288
- this.utf8CodePoint = (this.utf8CodePoint << 6) | (bite & 0x3f);
289
-
290
- // 7. Increase utf-8 bytes seen by one.
291
- this.utf8BytesSeen += 1;
292
-
293
- // 8. If utf-8 bytes seen is not equal to utf-8 bytes needed,
294
- // continue.
295
- if (this.utf8BytesSeen !== this.utf8BytesNeeded) return null;
296
-
297
- // 9. Let code point be utf-8 code point.
298
- const code_point = this.utf8CodePoint;
299
-
300
- // 10. Set utf-8 code point, utf-8 bytes needed, and utf-8 bytes
301
- // seen to 0.
302
- this.utf8CodePoint = 0;
303
- this.utf8BytesNeeded = 0;
304
- this.utf8BytesSeen = 0;
350
+ state.pending = pending;
351
+ state.bomSeen = bomSeen;
352
+ return output;
353
+ }
305
354
 
306
- // 11. Return a code point whose value is code point.
307
- return code_point;
355
+ function decodeUTF8(bytes: Uint8Array, state: TextDecoderState, stream: boolean): string {
356
+ if (state.pending.length === 0) {
357
+ if (!stream && !state.bomSeen) {
358
+ const skipBOM =
359
+ !state.ignoreBOM && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0;
360
+ const result = decodeUTF8Fast(bytes, skipBOM);
361
+ if (typeof result === 'string') {
362
+ state.bomSeen = bytes.length > 0;
363
+ return result;
364
+ }
365
+ state.bomSeen = skipBOM > 0 || result.index > skipBOM;
366
+ return decodeUTF8General(bytes, state, false, result.output, result.index);
367
+ }
308
368
  }
369
+
370
+ return decodeUTF8General(bytes, state, stream);
309
371
  }
310
372
 
311
- // 8.1 Interface TextDecoder
312
373
  // @docsMissing
313
374
  export class TextDecoder {
314
- private _encoding: Encoding | null;
315
- private _ignoreBOM: boolean;
316
- private _errorMode: string;
317
- private _BOMseen: boolean = false;
318
- private _doNotFlush: boolean = false;
319
- private _decoder: UTF8Decoder | null = null;
375
+ private readonly _state: TextDecoderState;
320
376
 
321
- constructor(
322
- label: string = 'utf-8',
323
- options: {
324
- fatal?: boolean;
325
- ignoreBOM?: boolean;
326
- } = {}
327
- ) {
328
- if (options != null && typeof options !== 'object') {
377
+ constructor(label: string = 'utf-8', options: { fatal?: boolean; ignoreBOM?: boolean } = {}) {
378
+ if (options == null || (typeof options !== 'object' && typeof options !== 'function')) {
329
379
  throw new TypeError(
330
380
  'Second argument of TextDecoder must be undefined or an object, e.g. { fatal: true }'
331
381
  );
332
382
  }
333
-
334
- const normalizedLabel = String(label).trim().toLowerCase();
335
- const encoding = getEncoding(normalizedLabel);
336
- if (encoding === null || encoding.name === 'replacement') {
337
- throw new RangeError(`Unknown encoding: ${label} (normalized: ${normalizedLabel})`);
338
- }
339
-
340
- if (!DECODERS[encoding.name]) {
341
- throw new Error(`Decoder not present: ${encoding.name}`);
342
- }
343
-
344
- this._encoding = encoding;
345
- this._ignoreBOM = !!options.ignoreBOM;
346
- this._errorMode = options.fatal ? 'fatal' : 'replacement';
383
+ assertValidUTF8Label(label);
384
+ this._state = {
385
+ fatal: Boolean(options.fatal),
386
+ ignoreBOM: Boolean(options.ignoreBOM),
387
+ bomSeen: false,
388
+ pending: EMPTY_BYTES,
389
+ streaming: false,
390
+ };
347
391
  }
348
392
 
349
- // Getter methods for encoding, fatal, and ignoreBOM
350
393
  get encoding(): string {
351
- return this._encoding?.name.toLowerCase() ?? '';
394
+ return 'utf-8';
352
395
  }
353
396
 
354
397
  get fatal(): boolean {
355
- return this._errorMode === 'fatal';
398
+ return this._state.fatal;
356
399
  }
357
400
 
358
401
  get ignoreBOM(): boolean {
359
- return this._ignoreBOM;
402
+ return this._state.ignoreBOM;
360
403
  }
361
404
 
362
- decode(input?: ArrayBuffer | DataView, options: { stream?: boolean } = {}): string {
363
- const bytes = normalizeBytes(input);
364
-
365
- // 1. If the do not flush flag is unset, set decoder to a new
366
- // encoding's decoder, set stream to a new stream, and unset the
367
- // BOM seen flag.
368
- if (!this._doNotFlush) {
369
- this._decoder = DECODERS[this._encoding!.name]({
370
- fatal: this.fatal,
371
- });
372
- this._BOMseen = false;
373
- }
374
-
375
- // 2. If options's stream is true, set the do not flush flag, and
376
- // unset the do not flush flag otherwise.
377
- this._doNotFlush = Boolean(options['stream']);
378
-
379
- // 3. If input is given, push a copy of input to stream.
380
- // TODO: Align with spec algorithm - maintain stream on instance.
381
- const input_stream = new Stream(bytes);
382
-
383
- // 4. Let output be a new stream.
384
- const output: number[] = [];
385
-
386
- while (true) {
387
- const token = input_stream.read();
388
-
389
- if (token === END_OF_STREAM) break;
390
-
391
- const result = this._decoder!.handler(input_stream, token);
392
-
393
- if (result === FINISHED) break;
394
-
395
- if (result !== null) {
396
- output.push(result);
397
- }
405
+ decode(input?: ArrayBuffer | ArrayBufferView, options: { stream?: boolean } = {}): string {
406
+ if (options == null || (typeof options !== 'object' && typeof options !== 'function')) {
407
+ throw new TypeError('The options argument must be undefined or an object');
398
408
  }
399
-
400
- if (!this._doNotFlush) {
401
- do {
402
- const result = this._decoder!.handler(input_stream, input_stream.read());
403
- if (result === FINISHED) break;
404
- if (result === null) continue;
405
- if (Array.isArray(result)) output.push(...result);
406
- else output.push(result);
407
- } while (!input_stream.endOfStream());
408
- this._decoder = null;
409
+ const bytes = normalizeBytes(input);
410
+ const stream = Boolean(options.stream);
411
+ const state = this._state;
412
+ if (!state.streaming) {
413
+ state.bomSeen = false;
414
+ state.pending = EMPTY_BYTES;
409
415
  }
410
416
 
411
- return this.serializeStream(output);
412
- }
413
-
414
- // serializeStream method for converting code points to a string
415
- private serializeStream(stream: number[]): string {
416
- if (this._encoding!.name === 'UTF-8') {
417
- if (!this._ignoreBOM && !this._BOMseen && stream[0] === 0xfeff) {
418
- // If BOM is detected at the start of the stream and we're not ignoring it
419
- this._BOMseen = true;
420
- stream.shift(); // Remove the BOM
421
- } else if (stream.length > 0) {
422
- this._BOMseen = true;
423
- }
417
+ try {
418
+ const output = decodeUTF8(bytes, state, stream);
419
+ state.streaming = stream;
420
+ return output;
421
+ } catch (error) {
422
+ state.pending = EMPTY_BYTES;
423
+ state.streaming = false;
424
+ throw error;
424
425
  }
425
-
426
- // Convert the stream of code points to a string
427
- return codePointsToString(stream);
428
426
  }
429
427
  }
@@ -249,6 +249,8 @@ describe('TextDecoder', () => {
249
249
  chars.substring(TypeConstructor.BYTES_PER_ELEMENT, TypeConstructor.BYTES_PER_ELEMENT + 8)
250
250
  );
251
251
  });
252
+
253
+ expect(decoder.decode(new DataView(buffer, 2, 8))).toBe(chars.substring(2, 10));
252
254
  });
253
255
  });
254
256
 
@@ -274,6 +276,15 @@ describe('TextDecoder', () => {
274
276
  });
275
277
  });
276
278
 
279
+ it('accepts callable options dictionaries', () => {
280
+ const options = () => {};
281
+
282
+ // @ts-expect-error Web IDL dictionaries accept callable objects
283
+ expect(new TextDecoder('utf-8', options).fatal).toBe(false);
284
+ // @ts-expect-error Web IDL dictionaries accept callable objects
285
+ expect(new TextDecoder().decode(new Uint8Array([0x61]), options)).toBe('a');
286
+ });
287
+
277
288
  // https://github.com/inexorabletash/text-encoding/blob/3f330964c0e97e1ed344c2a3e963f4598610a7ad/test/test-misc.js#L383
278
289
  it('encode() called with falsy arguments (polyfill bindings)', () => {
279
290
  const encoder = new TextEncoder();
@@ -283,6 +294,137 @@ describe('TextDecoder', () => {
283
294
  expect([...encoder.encode(0)]).toEqual([48]);
284
295
  });
285
296
 
297
+ describe('indexed UTF-8 decoder', () => {
298
+ it('decodes valid sequences at each UTF-8 boundary', () => {
299
+ const bytes = new Uint8Array([
300
+ 0x00, 0x7f, 0xc2, 0x80, 0xdf, 0xbf, 0xe0, 0xa0, 0x80, 0xed, 0x9f, 0xbf, 0xee, 0x80, 0x80,
301
+ 0xf0, 0x90, 0x80, 0x80, 0xf4, 0x8f, 0xbf, 0xbf,
302
+ ]);
303
+
304
+ expect(new TextDecoder().decode(bytes)).toBe(
305
+ '\u0000\u007f\u0080\u07ff\u0800\ud7ff\ue000\u{10000}\u{10ffff}'
306
+ );
307
+ });
308
+
309
+ it('reads the stream option once for malformed input', () => {
310
+ let reads = 0;
311
+ const options = {
312
+ get stream() {
313
+ reads++;
314
+ return false;
315
+ },
316
+ };
317
+
318
+ expect(new TextDecoder().decode(new Uint8Array([0xff]), options)).toBe('\ufffd');
319
+ expect(reads).toBe(1);
320
+ });
321
+
322
+ it('handles incomplete and malformed sequences across every chunk boundary', () => {
323
+ const NativeTextDecoder = require('node:util').TextDecoder;
324
+ const cases = [
325
+ [0xef, 0xbb, 0xbf, 0x61],
326
+ [0xc2, 0xa2, 0xe6, 0xb0, 0xb4, 0xf0, 0x9d, 0x84, 0x9e],
327
+ [0xe0, 0x80, 0x80, 0x61],
328
+ [0xed, 0xa0, 0x80, 0x61],
329
+ [0xf4, 0x90, 0x80, 0x80, 0x61],
330
+ [0xe1, 0x80, 0x41, 0xc2],
331
+ [0xff, 0xef, 0xbb, 0xbf],
332
+ [0x61, 0xef, 0xbb, 0xbf],
333
+ ];
334
+
335
+ for (const input of cases) {
336
+ for (let split = 0; split <= input.length; split++) {
337
+ const decoder = new TextDecoder();
338
+ const nativeDecoder = new NativeTextDecoder();
339
+ const first = new Uint8Array(input.slice(0, split));
340
+ const second = new Uint8Array(input.slice(split));
341
+ const actual =
342
+ decoder.decode(first, { stream: true }) +
343
+ decoder.decode(second, { stream: true }) +
344
+ decoder.decode();
345
+ const expected =
346
+ nativeDecoder.decode(first, { stream: true }) +
347
+ nativeDecoder.decode(second, { stream: true }) +
348
+ nativeDecoder.decode();
349
+
350
+ expect(actual).toBe(expected);
351
+ }
352
+ }
353
+ });
354
+
355
+ it('decodes large ASCII and multi-byte inputs', () => {
356
+ const value = 'a'.repeat(9000) + '水𝄞'.repeat(9000);
357
+ const encoded = new TextEncoder().encode(value);
358
+
359
+ expect(new TextDecoder().decode(encoded)).toBe(value);
360
+ });
361
+
362
+ it.each([0, 1, 31, 32, 33, 4095, 4096, 4097, 8193])(
363
+ 'decodes %i ASCII bytes across conversion boundaries',
364
+ (length) => {
365
+ const bytes = new Uint8Array(length);
366
+ for (let i = 0; i < length; i++) bytes[i] = 32 + (i % 95);
367
+
368
+ const NativeTextDecoder = require('node:util').TextDecoder;
369
+ expect(new TextDecoder().decode(bytes)).toBe(new NativeTextDecoder().decode(bytes));
370
+ }
371
+ );
372
+
373
+ it('agrees with the native decoder for every two-byte input', () => {
374
+ const NativeTextDecoder = require('node:util').TextDecoder;
375
+ const bytes = new Uint8Array(2);
376
+ for (let first = 0; first <= 0xff; first++) {
377
+ bytes[0] = first;
378
+ for (let second = 0; second <= 0xff; second++) {
379
+ bytes[1] = second;
380
+ expect(new TextDecoder().decode(bytes)).toBe(new NativeTextDecoder().decode(bytes));
381
+ }
382
+ }
383
+ });
384
+
385
+ it('agrees with the native decoder for malformed inputs and all streaming splits', () => {
386
+ const NativeTextDecoder = require('node:util').TextDecoder;
387
+ let random = 0x12345678;
388
+ const nextByte = () => {
389
+ random = (Math.imul(random, 1664525) + 1013904223) >>> 0;
390
+ return random >>> 24;
391
+ };
392
+
393
+ for (let test = 0; test < 2000; test++) {
394
+ const input = new Uint8Array(test % 9);
395
+ for (let i = 0; i < input.length; i++) input[i] = nextByte();
396
+ expect(new TextDecoder().decode(input)).toBe(new NativeTextDecoder().decode(input));
397
+
398
+ for (let split = 0; split <= input.length; split++) {
399
+ const decoder = new TextDecoder();
400
+ const nativeDecoder = new NativeTextDecoder();
401
+ const actual =
402
+ decoder.decode(input.subarray(0, split), { stream: true }) +
403
+ decoder.decode(input.subarray(split), { stream: true }) +
404
+ decoder.decode();
405
+ const expected =
406
+ nativeDecoder.decode(input.subarray(0, split), { stream: true }) +
407
+ nativeDecoder.decode(input.subarray(split), { stream: true }) +
408
+ nativeDecoder.decode();
409
+ expect(actual).toBe(expected);
410
+ }
411
+ }
412
+ });
413
+
414
+ it('starts a fresh BOM session after a completed decode', () => {
415
+ const decoder = new TextDecoder();
416
+ expect(decoder.decode(new Uint8Array([0x61]))).toBe('a');
417
+ expect(decoder.decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x62]))).toBe('b');
418
+ });
419
+
420
+ it('recovers cleanly after a fatal streaming error', () => {
421
+ const decoder = new TextDecoder('utf-8', { fatal: true });
422
+ expect(decoder.decode(new Uint8Array([0xe2]), { stream: true })).toBe('');
423
+ expect(() => decoder.decode(new Uint8Array([0x41]), { stream: true })).toThrow(TypeError);
424
+ expect(decoder.decode(new Uint8Array([0x42]))).toBe('B');
425
+ });
426
+ });
427
+
286
428
  // https://github.com/inexorabletash/text-encoding/blob/master/test/test-utf.js
287
429
  describe('UTF-8 - Encode/Decode - reference sample', () => {
288
430
  // z, cent, CJK water, G-Clef, Private-use character
package/template.tgz CHANGED
Binary file