Skip to content

Commit

Permalink
fix: readonly permissions with disk FS provider (#12354)
Browse files Browse the repository at this point in the history
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
  • Loading branch information
kittaakos authored Jul 21, 2023
1 parent 5b69857 commit 34eba25
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/filesystem/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"minimatch": "^5.1.0",
"multer": "1.4.4-lts.1",
"rimraf": "^2.6.2",
"stat-mode": "^1.0.0",
"tar-fs": "^1.16.2",
"trash": "^7.2.0",
"uuid": "^8.0.0",
Expand Down
109 changes: 109 additions & 0 deletions packages/filesystem/src/node/disk-file-system-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// *****************************************************************************
// Copyright (C) 2023 Arduino SA and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { EncodingService } from '@theia/core/lib/common/encoding-service';
import { ILogger } from '@theia/core/lib/common/logger';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { IPCConnectionProvider } from '@theia/core/lib/node/messaging/ipc-connection-provider';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { equal, fail } from 'assert';
import { promises as fs } from 'fs';
import { join } from 'path';
import * as temp from 'temp';
import { v4 } from 'uuid';
import { FilePermission, FileSystemProviderError, FileSystemProviderErrorCode } from '../common/files';
import { DiskFileSystemProvider } from './disk-file-system-provider';
import { bindFileSystemWatcherServer } from './filesystem-backend-module';

const tracked = temp.track();

describe('disk-file-system-provider', () => {
let toDisposeAfter: DisposableCollection;
let fsProvider: DiskFileSystemProvider;

before(() => {
fsProvider = createContainer().get<DiskFileSystemProvider>(
DiskFileSystemProvider
);
toDisposeAfter = new DisposableCollection(
fsProvider,
Disposable.create(() => tracked.cleanupSync())
);
});

after(() => {
toDisposeAfter.dispose();
});

describe('stat', () => {
it("should omit the 'permissions' property of the stat if the file can be both read and write", async () => {
const tempDirPath = tracked.mkdirSync();
const tempFilePath = join(tempDirPath, `${v4()}.txt`);
await fs.writeFile(tempFilePath, 'some content', { encoding: 'utf8' });

let content = await fs.readFile(tempFilePath, { encoding: 'utf8' });
equal(content, 'some content');

await fs.writeFile(tempFilePath, 'other content', { encoding: 'utf8' });

content = await fs.readFile(tempFilePath, { encoding: 'utf8' });
equal(content, 'other content');

const stat = await fsProvider.stat(FileUri.create(tempFilePath));
equal(stat.permissions, undefined);
});

it("should set the 'permissions' property to `Readonly` if the file can be read but not write", async () => {
const tempDirPath = tracked.mkdirSync();
const tempFilePath = join(tempDirPath, `${v4()}.txt`);
await fs.writeFile(tempFilePath, 'readonly content', {
encoding: 'utf8',
});

await fs.chmod(tempFilePath, '444'); // read-only for owner/group/world

try {
await fsProvider.writeFile(FileUri.create(tempFilePath), new Uint8Array(), { create: false, overwrite: true });
fail('Expected an EACCES error for readonly (chmod 444) files');
} catch (err) {
equal(err instanceof FileSystemProviderError, true);
equal((<FileSystemProviderError>err).code, FileSystemProviderErrorCode.NoPermissions);
}

const content = await fs.readFile(tempFilePath, { encoding: 'utf8' });
equal(content, 'readonly content');

const stat = await fsProvider.stat(FileUri.create(tempFilePath));
equal(stat.permissions, FilePermission.Readonly);
});
});

function createContainer(): Container {
const container = new Container({ defaultScope: 'Singleton' });
const module = new ContainerModule(bind => {
bind(DiskFileSystemProvider).toSelf().inSingletonScope();
bind(EncodingService).toSelf().inSingletonScope();
bindFileSystemWatcherServer(bind);
bind(MockLogger).toSelf().inSingletonScope();
bind(ILogger).toService(MockLogger);
bind(IPCConnectionProvider).toSelf().inSingletonScope();
});
container.load(module);
return container;
}
});
8 changes: 5 additions & 3 deletions packages/filesystem/src/node/disk-file-system-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
FileSystemProviderError,
FileChange,
WatchOptions,
FileUpdateOptions, FileUpdateResult, FileReadStreamOptions
FileUpdateOptions, FileUpdateResult, FileReadStreamOptions, FilePermission
} from '../common/files';
import { FileSystemWatcherServer } from '../common/filesystem-watcher-protocol';
import trash = require('trash');
Expand All @@ -65,6 +65,7 @@ import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { ReadableStreamEvents, newWriteableStream } from '@theia/core/lib/common/stream';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { readFileIntoStream } from '../common/io';
import { Mode } from 'stat-mode';

export namespace DiskFileSystemProvider {
export interface StatAndLink {
Expand Down Expand Up @@ -151,12 +152,13 @@ export class DiskFileSystemProvider implements Disposable,
async stat(resource: URI): Promise<Stat> {
try {
const { stat, symbolicLink } = await this.statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly

const mode = new Mode(stat);
return {
type: this.toType(stat, symbolicLink),
ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time
mtime: stat.mtime.getTime(),
size: stat.size
size: stat.size,
permissions: !mode.owner.write ? FilePermission.Readonly : undefined,
};
} catch (error) {
throw this.toFileSystemProviderError(error);
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10222,6 +10222,11 @@ ssri@9.0.1, ssri@^9.0.0:
dependencies:
minipass "^3.1.1"

stat-mode@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465"
integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==

statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
Expand Down

0 comments on commit 34eba25

Please sign in to comment.