Skip to content

Commit e0e9df8

Browse files
committed
vfs: support renaming implicit ZIP directories
Treat archive entry prefixes as directories when renaming with ZipProvider. Move all descendant entries to the new prefix for both asynchronous and synchronous operations. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol
1 parent 6e7818e commit e0e9df8

2 files changed

Lines changed: 67 additions & 16 deletions

File tree

lib/internal/vfs/providers/ziparchive.js

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const { VirtualProvider } = require('internal/vfs/provider');
2323
const { VirtualFileHandle } = require('internal/vfs/file_handle');
2424
const {
2525
createEEXIST,
26+
createEINVAL,
2627
createEISDIR,
2728
createENOENT,
2829
createENOTDIR,
@@ -102,6 +103,28 @@ function methodOption(method) {
102103
return 'deflate';
103104
}
104105

106+
function renameOptions(entry) {
107+
return {
108+
mode: entry.mode || undefined,
109+
modified: entry.modified,
110+
method: methodOption(entry.method),
111+
};
112+
}
113+
114+
function directoryRenames(source, oldName, newName) {
115+
const entries = [];
116+
const prefix = `${oldName}/`;
117+
for (const name of source.keys()) {
118+
if (StringPrototypeStartsWith(name, prefix)) {
119+
ArrayPrototypePush(entries, {
120+
oldName: name,
121+
newName: newName + StringPrototypeSlice(name, oldName.length),
122+
});
123+
}
124+
}
125+
return entries;
126+
}
127+
105128
/**
106129
* A file handle over one ZIP entry. ZIP members can't be edited in place
107130
* (they're a single compressed blob), so writes accumulate in memory and are
@@ -530,28 +553,48 @@ class ZipProvider extends VirtualProvider {
530553
const oldName = normalize(oldPath);
531554
const newName = normalize(newPath);
532555
const entry = await this.#getEntry(oldName);
533-
if (entry === null) throw createENOENT('rename', oldPath);
534-
const content = await entry.content();
535-
await this.#source.add(newName, content, {
536-
mode: entry.mode || undefined,
537-
modified: entry.modified,
538-
method: methodOption(entry.method),
539-
});
540-
await this.#source.delete(oldName);
556+
const entries = entry === null ?
557+
directoryRenames(this.#source, oldName, newName) :
558+
[{ oldName, newName, entry }];
559+
if (entries.length === 0) throw createENOENT('rename', oldPath);
560+
if (oldName === newName) return;
561+
if (entry === null && StringPrototypeStartsWith(newName, `${oldName}/`)) {
562+
throw createEINVAL('rename', oldPath);
563+
}
564+
565+
for (let i = 0; i < entries.length; i++) {
566+
const item = entries[i];
567+
item.entry ??= await this.#getEntry(item.oldName);
568+
await this.#source.add(
569+
item.newName, await item.entry.content(), renameOptions(item.entry));
570+
}
571+
for (let i = 0; i < entries.length; i++) {
572+
await this.#source.delete(entries[i].oldName);
573+
}
541574
}
542575
renameSync(oldPath, newPath) {
543576
if (this.readonly) throw createEROFS('rename', oldPath);
544577
const oldName = normalize(oldPath);
545578
const newName = normalize(newPath);
546579
const entry = this.#getEntrySync(oldName);
547-
if (entry === null) throw createENOENT('rename', oldPath);
548-
const content = entry.contentSync();
549-
this.#source.addSync(newName, content, {
550-
mode: entry.mode || undefined,
551-
modified: entry.modified,
552-
method: methodOption(entry.method),
553-
});
554-
this.#deleteEntrySync(oldName);
580+
const entries = entry === null ?
581+
directoryRenames(this.#source, oldName, newName) :
582+
[{ oldName, newName, entry }];
583+
if (entries.length === 0) throw createENOENT('rename', oldPath);
584+
if (oldName === newName) return;
585+
if (entry === null && StringPrototypeStartsWith(newName, `${oldName}/`)) {
586+
throw createEINVAL('rename', oldPath);
587+
}
588+
589+
for (let i = 0; i < entries.length; i++) {
590+
const item = entries[i];
591+
item.entry ??= this.#getEntrySync(item.oldName);
592+
this.#source.addSync(
593+
item.newName, item.entry.contentSync(), renameOptions(item.entry));
594+
}
595+
for (let i = 0; i < entries.length; i++) {
596+
this.#deleteEntrySync(entries[i].oldName);
597+
}
555598
}
556599

557600
/**

test/parallel/test-vfs-zip-provider.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ async function buildArchive(entries, comment) {
119119
await assert.rejects(archiveVfs.promises.open('/does-not-exist.txt', 'r'), { code: 'ENOENT' });
120120
await assert.rejects(archiveVfs.promises.open('/a.txt', 'wx'), { code: 'EEXIST' });
121121
await assert.rejects(archiveVfs.promises.open('/dir', 'r'), { code: 'EISDIR' });
122+
123+
await archiveVfs.promises.rename('/dir', '/renamed-dir');
124+
await assert.rejects(archiveVfs.promises.stat('/dir'), { code: 'ENOENT' });
125+
assert.strictEqual(await archiveVfs.promises.readFile('/renamed-dir/b.txt', 'utf8'), 'nested');
122126
}
123127

124128
// --- ZipFile-backed, read-only: writes rejected with EROFS ----------------
@@ -220,6 +224,10 @@ async function buildArchive(entries, comment) {
220224
assert.throws(() => archiveVfs.openSync('/does-not-exist.txt', 'r'), { code: 'ENOENT' });
221225
assert.throws(() => archiveVfs.openSync('/a.txt', 'wx'), { code: 'EEXIST' });
222226
assert.throws(() => archiveVfs.openSync('/dir', 'r'), { code: 'EISDIR' });
227+
228+
archiveVfs.renameSync('/dir', '/renamed-dir');
229+
assert.throws(() => archiveVfs.statSync('/dir'), { code: 'ENOENT' });
230+
assert.strictEqual(archiveVfs.readFileSync('/renamed-dir/b.txt', 'utf8'), 'nested');
223231
}
224232

225233
// --- ZipFile-backed via openSync: sync-only round trip on disk -----------

0 commit comments

Comments
 (0)