Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] master from streamich:master #21

Merged
merged 10 commits into from
Sep 19, 2024
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# [4.12.0](https://github.com/streamich/memfs/compare/v4.11.2...v4.12.0) (2024-09-19)


### Features

* improve permissions support ([#1059](https://github.com/streamich/memfs/issues/1059)) ([575a76b](https://github.com/streamich/memfs/commit/575a76b5f538f65ca5b8813073bc783f3d408a1b))

## [4.11.2](https://github.com/streamich/memfs/compare/v4.11.1...v4.11.2) (2024-09-17)


### Bug Fixes

* add `parentPath` to `Dirent` ([#1058](https://github.com/streamich/memfs/issues/1058)) ([9156c84](https://github.com/streamich/memfs/commit/9156c8466b530ad985b462890c7164dfeeaf472f)), closes [#735](https://github.com/streamich/memfs/issues/735) [#735](https://github.com/streamich/memfs/issues/735)

## [4.11.1](https://github.com/streamich/memfs/compare/v4.11.0...v4.11.1) (2024-08-01)


### Bug Fixes

* 🐛 declare ReadableStream global type ([#1048](https://github.com/streamich/memfs/issues/1048)) ([57fba2c](https://github.com/streamich/memfs/commit/57fba2c825b21e14645cdeeec6576fe44331ce7c))

# [4.11.0](https://github.com/streamich/memfs/compare/v4.10.0...v4.11.0) (2024-07-27)


Expand Down
24 changes: 12 additions & 12 deletions docs/snapshot/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ import * as snapshot from 'memfs/lib/snapshot';
You can convert any folder of an `fs`-like file system into a POJO snapshot.

```ts
const snap = snapshot.toSnapshotSync({ fs, dir });
const snap = await snapshot.toSnapshot({ fs: fs.promises, dir });
const snap = snapshot.toSnapshotSync({ fs, path });
const snap = await snapshot.toSnapshot({ fs: fs.promises, path });
```

Then import it back from snapshot.

```ts
snapshot.fromSnapshotSync(snap, { fs, dir });
await snapshot.fromSnapshot(snap, { fs: fs.promises, dir });
snapshot.fromSnapshotSync(snap, { fs, path });
await snapshot.fromSnapshot(snap, { fs: fs.promises, path });
```

## Binary snapshot
Expand All @@ -47,15 +47,15 @@ Binary snapshots are encoded as CBOR `Uint8Array` buffers. You can convert any
folder of an `fs`-like file system into a `Uint8Array` snapshot.

```ts
const uint8 = snapshot.toBinarySnapshotSync({ fs, dir });
const uint8 = await snapshot.toBinarySnapshot({ fs: fs.promises, dir });
const uint8 = snapshot.toBinarySnapshotSync({ fs, path });
const uint8 = await snapshot.toBinarySnapshot({ fs: fs.promises, path });
```

Then import it back from `Uint8Array` snapshot.

```ts
snapshot.fromBinarySnapshotSync(uint8, { fs, dir });
await snapshot.fromBinarySnapshot(uint8, { fs: fs.promises, dir });
snapshot.fromBinarySnapshotSync(uint8, { fs, path });
await snapshot.fromBinarySnapshot(uint8, { fs: fs.promises, path });
```

## JSON snapshot
Expand All @@ -67,15 +67,15 @@ data is encoded as Base64 data URL strings. The resulting JSON is returned as
You can convert any folder of an `fs`-like file system into a `Uint8Array` snapshot.

```ts
const uint8 = snapshot.toJsonSnapshotSync({ fs, dir });
const uint8 = await snapshot.toJsonSnapshot({ fs: fs.promises, dir });
const uint8 = snapshot.toJsonSnapshotSync({ fs, path });
const uint8 = await snapshot.toJsonSnapshot({ fs: fs.promises, path });
```

Then import it back from `Uint8Array` snapshot.

```ts
snapshot.fromJsonSnapshotSync(uint8, { fs, dir });
await snapshot.fromJsonSnapshot(uint8, { fs: fs.promises, dir });
snapshot.fromJsonSnapshotSync(uint8, { fs, path });
await snapshot.fromJsonSnapshot(uint8, { fs: fs.promises, path });
```

## Encoding format
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "memfs",
"version": "4.11.0",
"version": "4.12.0",
"description": "In-memory file-system with Node's fs API.",
"keywords": [
"fs",
Expand Down
2 changes: 2 additions & 0 deletions src/Dirent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export class Dirent implements IDirent {
dirent.name = strToEncoding(link.getName(), encoding);
dirent.mode = mode;
dirent.path = link.getParentPath();
dirent.parentPath = dirent.path;

return dirent;
}

name: TDataOut = '';
path = '';
parentPath = '';
private mode: number = 0;

private _checkModeProperty(property: number): boolean {
Expand Down
12 changes: 8 additions & 4 deletions src/__tests__/promises.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,22 @@ describe('Promises API', () => {
});
});
describe('chmod(mode)', () => {
const vol = new Volume();
const { promises } = vol;
vol.fromJSON({
'/foo': 'bar',
let vol;
beforeEach(() => {
vol = new Volume();
vol.fromJSON({
'/foo': 'bar',
});
});
it('Change mode of existing file', async () => {
const { promises } = vol;
const fileHandle = await promises.open('/foo', 'a');
await fileHandle.chmod(0o444);
expect(vol.statSync('/foo').mode & 0o777).toEqual(0o444);
await fileHandle.close();
});
it('Reject when the file handle was closed', async () => {
const { promises } = vol;
const fileHandle = await promises.open('/foo', 'a');
await fileHandle.close();
return expect(fileHandle.chmod(0o666)).rejects.toBeInstanceOf(Error);
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { createFsFromVolume, Volume } from '..';
import { Link, Node } from '../node';

// Turn the done callback into an incremental one that will only fire after being called
// `times` times, failing with the first reported error if such exists.
// Useful for testing callback-style functions with several different fixtures without
// having to clutter the test suite with a multitude of individual tests (like it.each would).
export const multitest = (_done: (err?: Error) => void, times: number) => {
let err;
return function done(_err?: Error) {
err ??= _err;
if (!--times) _done(_err);
};
};

export const create = (json: { [s: string]: string } = { '/foo': 'bar' }) => {
const vol = Volume.fromJSON(json);
return vol;
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/volume/ReadStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,46 @@ describe('ReadStream', () => {
done();
});
});

it('should emit EACCES error when file has insufficient permissions', done => {
const fs = createFs({ '/test': 'test' });
fs.chmodSync('/test', 0o333); // wx
new fs.ReadStream('/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error when containing directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/foo', 0o666); // rw
new fs.ReadStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error when intermediate directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/', 0o666); // rw
new fs.ReadStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});
});
56 changes: 56 additions & 0 deletions src/__tests__/volume/WriteStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,60 @@ describe('WriteStream', () => {
done();
});
});

it('should emit EACCES error when file has insufficient permissions', done => {
const fs = createFs({ '/test': 'test' });
fs.chmodSync('/test', 0o555); // rx
new fs.WriteStream('/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for an existing file when containing directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/foo', 0o666); // rw
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for when intermediate directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/', 0o666); // rw
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for a non-existent file when containing directory has insufficient permissions', done => {
const fs = createFs({});
fs.mkdirSync('/foo', { mode: 0o555 }); // rx
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});
});
53 changes: 52 additions & 1 deletion src/__tests__/volume/appendFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create } from '../util';
import { create, multitest } from '../util';

describe('appendFile(file, data[, options], callback)', () => {
it('Simple write to non-existing file', done => {
Expand All @@ -15,4 +15,55 @@ describe('appendFile(file, data[, options], callback)', () => {
done();
});
});

it('Appending gives EACCES without sufficient permissions on the file', done => {
const vol = create({ '/foo': 'foo' });
vol.chmodSync('/foo', 0o555); // rx across the board
vol.appendFile('/foo', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});

it('Appending gives EACCES if file does not exist and containing directory has insufficient permissions', _done => {
const perms = [
0o555, // rx across the board
0o666, // rw across the board
];
const done = multitest(_done, perms.length);

perms.forEach(perm => {
const vol = create({});
vol.mkdirSync('/foo', { mode: perm });
vol.appendFile('/foo/test', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});
});

it('Appending gives EACCES if intermediate directory has insufficient permissions', done => {
const vol = create({});
vol.mkdirSync('/foo');
vol.chmodSync('/', 0o666); // rw
vol.appendFile('/foo/test', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});
});
29 changes: 29 additions & 0 deletions src/__tests__/volume/appendFileSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,33 @@ describe('appendFileSync(file, data, options)', () => {
vol.appendFileSync('/a', 'c');
expect(vol.readFileSync('/a', 'utf8')).toEqual('bc');
});
it('Appending throws EACCES without sufficient permissions on the file', () => {
const vol = create({ '/foo': 'foo' });
vol.chmodSync('/foo', 0o555); // rx across the board
expect(() => {
vol.appendFileSync('/foo', 'bar');
}).toThrowError(/EACCES/);
});
it('Appending throws EACCES if file does not exist and containing directory has insufficient permissions', () => {
const perms = [
0o555, // rx across the board
// 0o666, // rw across the board
// 0o111, // x
// 0o222 // w
];
perms.forEach(perm => {
const vol = create({});
vol.mkdirSync('/foo', perm);
expect(() => {
vol.appendFileSync('/foo/test', 'bar');
}).toThrowError(/EACCES/);
});
});
it('Appending throws EACCES if intermediate directory has insufficient permissions', () => {
const vol = create({ '/foo/test': 'test' });
vol.chmodSync('/', 0o666); // rw
expect(() => {
vol.appendFileSync('/foo/test', 'bar');
}).toThrowError(/EACCES/);
});
});
Loading