s3mini 0.7.0 → 0.8.0

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.
package/src/S3.ts CHANGED
@@ -1,8 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  import * as C from './consts.js';
4
+ import {
5
+ hexFromBuffer,
6
+ sha256,
7
+ hmac,
8
+ uriResourceEscape,
9
+ getByteSize,
10
+ sanitizeETag,
11
+ uriEscape,
12
+ parseXml,
13
+ escapeXml,
14
+ base64FromBuffer,
15
+ extractErrCode,
16
+ S3NetworkError,
17
+ S3ServiceError,
18
+ } from './utils.js';
4
19
  import type * as IT from './types.js';
5
- import * as U from './utils.js';
6
20
 
7
21
  /**
8
22
  * S3 class for interacting with S3-compatible object storage services.
@@ -11,7 +25,7 @@ import * as U from './utils.js';
11
25
  *
12
26
  * @class
13
27
  * @example
14
- * const s3 = new CoreS3({
28
+ * const s3 = new S3mini({
15
29
  * accessKeyId: 'your-access-key',
16
30
  * secretAccessKey: 'your-secret-key',
17
31
  * endpoint: 'https://your-s3-endpoint.com/bucket-name',
@@ -43,15 +57,15 @@ class S3mini {
43
57
  * @param {typeof fetch} [config.fetch=globalThis.fetch] - Custom fetch implementation to use for HTTP requests.
44
58
  * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
45
59
  */
46
- readonly accessKeyId: string;
47
- readonly secretAccessKey: string;
60
+ readonly #accessKeyId: string;
61
+ readonly #secretAccessKey: string;
48
62
  readonly endpoint: URL;
49
63
  readonly region: string;
50
64
  readonly bucketName: string;
51
65
  readonly requestSizeInBytes: number;
52
66
  readonly requestAbortTimeout?: number;
53
67
  readonly logger?: IT.Logger;
54
- readonly fetch: typeof fetch;
68
+ readonly _fetch: typeof fetch;
55
69
  private signingKeyDate?: string;
56
70
  private signingKey?: ArrayBuffer;
57
71
 
@@ -66,15 +80,15 @@ class S3mini {
66
80
  fetch = globalThis.fetch,
67
81
  }: IT.S3Config) {
68
82
  this._validateConstructorParams(accessKeyId, secretAccessKey, endpoint);
69
- this.accessKeyId = accessKeyId;
70
- this.secretAccessKey = secretAccessKey;
83
+ this.#accessKeyId = accessKeyId;
84
+ this.#secretAccessKey = secretAccessKey;
71
85
  this.endpoint = new URL(this._ensureValidUrl(endpoint));
72
86
  this.region = region;
73
87
  this.bucketName = this._extractBucketName();
74
88
  this.requestSizeInBytes = requestSizeInBytes;
75
89
  this.requestAbortTimeout = requestAbortTimeout;
76
90
  this.logger = logger;
77
- this.fetch = fetch;
91
+ this._fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => fetch(input, init);
78
92
  }
79
93
 
80
94
  private _sanitize(obj: unknown): unknown {
@@ -120,7 +134,7 @@ class S3mini {
120
134
  region: this.region,
121
135
  endpoint: this.endpoint.toString(),
122
136
  // Only include the first few characters of the access key, if it exists
123
- accessKeyId: this.accessKeyId ? `${this.accessKeyId.substring(0, 4)}...` : undefined,
137
+ accessKeyId: this.#accessKeyId ? `${this.#accessKeyId.substring(0, 4)}...` : undefined,
124
138
  }),
125
139
  };
126
140
 
@@ -292,14 +306,14 @@ class S3mini {
292
306
  }
293
307
  }
294
308
  const canonicalRequest = `${method}\n${url.pathname}\n${this._buildCanonicalQueryString(query)}\n${canonicalHeaders}\n\n${signedHeaders}\n${C.UNSIGNED_PAYLOAD}`;
295
- const stringToSign = `${C.AWS_ALGORITHM}\n${fullDatetime}\n${credentialScope}\n${U.hexFromBuffer(await U.sha256(canonicalRequest))}`;
309
+ const stringToSign = `${C.AWS_ALGORITHM}\n${fullDatetime}\n${credentialScope}\n${hexFromBuffer(await sha256(canonicalRequest))}`;
296
310
  if (shortDatetime !== this.signingKeyDate || !this.signingKey) {
297
311
  this.signingKeyDate = shortDatetime;
298
312
  this.signingKey = await this._getSignatureKey(shortDatetime);
299
313
  }
