fix(webworker): store and honour VFS permission bits
This commit is contained in:
1 parent
8081620a35
commit
c2a30af92d
5 files changed
+130
-45
No files matched your search
+20
-10
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import { requireActiveVfs } from '../../../storage/active.ts'
|
||||
import type { MemoryVfs } from '../../../storage/memory.ts'
|
||||
import type { VfsBigIntStats, VfsStatOptions, VfsStats } from '../../../storage/types.ts'
|
||||
import type { VfsBigIntStats, VfsStatOptions, VfsStats, VfsWriteOptions } from '../../../storage/types.ts'
|
||||
import { Buffer } from 'buffer'
|
||||
import { dirname } from './path.ts'
|
||||
|
||||
@@ -116,9 +116,10 @@ export function readFileSync(path: PathArg, options?: EncodingOption): Buffer |
|
||||
* Write a file.
|
||||
* @param path - file path.
|
||||
* @param data - bytes or text.
|
||||
* @param options - write flag and creation mode, forwarded to the VFS.
|
||||
*/
|
||||
export function writeFileSync(path: PathArg, data: string | Uint8Array): void {
|
||||
vfs().writeFileSync(asPath(path), data)
|
||||
export function writeFileSync(path: PathArg, data: string | Uint8Array, options?: VfsWriteOptions): void {
|
||||
vfs().writeFileSync(asPath(path), data, options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,6 +150,15 @@ export function statSync(path: PathArg, options?: VfsStatOptions): VfsStats | Vf
|
||||
return vfs().statSync(asPath(path), options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an entry's permission bits; stat reads back exactly what was set.
|
||||
* @param path - the path.
|
||||
* @param mode - new permission bits (`0o777` mask), numeric or Node's octal string form.
|
||||
*/
|
||||
export function chmodSync(path: PathArg, mode: number | string): void {
|
||||
vfs().chmodSync(asPath(path), typeof mode === 'string' ? Number.parseInt(mode, 8) : mode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stat a path without following symlinks (the image has none).
|
||||
* @param path - the path.
|
||||
@@ -190,7 +200,7 @@ export function readdirSync(
|
||||
* @param options - `recursive` creates parents.
|
||||
* @returns the first created path when recursive, else undefined.
|
||||
*/
|
||||
export function mkdirSync(path: PathArg, options?: { recursive?: boolean }): string | undefined {
|
||||
export function mkdirSync(path: PathArg, options?: { recursive?: boolean; mode?: number }): string | undefined {
|
||||
return vfs().mkdirSync(asPath(path), options)
|
||||
}
|
||||
|
||||
@@ -489,19 +499,20 @@ export const promises = {
|
||||
writeFile: async (
|
||||
path: PathArg,
|
||||
data: string | Uint8Array,
|
||||
options?: { flag?: string } | BufferEncoding | null,
|
||||
options?: { flag?: string; mode?: number } | BufferEncoding | null,
|
||||
): Promise<void> => {
|
||||
const flag = typeof options === 'object' && options !== null ? options.flag : undefined
|
||||
const mode = typeof options === 'object' && options !== null ? options.mode : undefined
|
||||
if (flag !== undefined && flag.includes('x') && existsSync(path)) {
|
||||
const error = new Error(`EEXIST: file already exists, open '${asPath(path)}'`) as Error & { code: string }
|
||||
error.code = 'EEXIST'
|
||||
throw error
|
||||
}
|
||||
if (flag !== undefined && flag.startsWith('a')) appendFileSync(path, data)
|
||||
else writeFileSync(path, data)
|
||||
else writeFileSync(path, data, { ...flag === undefined ? {} : { flag }, ...mode === undefined ? {} : { mode } })
|
||||
},
|
||||
appendFile: async (path: PathArg, data: string | Uint8Array): Promise<void> => { appendFileSync(path, data) },
|
||||
mkdir: async (path: PathArg, options?: { recursive?: boolean }): Promise<string | undefined> => mkdirSync(path, options),
|
||||
mkdir: async (path: PathArg, options?: { recursive?: boolean; mode?: number }): Promise<string | undefined> => mkdirSync(path, options),
|
||||
mkdtemp: async (prefix: string): Promise<string> => mkdtempSync(prefix),
|
||||
readdir: async (
|
||||
path: PathArg,
|
||||
@@ -514,8 +525,7 @@ export const promises = {
|
||||
unlink: async (path: PathArg): Promise<void> => { unlinkSync(path) },
|
||||
rename: async (from: PathArg, to: PathArg): Promise<void> => { renameSync(from, to) },
|
||||
access: async (path: PathArg): Promise<void> => { accessSync(path) },
|
||||
// Permission bits have no meaning in the VFS; callers only ever relax them.
|
||||
chmod: async (): Promise<void> => { /* no permission model */ },
|
||||
chmod: async (path: PathArg, mode: number | string): Promise<void> => { chmodSync(path, mode) },
|
||||
cp: async (from: PathArg, to: PathArg): Promise<void> => {
|
||||
const source = asPath(from)
|
||||
const target = asPath(to)
|
||||
@@ -567,7 +577,7 @@ type NodeFace = Partial<Omit<typeof import('node:fs'), OwnSignature>>
|
||||
/** CommonJS default export: the members `require()` hands a caller of this module. */
|
||||
export default {
|
||||
constants, promises, Dirent,
|
||||
readFileSync, writeFileSync, appendFileSync, existsSync, statSync, lstatSync, realpathSync,
|
||||
readFileSync, writeFileSync, appendFileSync, existsSync, statSync, lstatSync, realpathSync, chmodSync,
|
||||
readdirSync, mkdirSync, mkdtempSync, rmSync, unlinkSync, renameSync, accessSync, opendirSync,
|
||||
openHandleSync, linkSync,
|
||||
openSync, readSync, writeSync, closeSync, watchFile, unwatchFile,
|
||||
|
||||
@@ -17,8 +17,16 @@ const encoder = new TextEncoder()
|
||||
interface FileNode {
|
||||
bytes: Uint8Array
|
||||
mtimeMs: number
|
||||
/** Permission bits (`0o777` mask), set at creation and changed only by `chmod`. */
|
||||
mode: number
|
||||
}
|
||||
|
||||
/** Creation default for files, Node's `0o666` under the classic `022` umask. */
|
||||
const DEFAULT_FILE_MODE = 0o644
|
||||
|
||||
/** Creation default for directories, Node's `0o777` under the classic `022` umask. */
|
||||
const DEFAULT_DIRECTORY_MODE = 0o755
|
||||
|
||||
function fail(code: string, syscall: string, path: string, detail?: string): never {
|
||||
const error = new Error(`${code}: ${detail ?? syscall} failed, ${syscall} '${path}'`) as VfsError
|
||||
error.code = code
|
||||
@@ -33,15 +41,17 @@ function encodingOf(options: VfsReadOptions): VfsEncoding | undefined {
|
||||
return options.encoding ?? undefined
|
||||
}
|
||||
|
||||
// Owner-only permission bits (600/700), here and in the BigInt shape below:
|
||||
// the VFS has one owner, and dsh-credentials-local refuses to start over a
|
||||
// secrets file readable beyond its owner.
|
||||
function statsOf(size: number, mtimeMs: number, directory: boolean): VfsStats {
|
||||
// Permission bits are entry state: creation takes the caller's mode (or the
|
||||
// umask-free default), `chmod` changes it, and both stat shapes report the
|
||||
// stored value — the round-trip consumers like dsh-credentials-local's
|
||||
// owner-only check rely on. The bits are never enforced: a single-owner
|
||||
// filesystem reads and writes as its owner regardless, like root.
|
||||
function statsOf(size: number, mtimeMs: number, directory: boolean, mode: number): VfsStats {
|
||||
return {
|
||||
size,
|
||||
mtimeMs,
|
||||
mtime: new Date(mtimeMs),
|
||||
mode: directory ? 0o040700 : 0o100600,
|
||||
mode: (directory ? 0o040000 : 0o100000) | (mode & 0o777),
|
||||
isFile: () => !directory,
|
||||
isDirectory: () => directory,
|
||||
isSymbolicLink: () => false,
|
||||
@@ -62,15 +72,16 @@ function statsOf(size: number, mtimeMs: number, directory: boolean): VfsStats {
|
||||
* @param mtimeMs - Modification time the entry carries.
|
||||
* @param directory - Whether the entry is a directory.
|
||||
* @param ino - Identity of the entry at this path.
|
||||
* @param mode - Stored permission bits of the entry.
|
||||
* @returns Stats in the shape Node returns under `{ bigint: true }`.
|
||||
*/
|
||||
function bigIntStatsOf(size: number, mtimeMs: number, directory: boolean, ino: bigint): VfsBigIntStats {
|
||||
function bigIntStatsOf(size: number, mtimeMs: number, directory: boolean, ino: bigint, mode: number): VfsBigIntStats {
|
||||
const milliseconds = BigInt(Math.trunc(mtimeMs))
|
||||
const nanoseconds = milliseconds * 1_000_000n
|
||||
const time = new Date(mtimeMs)
|
||||
return {
|
||||
size: BigInt(size),
|
||||
mode: directory ? 0o040700n : 0o100600n,
|
||||
mode: BigInt((directory ? 0o040000 : 0o100000) | (mode & 0o777)),
|
||||
dev: 1n,
|
||||
ino,
|
||||
nlink: 1n,
|
||||
@@ -104,6 +115,8 @@ function bigIntStatsOf(size: number, mtimeMs: number, directory: boolean, ino: b
|
||||
export class MemoryVfs {
|
||||
private readonly files = new Map<string, FileNode>()
|
||||
private readonly directories = new Set<string>([SEP])
|
||||
/** Directory permission bits; absence means {@link DEFAULT_DIRECTORY_MODE}. */
|
||||
private readonly directoryModes = new Map<string, number>()
|
||||
private temporaries = 0
|
||||
// Identity per path, assigned on first stat and dropped when the path goes:
|
||||
// the filesystem service builds its version token from `ino` plus the
|
||||
@@ -118,7 +131,7 @@ export class MemoryVfs {
|
||||
this.writeFileSync(path, data, options)
|
||||
},
|
||||
appendFile: async (path: string, data: string | Uint8Array): Promise<void> => { this.appendFileSync(path, data) },
|
||||
mkdir: async (path: string, options?: { recursive?: boolean }): Promise<string | undefined> => this.mkdirSync(path, options),
|
||||
mkdir: async (path: string, options?: { recursive?: boolean; mode?: number }): Promise<string | undefined> => this.mkdirSync(path, options),
|
||||
readdir: async (path: string, options?: { withFileTypes?: boolean }): Promise<string[] & VfsDirent[]> =>
|
||||
this.readdirSync(path, options),
|
||||
stat: async (path: string, options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats> => this.statSync(path, options),
|
||||
@@ -130,7 +143,7 @@ export class MemoryVfs {
|
||||
mkdtemp: async (prefix: string): Promise<string> => this.mkdtempSync(prefix),
|
||||
link: async (existing: string, next: string): Promise<void> => { this.linkSync(existing, next) },
|
||||
truncate: async (path: string, length?: number): Promise<void> => { this.truncateSync(path, length) },
|
||||
chmod: async (): Promise<void> => {},
|
||||
chmod: async (path: string, mode: number): Promise<void> => { this.chmodSync(path, mode) },
|
||||
opendir: async (path: string): Promise<VfsDir> => this.opendir(path),
|
||||
open: async (path: string, flags?: string, mode?: number): Promise<VfsFileHandle> => this.open(path, flags, mode),
|
||||
/** Resolves for any existing path: the VFS grants read and write to everything it holds. */
|
||||
@@ -181,14 +194,14 @@ export class MemoryVfs {
|
||||
statSync(path: string, options?: VfsStatOptions): VfsStats | VfsBigIntStats {
|
||||
const target = this.key(path)
|
||||
const node = this.files.get(target)
|
||||
const [size, mtimeMs, directory] = node !== undefined
|
||||
? [node.bytes.length, node.mtimeMs, false] as const
|
||||
const [size, mtimeMs, directory, mode] = node !== undefined
|
||||
? [node.bytes.length, node.mtimeMs, false, node.mode] as const
|
||||
: this.directories.has(target)
|
||||
? [0, 0, true] as const
|
||||
? [0, 0, true, this.directoryModes.get(target) ?? DEFAULT_DIRECTORY_MODE] as const
|
||||
: fail('ENOENT', 'stat', target)
|
||||
return options?.bigint === true
|
||||
? bigIntStatsOf(size, mtimeMs, directory, this.identityOf(target))
|
||||
: statsOf(size, mtimeMs, directory)
|
||||
? bigIntStatsOf(size, mtimeMs, directory, this.identityOf(target), mode)
|
||||
: statsOf(size, mtimeMs, directory, mode)
|
||||
}
|
||||
|
||||
/** @returns Stats in the plain shape, for internal callers that read `size`/`mtimeMs`. */
|
||||
@@ -285,7 +298,7 @@ export class MemoryVfs {
|
||||
* @param options - `recursive` creates missing parents.
|
||||
* @returns First created path when recursive, otherwise undefined.
|
||||
*/
|
||||
mkdirSync(path: string, options?: { recursive?: boolean }): string | undefined {
|
||||
mkdirSync(path: string, options?: { recursive?: boolean; mode?: number }): string | undefined {
|
||||
const target = this.key(path)
|
||||
if (this.files.has(target)) fail('EEXIST', 'mkdir', target)
|
||||
if (this.directories.has(target)) {
|
||||
@@ -298,6 +311,7 @@ export class MemoryVfs {
|
||||
this.mkdirSync(parent, options)
|
||||
}
|
||||
this.directories.add(target)
|
||||
if (options?.mode !== undefined) this.directoryModes.set(target, options.mode & 0o777)
|
||||
return target
|
||||
}
|
||||
|
||||
@@ -314,7 +328,10 @@ export class MemoryVfs {
|
||||
const flag = options?.flag ?? 'w'
|
||||
if (flag.startsWith('wx') && this.files.has(target)) fail('EEXIST', 'open', target)
|
||||
if (flag.startsWith('a')) { this.appendFileSync(target, data); return }
|
||||
this.files.set(target, { bytes: typeof data === 'string' ? encoder.encode(data) : data, mtimeMs: this.touch(target) })
|
||||
// POSIX open(O_CREAT): the mode applies at creation only; a rewrite keeps
|
||||
// the entry's bits.
|
||||
const mode = this.files.get(target)?.mode ?? (options?.mode !== undefined ? options.mode & 0o777 : DEFAULT_FILE_MODE)
|
||||
this.files.set(target, { bytes: typeof data === 'string' ? encoder.encode(data) : data, mtimeMs: this.touch(target), mode })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,11 +362,10 @@ export class MemoryVfs {
|
||||
* Open a file handle.
|
||||
* @param path - File path.
|
||||
* @param flags - Node open flags; `r` requires the file, `wx` refuses an existing one.
|
||||
* @param mode - Accepted and ignored: the VFS has no permission bits.
|
||||
* @param mode - Permission bits applied when the open creates the file.
|
||||
* @returns File handle.
|
||||
*/
|
||||
open(path: string, flags = 'r', mode?: number): VfsFileHandle {
|
||||
void mode
|
||||
const target = this.key(path)
|
||||
// Durable writers fsync the parent directory by opening it read-only.
|
||||
if (this.directories.has(target)) {
|
||||
@@ -366,9 +382,10 @@ export class MemoryVfs {
|
||||
if (flags.startsWith('r') && !exists) fail('ENOENT', 'open', target)
|
||||
if (flags.startsWith('wx') && exists) fail('EEXIST', 'open', target)
|
||||
if (!flags.startsWith('r') && !this.directories.has(dirname(target))) fail('ENOENT', 'open', target)
|
||||
if (flags.startsWith('w') && !flags.startsWith('wx')) this.writeFileSync(target, new Uint8Array())
|
||||
if (flags.startsWith('wx')) this.writeFileSync(target, new Uint8Array(), { flag: 'wx' })
|
||||
if (flags.startsWith('a') && !exists) this.writeFileSync(target, new Uint8Array())
|
||||
const creation = mode === undefined ? {} : { mode }
|
||||
if (flags.startsWith('w') && !flags.startsWith('wx')) this.writeFileSync(target, new Uint8Array(), creation)
|
||||
if (flags.startsWith('wx')) this.writeFileSync(target, new Uint8Array(), { flag: 'wx', ...creation })
|
||||
if (flags.startsWith('a') && !exists) this.writeFileSync(target, new Uint8Array(), creation)
|
||||
const appending = flags.startsWith('a')
|
||||
return {
|
||||
write: async (data: string | Uint8Array): Promise<{ bytesWritten: number }> => {
|
||||
@@ -387,7 +404,7 @@ export class MemoryVfs {
|
||||
truncate: async (length = 0): Promise<void> => {
|
||||
const node = this.files.get(target)
|
||||
if (node === undefined) fail('ENOENT', 'ftruncate', target)
|
||||
this.files.set(target, { bytes: node.bytes.slice(0, length), mtimeMs: this.touch(target) })
|
||||
this.files.set(target, { bytes: node.bytes.slice(0, length), mtimeMs: this.touch(target), mode: node.mode })
|
||||
},
|
||||
...this.handleTail(target),
|
||||
}
|
||||
@@ -424,7 +441,7 @@ export class MemoryVfs {
|
||||
const merged = new Uint8Array(existing.bytes.length + addition.length)
|
||||
merged.set(existing.bytes)
|
||||
merged.set(addition, existing.bytes.length)
|
||||
this.files.set(target, { bytes: merged, mtimeMs: this.touch(target) })
|
||||
this.files.set(target, { bytes: merged, mtimeMs: this.touch(target), mode: existing?.mode ?? DEFAULT_FILE_MODE })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -453,8 +470,12 @@ export class MemoryVfs {
|
||||
}
|
||||
for (const candidate of [...this.directories]) {
|
||||
if (!candidate.startsWith(prefix) && candidate !== source) continue
|
||||
const moved = candidate === source ? destination : join(destination, candidate.slice(prefix.length))
|
||||
this.directories.delete(candidate)
|
||||
this.directories.add(candidate === source ? destination : join(destination, candidate.slice(prefix.length)))
|
||||
this.directories.add(moved)
|
||||
const bits = this.directoryModes.get(candidate)
|
||||
this.directoryModes.delete(candidate)
|
||||
if (bits !== undefined) this.directoryModes.set(moved, bits)
|
||||
}
|
||||
this.forgetIdentity(source)
|
||||
this.forgetIdentity(destination)
|
||||
@@ -488,7 +509,26 @@ export class MemoryVfs {
|
||||
const target = this.key(path)
|
||||
const node = this.files.get(target)
|
||||
if (node === undefined) fail('ENOENT', 'truncate', target)
|
||||
this.files.set(target, { bytes: node.bytes.slice(0, length), mtimeMs: this.touch(target) })
|
||||
this.files.set(target, { bytes: node.bytes.slice(0, length), mtimeMs: this.touch(target), mode: node.mode })
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an entry's permission bits; stat reads back exactly what was set.
|
||||
* @param path - File or directory path.
|
||||
* @param mode - New permission bits (`0o777` mask).
|
||||
*/
|
||||
chmodSync(path: string, mode: number): void {
|
||||
const target = this.key(path)
|
||||
const node = this.files.get(target)
|
||||
if (node !== undefined) {
|
||||
node.mode = mode & 0o777
|
||||
return
|
||||
}
|
||||
if (this.directories.has(target)) {
|
||||
this.directoryModes.set(target, mode & 0o777)
|
||||
return
|
||||
}
|
||||
fail('ENOENT', 'chmod', target)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -516,8 +556,13 @@ export class MemoryVfs {
|
||||
if (options?.recursive !== true) fail('ERR_FS_EISDIR', 'rm', target)
|
||||
const prefix = `${target}${SEP}`
|
||||
for (const candidate of [...this.files.keys()]) if (candidate.startsWith(prefix)) this.files.delete(candidate)
|
||||
for (const candidate of [...this.directories]) if (candidate.startsWith(prefix)) this.directories.delete(candidate)
|
||||
for (const candidate of [...this.directories]) {
|
||||
if (!candidate.startsWith(prefix)) continue
|
||||
this.directories.delete(candidate)
|
||||
this.directoryModes.delete(candidate)
|
||||
}
|
||||
this.directories.delete(target)
|
||||
this.directoryModes.delete(target)
|
||||
this.forgetIdentity(target)
|
||||
return
|
||||
}
|
||||
@@ -540,19 +585,23 @@ export class MemoryVfs {
|
||||
* Seed a file and its parent directories, for image loading and tests.
|
||||
* @param path - File path.
|
||||
* @param data - Text or bytes.
|
||||
* @param mode - Permission bits recorded for the entry.
|
||||
*/
|
||||
seed(path: string, data: string | Uint8Array): void {
|
||||
seed(path: string, data: string | Uint8Array, mode = DEFAULT_FILE_MODE): void {
|
||||
const target = this.key(path)
|
||||
this.mkdirSync(dirname(target), { recursive: true })
|
||||
this.files.set(target, { bytes: typeof data === 'string' ? encoder.encode(data) : data, mtimeMs: this.touch(target) })
|
||||
this.files.set(target, { bytes: typeof data === 'string' ? encoder.encode(data) : data, mtimeMs: this.touch(target), mode: mode & 0o777 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory and its parents.
|
||||
* @param path - Directory path.
|
||||
* @param mode - Permission bits recorded for the directory itself.
|
||||
*/
|
||||
seedDirectory(path: string): void {
|
||||
this.mkdirSync(this.key(path), { recursive: true })
|
||||
seedDirectory(path: string, mode = DEFAULT_DIRECTORY_MODE): void {
|
||||
const target = this.key(path)
|
||||
this.mkdirSync(target, { recursive: true })
|
||||
if (mode !== DEFAULT_DIRECTORY_MODE) this.directoryModes.set(target, mode & 0o777)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -586,10 +635,10 @@ export function loadVfsImage(image: Uint8Array, root = '/dsh', vfs = new MemoryV
|
||||
}
|
||||
const target = join(root, relativeName)
|
||||
if (entry.directory) {
|
||||
vfs.seedDirectory(target)
|
||||
vfs.seedDirectory(target, entry.mode)
|
||||
continue
|
||||
}
|
||||
vfs.seed(target, entry.bytes)
|
||||
vfs.seed(target, entry.bytes, entry.mode)
|
||||
}
|
||||
return vfs
|
||||
}
|
||||
@@ -20,6 +20,8 @@ export interface TarEntry {
|
||||
readonly name: string
|
||||
readonly bytes: Uint8Array
|
||||
readonly directory: boolean
|
||||
/** Permission bits from the header's mode field (`0o777` mask). */
|
||||
readonly mode: number
|
||||
}
|
||||
|
||||
/** Write an octal field: zero-padded digits with a terminating NUL. */
|
||||
@@ -123,13 +125,14 @@ export function parseTar(archive: Uint8Array): TarEntry[] {
|
||||
const prefix = readField(header, 345, 155)
|
||||
const name = prefix === '' ? short : `${prefix}/${short}`
|
||||
const size = Number.parseInt(readField(header, 124, 12).trim() || '0', 8)
|
||||
const mode = Number.parseInt(readField(header, 100, 8).trim() || '0', 8) & 0o777
|
||||
const typeflag = header[156]
|
||||
const directory = typeflag === 0x35 || name.endsWith('/')
|
||||
if (typeflag !== 0x30 && typeflag !== 0 && typeflag !== 0x35) {
|
||||
throw new Error(`vfs tar: unsupported entry type ${String.fromCharCode(typeflag ?? 0)} for "${name}"`)
|
||||
}
|
||||
const dataStart = offset + BLOCK
|
||||
entries.push({ name, bytes: archive.subarray(dataStart, dataStart + size), directory })
|
||||
entries.push({ name, bytes: archive.subarray(dataStart, dataStart + size), directory, mode })
|
||||
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK
|
||||
}
|
||||
return entries
|
||||
|
||||
@@ -168,7 +168,7 @@ fs.writeFileSync('/dsh/versioned.txt', 'one')
|
||||
{
|
||||
const stats = bigStats('/dsh/versioned.txt')
|
||||
check('bigint stat reports mode as a BigInt', typeof stats.mode, 'bigint')
|
||||
check('bigint mode masks to an owner-only file permission', Number(stats.mode & 0o777n), 0o600)
|
||||
check('bigint mode masks to the creation-default file permission', Number(stats.mode & 0o777n), 0o644)
|
||||
check('bigint stat reports the identity fields the version token needs', [
|
||||
typeof stats.dev, typeof stats.ino, typeof stats.size, typeof stats.mtimeNs, typeof stats.ctimeNs,
|
||||
], ['bigint', 'bigint', 'bigint', 'bigint', 'bigint'])
|
||||
@@ -199,5 +199,24 @@ fs.writeFileSync('/dsh/versioned.txt', 'one')
|
||||
check('a removed and recreated path reports a new identity', bigStats('/dsh/versioned.txt').ino !== first, true)
|
||||
}
|
||||
|
||||
check('a directory reports owner-only directory mode in the bigint shape', Number(bigStats('/dsh/config').mode & 0o777n), 0o700)
|
||||
check('a directory reports the creation-default mode in the bigint shape', Number(bigStats('/dsh/config').mode & 0o777n), 0o755)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Permission bits round-trip: creation takes the caller's mode, chmod changes
|
||||
// it, stat reads back the stored value. dsh-credentials-local's owner-only
|
||||
// check reads exactly this (writeFileAtomic writes `wx` + mode 600, renames
|
||||
// into place, then the provider stats the result).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fs.writeFileSync('/dsh/secrets.tmp', 'k: v\n', { mode: 0o600, flag: 'wx' })
|
||||
fs.renameSync('/dsh/secrets.tmp', '/dsh/secrets.yaml')
|
||||
check('a wx write with mode 600 stats as 600 after rename', fs.statSync('/dsh/secrets.yaml').mode & 0o777, 0o600)
|
||||
fs.writeFileSync('/dsh/secrets.yaml', 'k: w\n')
|
||||
check('a rewrite keeps the creation bits', fs.statSync('/dsh/secrets.yaml').mode & 0o777, 0o600)
|
||||
fs.chmodSync('/dsh/secrets.yaml', 0o640)
|
||||
check('chmod reads back exactly what was set', fs.statSync('/dsh/secrets.yaml').mode & 0o777, 0o640)
|
||||
await fsp.chmod('/dsh/secrets.yaml', 0o600)
|
||||
check('promises.chmod reads back through the bigint shape', Number(bigStats('/dsh/secrets.yaml').mode & 0o777n), 0o600)
|
||||
fs.mkdirSync('/dsh/vault', { mode: 0o700 })
|
||||
check('mkdir honours its mode option', fs.statSync('/dsh/vault').mode & 0o777, 0o700)
|
||||
check('promises.stat forwards the bigint option', typeof (await fsp.stat('/dsh/config', { bigint: true })).mode, 'bigint')
|
||||
@@ -21,6 +21,10 @@ describe('tar codec', () => {
|
||||
expect([...byName.keys()].sort()).toEqual(Object.keys(files).sort())
|
||||
expect([...byName.get('node_modules/pkg/lib/index.js')!.bytes]).toEqual([...payload])
|
||||
expect(byName.get('home/')!.bytes.byteLength).toBe(0)
|
||||
// The header mode field carries the packed permission bits: normal
|
||||
// 644/755, which the VFS mount reports back through stat.
|
||||
expect(byName.get('node_modules/pkg/lib/index.js')!.mode).toBe(0o644)
|
||||
expect(byName.get('home/')!.mode).toBe(0o755)
|
||||
})
|
||||
|
||||
it('mounts as a VFS with directories synthesized along file paths', () => {
|
||||
|
||||
Reference in new issue
Block a user