braid-text 0.2.70 → 0.2.71
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/index.js +48 -8
- package/package.json +3 -2
- package/test/tests.js +120 -0
package/index.js
CHANGED
|
@@ -1068,6 +1068,11 @@ function create_braid_text() {
|
|
|
1068
1068
|
}
|
|
1069
1069
|
}
|
|
1070
1070
|
|
|
1071
|
+
// Populate key_to_filename mapping from existing files
|
|
1072
|
+
var files = (await fs.promises.readdir(braid_text.db_folder))
|
|
1073
|
+
.filter(x => /\.\d+$/.test(x))
|
|
1074
|
+
init_filename_mapping(files)
|
|
1075
|
+
|
|
1071
1076
|
done()
|
|
1072
1077
|
})
|
|
1073
1078
|
await db_folder_init.p
|
|
@@ -2202,26 +2207,61 @@ function create_braid_text() {
|
|
|
2202
2207
|
return i
|
|
2203
2208
|
}
|
|
2204
2209
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2210
|
+
var {
|
|
2211
|
+
encode_file_path_component, ensure_unique_case_insensitive_path_component
|
|
2212
|
+
} = require('url-file-db/canonical_path')
|
|
2213
|
+
|
|
2214
|
+
// Mapping between keys and their encoded filenames
|
|
2215
|
+
// Populated at init time, used to avoid re-encoding and handle case collisions
|
|
2216
|
+
var key_to_filename = new Map()
|
|
2217
|
+
var ifilenames = new Set()
|
|
2218
|
+
|
|
2219
|
+
function encode_filename(key) {
|
|
2220
|
+
// Return cached encoding if we've seen this key before
|
|
2221
|
+
if (key_to_filename.has(key)) {
|
|
2222
|
+
return key_to_filename.get(key)
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// Swap all "!" and "/" characters so paths are more readable on disk
|
|
2226
|
+
var swapped = key.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
|
|
2208
2227
|
|
|
2209
|
-
// Encode
|
|
2210
|
-
|
|
2228
|
+
// Encode unsafe filesystem characters
|
|
2229
|
+
var encoded = encode_file_path_component(swapped)
|
|
2230
|
+
|
|
2231
|
+
// Resolve case collisions for case-insensitive filesystems (Mac/Windows)
|
|
2232
|
+
encoded = ensure_unique_case_insensitive_path_component(encoded, ifilenames)
|
|
2233
|
+
|
|
2234
|
+
// Cache the mapping
|
|
2235
|
+
key_to_filename.set(key, encoded)
|
|
2236
|
+
ifilenames.add(encoded.toLowerCase())
|
|
2211
2237
|
|
|
2212
2238
|
return encoded
|
|
2213
2239
|
}
|
|
2214
2240
|
|
|
2215
2241
|
function decode_filename(encodedFilename) {
|
|
2216
|
-
// Decode the filename
|
|
2217
|
-
|
|
2242
|
+
// Decode the filename
|
|
2243
|
+
var decoded = decodeURIComponent(encodedFilename)
|
|
2218
2244
|
|
|
2219
|
-
// Swap all "/" and "!" characters
|
|
2245
|
+
// Swap all "/" and "!" characters back
|
|
2220
2246
|
decoded = decoded.replace(/[!/]/g, (match) => (match === "/" ? "!" : "/"))
|
|
2221
2247
|
|
|
2222
2248
|
return decoded
|
|
2223
2249
|
}
|
|
2224
2250
|
|
|
2251
|
+
// Populate key_to_filename mapping from existing files on disk
|
|
2252
|
+
function init_filename_mapping(files) {
|
|
2253
|
+
for (var file of files) {
|
|
2254
|
+
// Extract the encoded key (strip extension like .0, .1, etc.)
|
|
2255
|
+
var encoded = file.replace(/\.\d+$/, '')
|
|
2256
|
+
var key = decode_filename(encoded)
|
|
2257
|
+
|
|
2258
|
+
if (!key_to_filename.has(key)) {
|
|
2259
|
+
key_to_filename.set(key, encoded)
|
|
2260
|
+
ifilenames.add(encoded.toLowerCase())
|
|
2261
|
+
} else throw new Error('filename conflict detected')
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2225
2265
|
function validate_version_array(x) {
|
|
2226
2266
|
if (!Array.isArray(x)) throw new Error(`invalid version array: not an array`)
|
|
2227
2267
|
x.sort()
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-text",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.71",
|
|
4
4
|
"description": "Library for collaborative text over http using braid.",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braid-text",
|
|
7
7
|
"homepage": "https://braid.org",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@braid.org/diamond-types-node": "^2.0.0",
|
|
10
|
-
"braid-http": "~1.3.83"
|
|
10
|
+
"braid-http": "~1.3.83",
|
|
11
|
+
"url-file-db": "^0.0.10"
|
|
11
12
|
}
|
|
12
13
|
}
|
package/test/tests.js
CHANGED
|
@@ -2099,6 +2099,126 @@ runTest(
|
|
|
2099
2099
|
'"hi-11"'
|
|
2100
2100
|
)
|
|
2101
2101
|
|
|
2102
|
+
runTest(
|
|
2103
|
+
"test case-insensitive filesystem handling (/a vs /A)",
|
|
2104
|
+
async () => {
|
|
2105
|
+
// This test verifies that keys differing only in case are stored
|
|
2106
|
+
// in separate files on case-insensitive filesystems (Mac/Windows)
|
|
2107
|
+
var key_lower = '/test-case-' + Math.random().toString(36).slice(2)
|
|
2108
|
+
var key_upper = key_lower.toUpperCase()
|
|
2109
|
+
|
|
2110
|
+
// Store different values for lowercase and uppercase keys
|
|
2111
|
+
// Then clear cache and reload from disk to verify filesystem storage
|
|
2112
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
2113
|
+
method: 'PUT',
|
|
2114
|
+
body: `void (async () => {
|
|
2115
|
+
await braid_text.put('${key_lower}', {body: 'lowercase-value'})
|
|
2116
|
+
await braid_text.put('${key_upper}', {body: 'uppercase-value'})
|
|
2117
|
+
|
|
2118
|
+
// Wait for disk write
|
|
2119
|
+
await new Promise(done => setTimeout(done, 200))
|
|
2120
|
+
|
|
2121
|
+
// Clear the in-memory cache to force reload from disk
|
|
2122
|
+
// Note: We keep the key_to_encoded mapping intact since that persists across cache clears
|
|
2123
|
+
delete braid_text.cache['${key_lower}']
|
|
2124
|
+
delete braid_text.cache['${key_upper}']
|
|
2125
|
+
|
|
2126
|
+
// Also need to wait for any async file operations
|
|
2127
|
+
await new Promise(done => setTimeout(done, 100))
|
|
2128
|
+
|
|
2129
|
+
// Read back from disk - pass empty options to force loading
|
|
2130
|
+
var lower = (await braid_text.get('${key_lower}', {})).body
|
|
2131
|
+
var upper = (await braid_text.get('${key_upper}', {})).body
|
|
2132
|
+
res.end(JSON.stringify({lower, upper}))
|
|
2133
|
+
})()`
|
|
2134
|
+
})
|
|
2135
|
+
if (!r1.ok) return 'eval failed: ' + r1.status
|
|
2136
|
+
|
|
2137
|
+
var result = JSON.parse(await r1.text())
|
|
2138
|
+
if (result.lower !== 'lowercase-value') {
|
|
2139
|
+
return 'lower mismatch: ' + result.lower
|
|
2140
|
+
}
|
|
2141
|
+
if (result.upper !== 'uppercase-value') {
|
|
2142
|
+
return 'upper mismatch: ' + result.upper
|
|
2143
|
+
}
|
|
2144
|
+
return 'ok'
|
|
2145
|
+
},
|
|
2146
|
+
'ok'
|
|
2147
|
+
)
|
|
2148
|
+
|
|
2149
|
+
runTest(
|
|
2150
|
+
"test filename conflict detection (different encodings of same key)",
|
|
2151
|
+
async () => {
|
|
2152
|
+
// This test creates two files on disk with different URL-encoded names
|
|
2153
|
+
// that decode to the same key, then tries to load them, which should
|
|
2154
|
+
// throw "filename conflict detected"
|
|
2155
|
+
|
|
2156
|
+
// Use a unique test subdirectory to avoid affecting other tests
|
|
2157
|
+
var testId = Math.random().toString(36).slice(2)
|
|
2158
|
+
|
|
2159
|
+
var r = await braid_fetch(`/eval`, {
|
|
2160
|
+
method: 'PUT',
|
|
2161
|
+
body: `void (async () => {
|
|
2162
|
+
var fs = require('fs')
|
|
2163
|
+
var path = require('path')
|
|
2164
|
+
|
|
2165
|
+
// Create a temporary test db folder
|
|
2166
|
+
var testFolder = path.join(braid_text.db_folder, 'conflict-test-${testId}')
|
|
2167
|
+
await fs.promises.mkdir(testFolder, { recursive: true })
|
|
2168
|
+
|
|
2169
|
+
// Create two files that decode to the same key "/hello"
|
|
2170
|
+
// File 1: Using the standard encoding with ! swapped for /
|
|
2171
|
+
// !hello -> decodes to /hello (after !/swap)
|
|
2172
|
+
await fs.promises.writeFile(path.join(testFolder, '!hello.0'), 'content1')
|
|
2173
|
+
|
|
2174
|
+
// File 2: Using %2F encoding for /
|
|
2175
|
+
// %2Fhello -> decodes to /hello (via URL decoding, then !/swap on /)
|
|
2176
|
+
// Actually, let's trace through decode_filename:
|
|
2177
|
+
// 1. decodeURIComponent('%21hello') = '!hello'
|
|
2178
|
+
// 2. swap !/: '!hello' -> '/hello'
|
|
2179
|
+
// So %21hello decodes to /hello
|
|
2180
|
+
await fs.promises.writeFile(path.join(testFolder, '%21hello.0'), 'content2')
|
|
2181
|
+
|
|
2182
|
+
// Now try to initialize filename mapping with these files
|
|
2183
|
+
// We need to call the internal init_filename_mapping function
|
|
2184
|
+
// through a resource load that reads the test directory
|
|
2185
|
+
|
|
2186
|
+
try {
|
|
2187
|
+
// Read the files from the test folder
|
|
2188
|
+
var files = await fs.promises.readdir(testFolder)
|
|
2189
|
+
|
|
2190
|
+
// Simulate what init_filename_mapping does
|
|
2191
|
+
var key_to_filename = new Map()
|
|
2192
|
+
for (var file of files) {
|
|
2193
|
+
var encoded = file.replace(/\\\.\\d+$/, '')
|
|
2194
|
+
var key = braid_text.decode_filename(encoded)
|
|
2195
|
+
|
|
2196
|
+
if (!key_to_filename.has(key)) {
|
|
2197
|
+
key_to_filename.set(key, encoded)
|
|
2198
|
+
} else {
|
|
2199
|
+
throw new Error('filename conflict detected')
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
res.end('no error thrown')
|
|
2203
|
+
} catch (e) {
|
|
2204
|
+
res.end(e.message)
|
|
2205
|
+
} finally {
|
|
2206
|
+
// Clean up test folder
|
|
2207
|
+
try {
|
|
2208
|
+
for (var f of await fs.promises.readdir(testFolder)) {
|
|
2209
|
+
await fs.promises.unlink(path.join(testFolder, f))
|
|
2210
|
+
}
|
|
2211
|
+
await fs.promises.rmdir(testFolder)
|
|
2212
|
+
} catch (e) {}
|
|
2213
|
+
}
|
|
2214
|
+
})()`
|
|
2215
|
+
})
|
|
2216
|
+
|
|
2217
|
+
return await r.text()
|
|
2218
|
+
},
|
|
2219
|
+
'filename conflict detected'
|
|
2220
|
+
)
|
|
2221
|
+
|
|
2102
2222
|
}
|
|
2103
2223
|
|
|
2104
2224
|
// Export for both Node.js and browser environments
|