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.
Files changed (3) hide show
  1. package/index.js +48 -8
  2. package/package.json +3 -2
  3. 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
- function encode_filename(filename) {
2206
- // Swap all "!" and "/" characters
2207
- let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
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 the filename using encodeURIComponent()
2210
- let encoded = encodeURIComponent(swapped)
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 using decodeURIComponent()
2217
- let decoded = decodeURIComponent(encodedFilename)
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.70",
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