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

Avoid corrupting stored image data in edit sessions #157582

Merged
merged 3 commits into from
Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/vs/platform/files/common/fileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from 'vs/base/c
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability } from 'vs/platform/files/common/files';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, hasFileReadCapability, IFileSystemProviderWithFileReadCapability } from 'vs/platform/files/common/files';
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { ILogService } from 'vs/platform/log/common/log';

Expand Down Expand Up @@ -146,14 +146,14 @@ export class FileService extends Disposable implements IFileService {
return provider;
}

private async withReadProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability> {
private async withReadProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability | IFileSystemProviderWithFileReadCapability> {
const provider = await this.withProvider(resource);

if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider) || hasFileReadStreamCapability(provider)) {
if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider) || hasFileReadStreamCapability(provider) || hasFileReadCapability(provider)) {
return provider;
}

throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`);
throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream, FileOpenReadWriteClose nor Readonly capability which is needed for the read operation.`);
}

private async withWriteProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability> {
Expand Down Expand Up @@ -464,7 +464,7 @@ export class FileService extends Disposable implements IFileService {
return this.doReadFile(provider, resource, options, token);
}

private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability | IFileSystemProviderWithFileReadCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
return new Promise<IFileContent>((resolve, reject) => {
this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
try {
Expand All @@ -477,7 +477,7 @@ export class FileService extends Disposable implements IFileService {
});
}

private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability | IFileSystemProviderWithFileReadCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
const stream = await this.doReadFileStream(provider, resource, {
...options,
// optimization: since we know that the caller does not
Expand All @@ -500,7 +500,7 @@ export class FileService extends Disposable implements IFileService {
return this.doReadFileStream(provider, resource, options, token);
}

private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {
private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability | IFileSystemProviderWithFileReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {

// install a cancellation token that gets cancelled
// when any error occurs. this allows us to resolve
Expand Down Expand Up @@ -596,7 +596,7 @@ export class FileService extends Disposable implements IFileService {
return stream;
}

private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability | IFileSystemProviderWithFileReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));

// Read the file into the stream async but do not wait for
Expand Down
8 changes: 8 additions & 0 deletions src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,14 @@ export function hasFileReadStreamCapability(provider: IFileSystemProvider): prov
return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadStream);
}

export interface IFileSystemProviderWithFileReadCapability extends IFileSystemProvider {
readFile(resource: URI): Promise<Uint8Array>;
}

export function hasFileReadCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileReadCapability {
return !!(provider.capabilities & FileSystemProviderCapabilities.Readonly);
}

export interface IFileSystemProviderWithFileAtomicReadCapability extends IFileSystemProvider {
readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise<Uint8Array>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { Action2, IAction2Options, registerAction2 } from 'vs/platform/actions/common/actions';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { IEditSessionsWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_DATA_VIEW_ID } from 'vs/workbench/contrib/editSessions/common/editSessions';
import { IEditSessionsWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_DATA_VIEW_ID, decodeEditSessionFileContent } from 'vs/workbench/contrib/editSessions/common/editSessions';
import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { joinPath, relativePath } from 'vs/base/common/resources';
import { VSBuffer } from 'vs/base/common/buffer';
import { encodeBase64 } from 'vs/base/common/buffer';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { EditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService';
Expand Down Expand Up @@ -45,7 +45,7 @@ import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneCont
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditSessionsDataViews } from 'vs/workbench/contrib/editSessions/browser/editSessionsViews';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { EditSessionsContentProvider } from 'vs/workbench/contrib/editSessions/browser/editSessionsContentProvider';
import { EditSessionsFileSystemProvider } from 'vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider';
import { isNative } from 'vs/base/common/platform';
import { WorkspaceFolderCountContext } from 'vs/workbench/common/contextkeys';

Expand Down Expand Up @@ -148,7 +148,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo

this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService);

textModelResolverService.registerTextModelContentProvider(EDIT_SESSIONS_SCHEME, instantiationService.createInstance(EditSessionsContentProvider));
this._register(this.fileService.registerProvider(EditSessionsFileSystemProvider.SCHEMA, new EditSessionsFileSystemProvider(this.editSessionsWorkbenchService)));
}

private registerViews() {
Expand Down Expand Up @@ -363,7 +363,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo

for (const { uri, type, contents } of changes) {
if (type === ChangeType.Addition) {
await this.fileService.writeFile(uri, VSBuffer.fromString(contents!));
await this.fileService.writeFile(uri, decodeEditSessionFileContent(editSession.version, contents!));
} else if (type === ChangeType.Deletion && await this.fileService.exists(uri)) {
await this.fileService.del(uri);
}
Expand Down Expand Up @@ -410,7 +410,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
hasEdits = true;

if (await this.fileService.exists(uri)) {
workingChanges.push({ type: ChangeType.Addition, fileType: FileType.File, contents: (await this.fileService.readFile(uri)).value.toString(), relativeFilePath: relativeFilePath });
const contents = encodeBase64((await this.fileService.readFile(uri)).value);
workingChanges.push({ type: ChangeType.Addition, fileType: FileType.File, contents: contents, relativeFilePath: relativeFilePath });
} else {
// Assume it's a deletion
workingChanges.push({ type: ChangeType.Deletion, fileType: FileType.File, contents: undefined, relativeFilePath: relativeFilePath });
Expand All @@ -428,7 +429,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
return undefined;
}

const data: EditSession = { folders, version: 1 };
const data: EditSession = { folders, version: 2 };

try {
this.logService.info(`Storing edit session...`);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files';
import { decodeEditSessionFileContent, EDIT_SESSIONS_SCHEME, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions';

export class EditSessionsFileSystemProvider implements IFileSystemProviderWithFileReadCapability {

static readonly SCHEMA = EDIT_SESSIONS_SCHEME;

constructor(
@IEditSessionsWorkbenchService private editSessionsWorkbenchService: IEditSessionsWorkbenchService,
) { }

readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly;

async readFile(resource: URI): Promise<Uint8Array> {
const match = /(?<ref>[^/]+)\/(?<folderName>[^/]+)\/(?<filePath>.*)/.exec(resource.path.substring(1));
if (!match?.groups) {
throw FileSystemProviderErrorCode.FileNotFound;
}
const { ref, folderName, filePath } = match.groups;
const data = await this.editSessionsWorkbenchService.read(ref);
if (!data) {
throw FileSystemProviderErrorCode.FileNotFound;
}
const content = data?.editSession.folders.find((f) => f.name === folderName)?.workingChanges.find((change) => change.relativeFilePath === filePath)?.contents;
if (!content) {
throw FileSystemProviderErrorCode.FileNotFound;
}
return decodeEditSessionFileContent(data.editSession.version, content).buffer;
}

async stat(resource: URI): Promise<IStat> {
const content = await this.readFile(resource);
const currentTime = Date.now();
return {
type: FileType.File,
permissions: FilePermission.Readonly,
mtime: currentTime,
ctime: currentTime,
size: content.byteLength
};
}

//#region Unsupported file operations
readonly onDidChangeCapabilities = Event.None;
readonly onDidChangeFile = Event.None;

watch(resource: URI, opts: IWatchOptions): IDisposable { return Disposable.None; }

async mkdir(resource: URI): Promise<void> { }
async readdir(resource: URI): Promise<[string, FileType][]> { return []; }

async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> { }
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> { }
//#endregion
}
14 changes: 13 additions & 1 deletion src/vs/workbench/contrib/editSessions/common/editSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { decodeBase64, VSBuffer } from 'vs/base/common/buffer';
import { Codicon } from 'vs/base/common/codicons';
import { localize } from 'vs/nls';
import { ILocalizedString } from 'vs/platform/action/common/action';
Expand Down Expand Up @@ -60,7 +61,7 @@ export interface Folder {
workingChanges: Change[];
}

export const EditSessionSchemaVersion = 1;
export const EditSessionSchemaVersion = 2;

export interface EditSession {
version: number;
Expand All @@ -79,3 +80,14 @@ export const EDIT_SESSIONS_VIEW_ICON = registerIcon('edit-sessions-view-icon', C
export const EDIT_SESSIONS_SHOW_VIEW = new RawContextKey<boolean>('editSessionsShowView', false);

export const EDIT_SESSIONS_SCHEME = 'vscode-edit-sessions';

export function decodeEditSessionFileContent(version: number, content: string): VSBuffer {
switch (version) {
case 1:
return VSBuffer.fromString(content);
case 2:
return decodeBase64(content);
default:
throw new Error('Upgrade to a newer version to decode this content.');
}
}