300
- const signature = U.hexFromBuffer(await U.hmac(this.signingKey, stringToSign));
314
+ const signature = hexFromBuffer(await hmac(this.signingKey, stringToSign));
301
315
  headers[C.HEADER_AUTHORIZATION] =
302
- `${C.AWS_ALGORITHM} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
316
+ `${C.AWS_ALGORITHM} Credential=${this.#accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
303
317
  return { url: url.toString(), headers };
304
318
  }
305
319
 
@@ -335,7 +349,7 @@ class S3mini {
335
349
  ...conditionalHeaders,
336
350
  };
337
351
 
338
- const encodedKey = key ? U.uriResourceEscape(key) : '';
352
+ const encodedKey = key ? uriResourceEscape(key) : '';
339
353
  const { url, headers: signedHeaders } = await this._sign(method, encodedKey, filteredOpts, baseHeaders);
340
354
  if (Object.keys(query).length > 0) {
341
355
  withQuery = true; // append query string to signed URL
@@ -360,7 +374,7 @@ class S3mini {
360
374
  * const cleanEtag = s3.sanitizeETag('"abc123"'); // Returns: 'abc123'
361
375
  */
362
376
  public sanitizeETag(etag: string): string {
363
- return U.sanitizeETag(etag);
377
+ return sanitizeETag(etag);
364
378
  }
365
379
 
366
380
  /**
@@ -376,7 +390,7 @@ class S3mini {
376
390
  `;
377
391
  const headers = {
378
392
  [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
379
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(xmlBody),
393
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
380
394
  };
381
395
  const res = await this._signedRequest('PUT', '', {
382
396
  body: xmlBody,
@@ -457,7 +471,7 @@ class S3mini {
457
471
  this._checkPrefix(prefix);
458
472
  this._checkOpts(opts);
459
473
 
460
- const keyPath = delimiter === '/' ? delimiter : U.uriEscape(delimiter);
474
+ const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
461
475
  const unlimited = !(maxKeys && maxKeys > 0);
462
476
  let remaining = unlimited ? Infinity : maxKeys;
463
477
  let token: string | undefined;
@@ -482,6 +496,47 @@ class S3mini {
482
496
  return all;
483
497
  }
484
498
 
499
+ /**
500
+ * Lists objects in the bucket with optional filtering and pagination using a continuation token.
501
+ * This method retrieves objects matching the criteria (paginated like listObjectsV2).
502
+ * @param {string} [delimiter='/'] - The delimiter to use for grouping objects.
503
+ * @param {string} [prefix=''] - The prefix to filter objects by.
504
+ * @param {number} [maxKeys] - The maximum number of keys to return. Uses a default value of 100.
505
+ * @param {string} [nextContinuationToken] - The nextContinuationToken to continue previous results. If not provided, starts from the beginning.
506
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
507
+ * @returns {Promise<{objects: IT.ListObject[] | null; nextContinuationToken?: string } | undefined | null>} A promise that resolves to an array of objects or null if the bucket is empty, along with nextContinuationToken if there are more reccords.
508
+ * @example
509
+ * // List all objects
510
+ * const { objects, nextContinuationToken } = await s3.listObjectsPaged();
511
+ *
512
+ * // List 200 objects with prefix
513
+ * const photos = await s3.listObjectsPaged('/', 'photos/', 200, "token...");
514
+ */
515
+ public async listObjectsPaged(
516
+ delimiter: string = '/',
517
+ prefix: string = '',
518
+ maxKeys: number = 100,
519
+ nextContinuationToken?: string,
520
+ opts: Record<string, unknown> = {},
521
+ ): Promise<{ objects: IT.ListObject[] | null; nextContinuationToken?: string } | undefined | null> {
522
+ this._checkDelimiter(delimiter);
523
+ this._checkPrefix(prefix);
524
+ this._checkOpts(opts);
525
+
526
+ const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
527
+ let token: string | undefined = nextContinuationToken;
528
+ const all: IT.ListObject[] = [];
529
+
530
+ const batchResult = await this._fetchObjectBatch(keyPath, prefix, maxKeys, token, opts);
531
+ if (batchResult === null) {
532
+ return null; // 404 - bucket not found
533
+ }
534
+
535
+ all.push(...batchResult.objects);
536
+ token = batchResult.continuationToken;
537
+ return { objects: all, nextContinuationToken: token };
538
+ }
539
+
485
540
  private async _fetchObjectBatch(
486
541
  keyPath: string,
487
542
  prefix: string,
@@ -546,7 +601,7 @@ class S3mini {
546
601
  objects: IT.ListObject[];
547
602
  continuationToken?: string;
548
603
  } {
549
- const raw = U.parseXml(xmlText) as Record<string, unknown>;
604
+ const raw = parseXml(xmlText) as Record<string, unknown>;
550
605
 
551
606
  if (typeof raw !== 'object' || !raw || 'error' in raw) {
552
607
  this._log('error', `${C.ERROR_PREFIX}Unexpected listObjects response shape: ${JSON.stringify(raw)}`);
@@ -604,7 +659,7 @@ class S3mini {
604
659
  this._checkOpts(opts);
605
660
 
606
661
  const query = { uploads: '', ...opts };
607
- const keyPath = delimiter === '/' ? delimiter : U.uriEscape(delimiter);
662
+ const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
608
663
 
609
664
  const res = await this._signedRequest(method, keyPath, {
610
665
  query,
@@ -618,7 +673,7 @@ class S3mini {
618
673
  // etag: res.headers.get(C.HEADER_ETAG) ?? '',
619
674
  // };
620
675
  // }
621
- const raw = U.parseXml(await res.text()) as unknown;
676
+ const raw = parseXml(await res.text()) as unknown;
622
677
  if (typeof raw !== 'object' || raw === null) {
623
678
  throw new Error(`${C.ERROR_PREFIX}Unexpected listMultipartUploads response shape`);
624
679
  }
@@ -647,10 +702,11 @@ class S3mini {
647
702
  tolerated: [200, 404, 412, 304],
648
703
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
649
704
  });
650
- if ([404, 412, 304].includes(res.status)) {
651
- return null;
705
+ const s = res.status;
706
+ if (s === 200) {
707
+ return res.text();
652
708
  }
653
- return res.text();
709
+ return null;
654
710
  }
655
711
 
656
712
  /**
@@ -671,10 +727,10 @@ class S3mini {
671
727
  tolerated: [200, 404, 412, 304],
672
728
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
673
729
  });
674
- if ([404, 412, 304].includes(res.status)) {
675
- return null;
730
+ if (res.status === 200) {
731
+ return res;
676
732
  }
677
- return res;
733
+ return null;
678
734
  }
679
735
 
680
736
  /**
@@ -695,10 +751,10 @@ class S3mini {
695
751
  tolerated: [200, 404, 412, 304],
696
752
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
697
753
  });
698
- if ([404, 412, 304].includes(res.status)) {
699
- return null;
754
+ if (res.status === 200) {
755
+ return res.arrayBuffer();
700
756
  }
701
- return res.arrayBuffer();
757
+ return null;
702
758
  }
703
759
 
704
760
  /**
@@ -719,10 +775,10 @@ class S3mini {
719
775
  tolerated: [200, 404, 412, 304],
720
776
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
721
777
  });
722
- if ([404, 412, 304].includes(res.status)) {
723
- return null;
778
+ if (res.status === 200) {
779
+ return res.json() as Promise<T>;
724
780
  }
725
- return res.json() as Promise<T>;
781
+ return null;
726
782
  }
727
783
 
728
784
  /**
@@ -744,8 +800,8 @@ class S3mini {
744
800
  tolerated: [200, 404, 412, 304],
745
801
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
746
802
  });
747
-
748
- if ([404, 412, 304].includes(res.status)) {
803
+ const s = res.status;
804
+ if (s === 404 || s === 412 || s === 304) {
749
805
  return { etag: null, data: null };
750
806
  }
751
807
 
@@ -753,7 +809,7 @@ class S3mini {
753
809
  if (!etag) {
754
810
  throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
755
811
  }
756
- return { etag: U.sanitizeETag(etag), data: await res.arrayBuffer() };
812
+ return { etag: sanitizeETag(etag), data: await res.arrayBuffer() };
757
813
  } catch (err) {
758
814
  this._log('error', `Error getting object ${key} with ETag: ${String(err)}`);
759
815
  throw err;
@@ -867,7 +923,7 @@ class S3mini {
867
923
  throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
868
924
  }
869
925
 
870
- return U.sanitizeETag(etag);
926
+ return sanitizeETag(etag);
871
927
  }
872
928
 
873
929
  /**
@@ -897,7 +953,7 @@ class S3mini {
897
953
  return this._signedRequest('PUT', key, {
898
954
  body: this._validateData(data),
899
955
  headers: {
900
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(data),
956
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
901
957
  [C.HEADER_CONTENT_TYPE]: fileType,
902
958
  ...additionalHeaders,
903
959
  ...ssecHeaders,
@@ -935,7 +991,7 @@ class S3mini {
935
991
  headers,
936
992
  withQuery: true,
937
993
  });
938
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
994
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
939
995
 
940
996
  if (parsed && typeof parsed === 'object') {
941
997
  // Check for both cases of InitiateMultipartUploadResult
@@ -990,12 +1046,12 @@ class S3mini {
990
1046
  query,
991
1047
  body,
992
1048
  headers: {
993
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(data),
1049
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
994
1050
  ...ssecHeaders,
995
1051
  },
996
1052
  });
997
1053
 
998
- return { partNumber, etag: U.sanitizeETag(res.headers.get('etag') || '') };
1054
+ return { partNumber, etag: sanitizeETag(res.headers.get('etag') || '') };
999
1055
  }
1000
1056
 
1001
1057
  /**
@@ -1025,7 +1081,7 @@ class S3mini {
1025
1081
  const xmlBody = this._buildCompleteMultipartUploadXml(parts);
1026
1082
  const headers = {
1027
1083
  [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
1028
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(xmlBody),
1084
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
1029
1085
  };
1030
1086
 
1031
1087
  const res = await this._signedRequest('POST', key, {
@@ -1035,7 +1091,7 @@ class S3mini {
1035
1091
  withQuery: true,
1036
1092
  });
1037
1093
 
1038
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1094
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
1039
1095
  if (parsed && typeof parsed === 'object') {
1040
1096
  // Check for both cases
1041
1097
  const result = parsed.completeMultipartUploadResult || parsed.CompleteMultipartUploadResult || parsed;
@@ -1048,7 +1104,7 @@ class S3mini {
1048
1104
  if (etag && typeof etag === 'string') {
1049
1105
  return {
1050
1106
  ...resultObj,
1051
- etag: U.sanitizeETag(etag),
1107
+ etag: sanitizeETag(etag),
1052
1108
  } as IT.CompleteMultipartUploadResult;
1053
1109
  }
1054
1110
 
@@ -1089,7 +1145,7 @@ class S3mini {
1089
1145
  headers,
1090
1146
  withQuery: true,
1091
1147
  });
1092
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1148
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
1093
1149
  if (
1094
1150
  parsed &&
1095
1151
  'error' in parsed &&
@@ -1220,7 +1276,7 @@ class S3mini {
1220
1276
  this._checkKey(sourceKey);
1221
1277
  this._checkKey(destinationKey);
1222
1278
 
1223
- const copySource = `/${this.bucketName}/${U.uriEscape(sourceKey)}`;
1279
+ const copySource = `/${this.bucketName}/${uriEscape(sourceKey)}`;
1224
1280
 
1225
1281
  return this._executeCopyOperation(destinationKey, copySource, options);
1226
1282
  }
@@ -1312,7 +1368,7 @@ class S3mini {
1312
1368
  }
1313
1369
 
1314
1370
  private _parseCopyObjectResponse(xmlText: string): IT.CopyObjectResult {
1315
- const parsed = U.parseXml(xmlText) as Record<string, unknown>;
1371
+ const parsed = parseXml(xmlText) as Record<string, unknown>;
1316
1372
  if (!parsed || typeof parsed !== 'object') {
1317
1373
  throw new Error(`${C.ERROR_PREFIX}Unexpected copyObject response format`);
1318
1374
  }
@@ -1323,7 +1379,7 @@ class S3mini {
1323
1379
  throw new Error(`${C.ERROR_PREFIX}ETag not found in copyObject response`);
1324
1380
  }
1325
1381
  return {
1326
- etag: U.sanitizeETag(etag),
1382
+ etag: sanitizeETag(etag),
1327
1383
  lastModified: lastModified ? new Date(lastModified as string) : undefined,
1328
1384
  };
1329
1385
  }
@@ -1340,13 +1396,13 @@ class S3mini {
1340
1396
  }
1341
1397
 
1342
1398
  private async _deleteObjectsProcess(keys: string[]): Promise<boolean[]> {
1343
- const objectsXml = keys.map(key => `<Object><Key>${U.escapeXml(key)}</Key></Object>`).join('');
1399
+ const objectsXml = keys.map(key => `<Object><Key>${escapeXml(key)}</Key></Object>`).join('');
1344
1400
  const xmlBody = '<Delete>' + objectsXml + '</Delete>';
1345
1401
  const query = { delete: '' };
1346
- const sha256base64 = U.base64FromBuffer(await U.sha256(xmlBody));
1402
+ const sha256base64 = base64FromBuffer(await sha256(xmlBody));
1347
1403
  const headers = {
1348
1404
  [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
1349
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(xmlBody),
1405
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
1350
1406
  [C.HEADER_AMZ_CHECKSUM_SHA256]: sha256base64,
1351
1407
  };
1352
1408
 
@@ -1356,7 +1412,7 @@ class S3mini {
1356
1412
  headers,
1357
1413
  withQuery: true,
1358
1414
  });
1359
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1415
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
1360
1416
  if (!parsed || typeof parsed !== 'object') {
1361
1417
  throw new Error(`${C.ERROR_PREFIX}Failed to delete objects: ${JSON.stringify(parsed)}`);
1362
1418
  }
@@ -1441,10 +1497,10 @@ class S3mini {
1441
1497
  ): Promise<Response> {
1442
1498
  this._log('info', `Sending ${method} request to ${url}`, `headers: ${JSON.stringify(headers)}`);
1443
1499
  try {
1444
- const res = await this.fetch(url, {
1500
+ const res = await this._fetch(url, {
1445
1501
  method,
1446
1502
  headers,
1447
- body: ['GET', 'HEAD'].includes(method) ? undefined : body,
1503
+ body: method === 'GET' || method === 'HEAD' ? undefined : body,
1448
1504
  signal: this.requestAbortTimeout ? AbortSignal.timeout(this.requestAbortTimeout) : undefined,
1449
1505
  });
1450
1506
  this._log('info', `Response status: ${res.status}, tolerated: ${toleratedStatusCodes.join(',')}`);
@@ -1454,9 +1510,9 @@ class S3mini {
1454
1510
  await this._handleErrorResponse(res);
1455
1511
  return res;
1456
1512
  } catch (err: unknown) {
1457
- const code = U.extractErrCode(err);
1513
+ const code = extractErrCode(err);
1458
1514
  if (code && ['ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'ECONNREFUSED'].includes(code)) {
1459
- throw new U.S3NetworkError(`S3 network error: ${code}`, code, err);
1515
+ throw new S3NetworkError(`S3 network error: ${code}`, code, err);
1460
1516
  }
1461
1517
  throw err;
1462
1518
  }
@@ -1466,7 +1522,7 @@ class S3mini {
1466
1522
  if (headers.get('content-type') !== 'application/xml') {
1467
1523
  return {};
1468
1524
  }
1469
- const parsedBody = U.parseXml(body);
1525
+ const parsedBody = parseXml(body);
1470
1526
  if (
1471
1527
  !parsedBody ||
1472
1528
  typeof parsedBody !== 'object' ||
@@ -1492,7 +1548,7 @@ class S3mini {
1492
1548
  'error',
1493
1549
  `${C.ERROR_PREFIX}Request failed with status ${res.status}: ${svcCode} - ${errorMessage},err body: ${errorBody}`,
1494
1550
  );
1495
- throw new U.S3ServiceError(`S3 returned ${res.status} – ${svcCode}`, res.status, svcCode, errorBody);
1551
+ throw new S3ServiceError(`S3 returned ${res.status} – ${svcCode}`, res.status, svcCode, errorBody);
1496
1552
  }
1497
1553
 
1498
1554
  private _buildCanonicalQueryString(queryParams: Record<string, unknown>): string {
@@ -1505,10 +1561,10 @@ class S3mini {
1505
1561
  .join('&');
1506
1562
  }
1507
1563
  private async _getSignatureKey(dateStamp: string): Promise<ArrayBuffer> {
1508
- const kDate = await U.hmac(`AWS4${this.secretAccessKey}`, dateStamp);
1509
- const kRegion = await U.hmac(kDate, this.region);
1510
- const kService = await U.hmac(kRegion, C.S3_SERVICE);
1511
- return await U.hmac(kService, C.AWS_REQUEST_TYPE);
1564
+ const kDate = await hmac(`AWS4${this.#secretAccessKey}`, dateStamp);
1565
+ const kRegion = await hmac(kDate, this.region);
1566
+ const kService = await hmac(kRegion, C.S3_SERVICE);
1567
+ return await hmac(kService, C.AWS_REQUEST_TYPE);
1512
1568
  }
1513
1569
  }
1514
1570
 
package/src/utils.ts CHANGED
@@ -3,7 +3,7 @@ import type { XmlValue, XmlMap, ListBucketResponse, ErrorWithCode } from './type
3
3
 
4
4
  const ENCODR = new TextEncoder();
5
5
  const chunkSize = 0x8000; // 32KB chunks
6
- const HEXS = '0123456789abcdef';
6
+ const HEX_CHARS = new Uint8Array([48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102]);
7
7
 
8
8
  export const getByteSize = (data: unknown): number => {
9
9
  if (typeof data === 'string') {
@@ -23,13 +23,15 @@ export const getByteSize = (data: unknown): number => {
23
23
  * @param {ArrayBuffer} buffer The raw bytes.
24
24
  * @returns {string} Hexadecimal string
25
25
  */
26
+
26
27
  export const hexFromBuffer = (buffer: ArrayBuffer): string => {
27
28
  const bytes = new Uint8Array(buffer);
28
- let hex = '';
29
- for (const byte of bytes) {
30
- hex += HEXS[byte >> 4]! + HEXS[byte & 0x0f]!;
29
+ const hex = new Uint8Array(bytes.length * 2);
30
+ for (let i = 0, j = 0; i < bytes.length; i++) {
31
+ hex[j++] = HEX_CHARS[bytes[i]! >> 4]!;
32
+ hex[j++] = HEX_CHARS[bytes[i]! & 0x0f]!;
31
33
  }
32
- return hex;
34
+ return String.fromCharCode(...hex);
33
35
  };
34
36
 
35
37
  /**
@@ -153,6 +155,38 @@ export const parseXml = (input: string): XmlValue => {
153
155
  return Object.keys(result).length > 0 ? result : unescapeXml(xmlContent.trim());
154
156
  };
155
157
 
158
+ // export const parseXml = (input: string): XmlValue => {
159
+ // const xml = input.replace(/<\?xml[^?]*\?>\s*/, '');
160
+ // const result: XmlMap = {};
161
+ // let i = 0;
162
+ // const len = xml.length;
163
+
164
+ // while (i < len) {
165
+ // const tagStart = xml.indexOf('<', i);
166
+ // if (tagStart === -1 || xml[tagStart + 1] === '/') {
167
+ // break;
168
+ // }
169
+
170
+ // const tagEnd = xml.indexOf('>', tagStart);
171
+ // const tag = xml.slice(tagStart + 1, tagEnd);
172
+ // const closeTag = `</${tag}>`;
173
+ // const closeIdx = xml.indexOf(closeTag, tagEnd);
174
+
175
+ // if (closeIdx === -1) {
176
+ // i = tagEnd + 1;
177
+ // continue;
178
+ // }
179
+
180
+ // const inner = xml.slice(tagEnd + 1, closeIdx);
181
+ // const node = inner.includes('<') ? parseXml(inner) : unescapeXml(inner);
182
+
183
+ // const cur = result[tag];
184
+ // result[tag] = cur === undefined ? node : Array.isArray(cur) ? [...cur, node] : [cur, node];
185
+ // i = closeIdx + closeTag.length;
186
+ // }
187
+ // return Object.keys(result).length ? result : unescapeXml(xml.trim());
188
+ // };
189
+
156
190
  /**
157
191
  * Encode a character as a URI percent-encoded hex value
158
192
  * @param c Character to encode