pinokiod 8.0.111 → 8.0.112

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.
@@ -3408,7 +3408,10 @@ class Vault {
3408
3408
  .replace(/[\\/]+$/, "")
3409
3409
  .slice(0, 4096)
3410
3410
  const root = source.root.replace(/[\\/]+$/, "")
3411
- const separator = root.includes("\\") && !root.includes("/") ? "\\" : "/"
3411
+ // Every source root is produced by path.resolve, so it is already native
3412
+ // and the platform separator is the right one -- no need to infer it from
3413
+ // the string, which a directory named with a backslash would fool.
3414
+ const separator = path.sep
3412
3415
  const prefix = parent
3413
3416
  ? `${root}${separator}${parent.split("/").join(separator)}${separator}`
3414
3417
  : `${root}${separator}`
@@ -3419,6 +3422,7 @@ class Vault {
3419
3422
  const level = await this.registry.treeLevel({
3420
3423
  sourceId: source.id,
3421
3424
  prefix,
3425
+ separator,
3422
3426
  view,
3423
3427
  statusFilter,
3424
3428
  query,
@@ -3438,7 +3442,10 @@ class Vault {
3438
3442
  // A run of folders holding nothing but one another reads as one row.
3439
3443
  // The shared prefix of the first and last path underneath is exactly
3440
3444
  // that run, because the two are the extremes of a sorted list.
3441
- const shared = commonDirectory(row.lo || "", row.hi || "")
3445
+ const native = (value) => separator === "/"
3446
+ ? String(value || "")
3447
+ : String(value || "").split(separator).join("/")
3448
+ const shared = commonDirectory(native(row.lo), native(row.hi))
3442
3449
  const label = shared && shared.startsWith(`${row.name}/`)
3443
3450
  ? shared
3444
3451
  : row.name
@@ -22,7 +22,9 @@ const canonicalPathKey = (value) => {
22
22
  // user cannot see a reason for. This narrows the match to the last segment.
23
23
  const pathNameKey = (value) => {
24
24
  const filePath = String(value || "")
25
- const cut = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"))
25
+ // Only the platform's own separator: registry paths are always native, and
26
+ // on POSIX a backslash is an ordinary character in a file name.
27
+ const cut = filePath.lastIndexOf(path.sep)
26
28
  return (cut < 0 ? filePath : filePath.slice(cut + 1)).toLowerCase()
27
29
  }
28
30
 
@@ -5175,7 +5177,8 @@ class RegistryCore {
5175
5177
  limit,
5176
5178
  externalSourceIds,
5177
5179
  nameOnly,
5178
- parentPrefix
5180
+ parentPrefix,
5181
+ parentSeparator
5179
5182
  } = options
5180
5183
  const where = ["unavailable_reason IS NOT 'stale'"]
5181
5184
  const values = []
@@ -5202,8 +5205,8 @@ class RegistryCore {
5202
5205
  const [low, high] = this.prefixRange(parentPrefix)
5203
5206
  where.push("path >= ? AND path < ?")
5204
5207
  values.push(low, high)
5205
- where.push("instr(replace(substr(path, ?), '\\', '/'), '/') = 0")
5206
- values.push(String(parentPrefix).length + 1)
5208
+ where.push("instr(substr(path, ?), ?) = 0")
5209
+ values.push(String(parentPrefix).length + 1, parentSeparator)
5207
5210
  }
5208
5211
  if (cursor && typeof cursor.path === "string") {
5209
5212
  if (sort === "asc" && Number.isFinite(cursor.size)) {
@@ -5246,7 +5249,8 @@ class RegistryCore {
5246
5249
  pageSize,
5247
5250
  externalSourceIds,
5248
5251
  nameOnly,
5249
- parentPrefix
5252
+ parentPrefix,
5253
+ parentSeparator
5250
5254
  } = options
5251
5255
  const sources = unrestricted
5252
5256
  ? [null]
@@ -5282,7 +5286,8 @@ class RegistryCore {
5282
5286
  limit,
5283
5287
  externalSourceIds,
5284
5288
  nameOnly,
5285
- parentPrefix
5289
+ parentPrefix,
5290
+ parentSeparator
5286
5291
  })
5287
5292
  stream.rows.push(...rows)
5288
5293
  if (rows.length < limit) stream.exhausted = true
@@ -5356,6 +5361,7 @@ class RegistryCore {
5356
5361
  statuses,
5357
5362
  query,
5358
5363
  nameOnly,
5364
+ separator,
5359
5365
  sizeSort,
5360
5366
  nameSort,
5361
5367
  limit,
@@ -5382,14 +5388,16 @@ class RegistryCore {
5382
5388
  : sizeSort === "desc"
5383
5389
  ? "bytes DESC, name ASC"
5384
5390
  : nameSort === "desc" ? "name DESC" : "name ASC"
5391
+ // Values bind in the order the placeholders appear: the CTE's substr, then
5392
+ // its WHERE, then the two separators in the outer select and filter.
5385
5393
  return this.database.prepare(`
5386
5394
  WITH scoped AS (
5387
- SELECT replace(substr(path, ?), '\', '/') AS rel, size, status
5395
+ SELECT substr(path, ?) AS rel, size, status
5388
5396
  FROM files
5389
5397
  WHERE ${where.join(" AND ")}
5390
5398
  )
5391
5399
  SELECT
5392
- substr(rel, 1, instr(rel, '/') - 1) AS name,
5400
+ substr(rel, 1, instr(rel, ?) - 1) AS name,
5393
5401
  COUNT(*) AS file_count,
5394
5402
  SUM(size) AS bytes,
5395
5403
  SUM(CASE WHEN status = 'duplicate' THEN 1 ELSE 0 END)
@@ -5401,13 +5409,15 @@ class RegistryCore {
5401
5409
  MIN(rel) AS lo,
5402
5410
  MAX(rel) AS hi
5403
5411
  FROM scoped
5404
- WHERE instr(rel, '/') > 0
5412
+ WHERE instr(rel, ?) > 0
5405
5413
  GROUP BY name
5406
5414
  ORDER BY ${order}
5407
5415
  LIMIT ? OFFSET ?
5408
5416
  `).all(
5409
5417
  String(prefix).length + 1,
5410
5418
  ...values,
5419
+ separator,
5420
+ separator,
5411
5421
  limit,
5412
5422
  Math.max(0, Number(offset) || 0)
5413
5423
  )
@@ -5495,6 +5505,7 @@ class RegistryCore {
5495
5505
  const {
5496
5506
  sourceId,
5497
5507
  prefix,
5508
+ separator,
5498
5509
  view,
5499
5510
  statusFilter,
5500
5511
  query,
@@ -5510,6 +5521,7 @@ class RegistryCore {
5510
5521
  directories: this.treeDirectoryRows({
5511
5522
  sourceId,
5512
5523
  prefix,
5524
+ separator,
5513
5525
  statuses: this.statusesForView(view, statusFilter),
5514
5526
  query,
5515
5527
  nameOnly,
@@ -5530,7 +5542,8 @@ class RegistryCore {
5530
5542
  unrestricted: false,
5531
5543
  externalSourceIds,
5532
5544
  nameOnly,
5533
- parentPrefix: prefix
5545
+ parentPrefix: prefix,
5546
+ parentSeparator: separator
5534
5547
  })
5535
5548
  }
5536
5549
  }
@@ -5548,7 +5561,8 @@ class RegistryCore {
5548
5561
  unrestricted,
5549
5562
  externalSourceIds,
5550
5563
  nameOnly,
5551
- parentPrefix
5564
+ parentPrefix,
5565
+ parentSeparator
5552
5566
  } = options
5553
5567
  // Size and name sorting are one server-side order, so only one can win.
5554
5568
  const sort = sizeSort === "asc" || sizeSort === "desc"
@@ -5564,7 +5578,8 @@ class RegistryCore {
5564
5578
  pageSize,
5565
5579
  externalSourceIds,
5566
5580
  nameOnly,
5567
- parentPrefix
5581
+ parentPrefix,
5582
+ parentSeparator
5568
5583
  })
5569
5584
  const hasMore = rows.length > pageSize
5570
5585
  if (hasMore) rows.pop()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.111",
3
+ "version": "8.0.112",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -375,6 +375,57 @@ describe("Save Space engine", () => {
375
375
  await close(reopened)
376
376
  })
377
377
 
378
+ test("the folder tree reads Windows paths", async () => {
379
+ // Backslash separators cannot be exercised through the filesystem on a
380
+ // POSIX runner, so this drives the registry with Windows-shaped rows. It
381
+ // guards the separator, which used to be written into the SQL text where
382
+ // JavaScript and SQLite escaped it differently and neither matched.
383
+ const home = await makeHome()
384
+ const RegistryCore = require("../kernel/vault/registry_core")
385
+ const core = new RegistryCore(home)
386
+ await core.load()
387
+ const root = "C:\\Users\\x\\pinokio\\api\\demo"
388
+ const insert = core.database.prepare(`INSERT INTO files
389
+ (path, hash, size, mtime, ctime, dev, ino, mode, uid, gid,
390
+ source_id, app, status, updated_at)
391
+ VALUES (?, ?, ?, 0, 0, 1, ?, 0, 0, 0, 'app:demo', 'demo',
392
+ 'reference', 0)`);
393
+ [
394
+ [`${root}\\app\\models\\big.bin`, 4000],
395
+ [`${root}\\app\\models\\small.bin`, 10],
396
+ [`${root}\\cache\\only\\deep\\x.bin`, 50],
397
+ [`${root}\\loose.bin`, 700]
398
+ ].forEach(([filePath, size], index) =>
399
+ insert.run(filePath, `h${index}`, size, index + 1))
400
+
401
+ const directories = core.treeDirectoryRows({
402
+ sourceId: "app:demo",
403
+ prefix: `${root}\\`,
404
+ separator: "\\",
405
+ statuses: [],
406
+ query: "",
407
+ nameOnly: false,
408
+ sizeSort: "desc",
409
+ limit: 50,
410
+ offset: 0
411
+ })
412
+ assert.deepEqual(directories.map((row) => row.name), ["app", "cache"])
413
+ assert.equal(directories[0].bytes, 4010)
414
+ // The collapse reads these, so they have to carry the whole subpath.
415
+ assert.equal(directories[1].lo, "cache\\only\\deep\\x.bin")
416
+
417
+ const files = core.fileStreamRows({
418
+ sourceId: "app:demo",
419
+ query: "",
420
+ sort: "path",
421
+ limit: 50,
422
+ parentPrefix: `${root}\\`,
423
+ parentSeparator: "\\"
424
+ })
425
+ assert.deepEqual(files.map((row) => row.path), [`${root}\\loose.bin`])
426
+ core.database.close()
427
+ })
428
+
378
429
  test("the folder tree resolves a group of locations to its members", async () => {
379
430
  const { home, vault } = await makeVault()
380
431
  await writeCandidate(