1
1
import {
2
+ CONTENT_HASH_OFFSET ,
2
3
CONTENT_HASH_SIZE ,
3
4
ENTRY_SIZE ,
4
5
HEADER_SIZE ,
6
+ PATH_HASH_OFFSET ,
5
7
PATH_HASH_SIZE ,
6
8
} from "../../utils/constants" ;
7
9
8
10
export class AssetsManifest {
9
- private data : ArrayBuffer ;
11
+ private data : Uint8Array ;
10
12
11
13
constructor ( data : ArrayBuffer ) {
12
- this . data = data ;
14
+ this . data = new Uint8Array ( data ) ;
13
15
}
14
16
15
17
async get ( pathname : string ) {
16
18
const pathHash = await hashPath ( pathname ) ;
17
- const entry = binarySearch (
18
- new Uint8Array ( this . data , HEADER_SIZE ) ,
19
- pathHash
20
- ) ;
21
- return entry ? contentHashToKey ( entry ) : null ;
19
+ const entry = binarySearch ( this . data , pathHash ) ;
20
+ return entry ? Uint8ToHexString ( entry ) : null ;
22
21
}
23
22
}
24
23
@@ -32,41 +31,103 @@ export const hashPath = async (path: string) => {
32
31
return new Uint8Array ( hashBuffer , 0 , PATH_HASH_SIZE ) ;
33
32
} ;
34
33
34
+ /**
35
+ * Search for an entry with the given hash path.
36
+ *
37
+ * @param manifest the manifest bytes
38
+ * @param pathHash the path hash to find in the manifest
39
+ * @returns The content hash when the entry is found and `false` otherwise
40
+ */
35
41
export const binarySearch = (
36
- arr : Uint8Array ,
37
- searchValue : Uint8Array
42
+ manifest : Uint8Array ,
43
+ pathHash : Uint8Array
38
44
) : Uint8Array | false => {
39
- if ( arr . byteLength === 0 ) {
40
- return false ;
41
- }
42
- const offset =
43
- arr . byteOffset + ( ( arr . byteLength / ENTRY_SIZE ) >> 1 ) * ENTRY_SIZE ;
44
- const current = new Uint8Array ( arr . buffer , offset , PATH_HASH_SIZE ) ;
45
- if ( current . byteLength !== searchValue . byteLength ) {
45
+ if ( pathHash . byteLength !== PATH_HASH_SIZE ) {
46
46
throw new TypeError (
47
- " Search value and current value are of different lengths"
47
+ ` Search value should have a length of ${ PATH_HASH_SIZE } `
48
48
) ;
49
49
}
50
- const cmp = compare ( searchValue , current ) ;
51
- if ( cmp < 0 ) {
52
- const nextOffset = arr . byteOffset ;
53
- const nextLength = offset - arr . byteOffset ;
54
- return binarySearch (
55
- new Uint8Array ( arr . buffer , nextOffset , nextLength ) ,
56
- searchValue
57
- ) ;
58
- } else if ( cmp > 0 ) {
59
- const nextOffset = offset + ENTRY_SIZE ;
60
- const nextLength = arr . buffer . byteLength - offset - ENTRY_SIZE ;
61
- return binarySearch (
62
- new Uint8Array ( arr . buffer , nextOffset , nextLength ) ,
63
- searchValue
50
+
51
+ const numberOfEntries = ( manifest . byteLength - HEADER_SIZE ) / ENTRY_SIZE ;
52
+
53
+ if ( numberOfEntries === 0 ) {
54
+ return false ;
55
+ }
56
+
57
+ let lowIndex = 0 ;
58
+ let highIndex = numberOfEntries - 1 ;
59
+
60
+ while ( lowIndex <= highIndex ) {
61
+ const middleIndex = ( lowIndex + highIndex ) >> 1 ;
62
+
63
+ const cmp = comparePathHashWithEntry ( pathHash , manifest , middleIndex ) ;
64
+
65
+ if ( cmp < 0 ) {
66
+ highIndex = middleIndex - 1 ;
67
+ continue ;
68
+ }
69
+
70
+ if ( cmp > 0 ) {
71
+ lowIndex = middleIndex + 1 ;
72
+ continue ;
73
+ }
74
+
75
+ return new Uint8Array (
76
+ manifest . buffer ,
77
+ HEADER_SIZE + middleIndex * ENTRY_SIZE + CONTENT_HASH_OFFSET ,
78
+ CONTENT_HASH_SIZE
64
79
) ;
65
- } else {
66
- return new Uint8Array ( arr . buffer , offset , ENTRY_SIZE ) ;
67
80
}
81
+
82
+ return false ;
68
83
} ;
69
84
85
+ /**
86
+ * Compares a search value with a path hash in the manifest
87
+ *
88
+ * @param searchValue a `Uint8Array` of size `PATH_HASH_SIZE`
89
+ * @param manifest the manifest bytes
90
+ * @param entryIndex the index in the manifest of the entry to compare
91
+ */
92
+ function comparePathHashWithEntry (
93
+ searchValue : Uint8Array ,
94
+ manifest : Uint8Array ,
95
+ entryIndex : number
96
+ ) {
97
+ let pathHashOffset = HEADER_SIZE + entryIndex * ENTRY_SIZE + PATH_HASH_OFFSET ;
98
+ for ( let offset = 0 ; offset < PATH_HASH_SIZE ; offset ++ , pathHashOffset ++ ) {
99
+ // We know that both values could not be undefined
100
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
101
+ const s = searchValue [ offset ] ! ;
102
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103
+ const e = manifest [ pathHashOffset ] ! ;
104
+ if ( s < e ) {
105
+ return - 1 ;
106
+ }
107
+ if ( s > e ) {
108
+ return 1 ;
109
+ }
110
+ }
111
+
112
+ return 0 ;
113
+ }
114
+
115
+ /**
116
+ * Converts an Uint8Array to an hex string
117
+ *
118
+ * @param array The content hash
119
+ * @returns padded hex string
120
+ */
121
+ const Uint8ToHexString = ( array : Uint8Array ) => {
122
+ return [ ...array ] . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
123
+ } ;
124
+
125
+ /**
126
+ * Compare two Uint8Array values
127
+ * @param a First array
128
+ * @param b Second array
129
+ * @returns -1 if a < b, 1 if a > b, 0 if equal
130
+ */
70
131
export const compare = ( a : Uint8Array , b : Uint8Array ) => {
71
132
if ( a . byteLength < b . byteLength ) {
72
133
return - 1 ;
@@ -87,11 +148,3 @@ export const compare = (a: Uint8Array, b: Uint8Array) => {
87
148
88
149
return 0 ;
89
150
} ;
90
-
91
- const contentHashToKey = ( buffer : Uint8Array ) => {
92
- const contentHash = buffer . slice (
93
- PATH_HASH_SIZE ,
94
- PATH_HASH_SIZE + CONTENT_HASH_SIZE
95
- ) ;
96
- return [ ...contentHash ] . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
97
- } ;
0 commit comments