@theia/filesystem 1.77.0-next.71 → 1.77.0-next.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/file-service-copy.spec.d.ts +2 -0
- package/lib/browser/file-service-copy.spec.d.ts.map +1 -0
- package/lib/browser/file-service-copy.spec.js +196 -0
- package/lib/browser/file-service-copy.spec.js.map +1 -0
- package/lib/browser/file-service.js +2 -2
- package/package.json +3 -3
- package/src/browser/file-service-copy.spec.ts +226 -0
- package/src/browser/file-service.ts +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-service-copy.spec.d.ts","sourceRoot":"","sources":["../../src/browser/file-service-copy.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 Sahil Gupta and others.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const tslib_1 = require("tslib");
|
|
19
|
+
const jsdom_1 = require("@theia/core/lib/browser/test/jsdom");
|
|
20
|
+
let disableJSDOM = (0, jsdom_1.enableJSDOM)();
|
|
21
|
+
const frontend_application_config_provider_1 = require("@theia/core/lib/browser/frontend-application-config-provider");
|
|
22
|
+
frontend_application_config_provider_1.FrontendApplicationConfigProvider.set({});
|
|
23
|
+
const core_1 = require("@theia/core");
|
|
24
|
+
const chai_1 = require("chai");
|
|
25
|
+
const sinon = tslib_1.__importStar(require("sinon"));
|
|
26
|
+
const file_service_1 = require("./file-service");
|
|
27
|
+
disableJSDOM();
|
|
28
|
+
function createMockProvider(caseSensitive) {
|
|
29
|
+
return {
|
|
30
|
+
capabilities: caseSensitive ? 1024 /* FileSystemProviderCapabilities.PathCaseSensitive */ : 0,
|
|
31
|
+
onDidChangeCapabilities: () => core_1.Disposable.NULL,
|
|
32
|
+
onDidChangeFile: () => core_1.Disposable.NULL,
|
|
33
|
+
onFileWatchError: () => core_1.Disposable.NULL,
|
|
34
|
+
watch: () => core_1.Disposable.NULL,
|
|
35
|
+
stat: () => { throw new Error('not implemented'); },
|
|
36
|
+
readdir: () => { throw new Error('not implemented'); },
|
|
37
|
+
readFile: () => { throw new Error('not implemented'); },
|
|
38
|
+
writeFile: () => { throw new Error('not implemented'); },
|
|
39
|
+
delete: () => { throw new Error('not implemented'); },
|
|
40
|
+
mkdir: () => { throw new Error('not implemented'); },
|
|
41
|
+
rename: () => { throw new Error('not implemented'); },
|
|
42
|
+
copy: () => { throw new Error('not implemented'); },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Resolve to the rejection reason of the given promise, or to `undefined` if it fulfills. */
|
|
46
|
+
async function rejection(promise) {
|
|
47
|
+
try {
|
|
48
|
+
await promise;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
describe('FileService move/copy validation', () => {
|
|
56
|
+
const sandbox = sinon.createSandbox();
|
|
57
|
+
let fileService;
|
|
58
|
+
let provider;
|
|
59
|
+
let existsStub;
|
|
60
|
+
before(() => {
|
|
61
|
+
disableJSDOM = (0, jsdom_1.enableJSDOM)();
|
|
62
|
+
});
|
|
63
|
+
after(() => {
|
|
64
|
+
disableJSDOM();
|
|
65
|
+
});
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
sandbox.restore();
|
|
68
|
+
});
|
|
69
|
+
/** Create a `FileService` backed by a single mock provider for both source and target. */
|
|
70
|
+
function setup(caseSensitive = true) {
|
|
71
|
+
sandbox.restore();
|
|
72
|
+
fileService = new file_service_1.FileService();
|
|
73
|
+
provider = createMockProvider(caseSensitive);
|
|
74
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
75
|
+
sandbox.stub(fileService, 'withProvider').resolves(provider);
|
|
76
|
+
sandbox.stub(fileService, 'withReadProvider').resolves(provider);
|
|
77
|
+
sandbox.stub(fileService, 'withWriteProvider').resolves(provider);
|
|
78
|
+
// The error messages name the involved resources through the label provider.
|
|
79
|
+
fileService.labelProvider = { getLongName: (resource) => resource.path.toString() };
|
|
80
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
81
|
+
// By default the target does not exist yet, which is the case when pasting a folder into one of its own subfolders.
|
|
82
|
+
existsStub = sandbox.stub(fileService, 'exists').resolves(false);
|
|
83
|
+
}
|
|
84
|
+
function validate(source, target, mode) {
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
return fileService.doValidateMoveCopy(provider, new core_1.URI(source), provider, new core_1.URI(target), mode, false);
|
|
87
|
+
}
|
|
88
|
+
describe('source containing the target', () => {
|
|
89
|
+
it('should reject copying a folder into a subfolder of itself', async () => {
|
|
90
|
+
setup();
|
|
91
|
+
const error = await rejection(validate('file:///a', 'file:///a/b/a', 'copy'));
|
|
92
|
+
(0, chai_1.expect)(error, 'copying a folder into its own subfolder was allowed').to.be.an.instanceOf(Error);
|
|
93
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
94
|
+
});
|
|
95
|
+
it('should reject moving a folder into a subfolder of itself', async () => {
|
|
96
|
+
setup();
|
|
97
|
+
const error = await rejection(validate('file:///a', 'file:///a/b', 'move'));
|
|
98
|
+
(0, chai_1.expect)(error, 'moving a folder into its own subfolder was allowed').to.be.an.instanceOf(Error);
|
|
99
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
100
|
+
});
|
|
101
|
+
it('should reject a deeply nested target inside the source', async () => {
|
|
102
|
+
setup();
|
|
103
|
+
const error = await rejection(validate('file:///a', 'file:///a/b/c/d/a', 'copy'));
|
|
104
|
+
(0, chai_1.expect)(error, 'copying a folder deep into its own subtree was allowed').to.be.an.instanceOf(Error);
|
|
105
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
106
|
+
});
|
|
107
|
+
it('should reject copying a folder into its own subfolder through FileService.copy', async () => {
|
|
108
|
+
setup();
|
|
109
|
+
const error = await rejection(fileService.copy(new core_1.URI('file:///a'), new core_1.URI('file:///a/b/a'), { fromUserGesture: false }));
|
|
110
|
+
(0, chai_1.expect)(error, 'FileService.copy allowed a copy into the source subtree').to.be.an.instanceOf(Error);
|
|
111
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
112
|
+
});
|
|
113
|
+
it('should reject moving a folder into its own subfolder through FileService.move', async () => {
|
|
114
|
+
setup();
|
|
115
|
+
const error = await rejection(fileService.move(new core_1.URI('file:///a'), new core_1.URI('file:///a/b'), { fromUserGesture: false }));
|
|
116
|
+
(0, chai_1.expect)(error, 'FileService.move allowed a move into the source subtree').to.be.an.instanceOf(Error);
|
|
117
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
118
|
+
});
|
|
119
|
+
it('should reject a target inside the source that only differs in case on a case-insensitive provider', async () => {
|
|
120
|
+
setup(false);
|
|
121
|
+
const error = await rejection(validate('file:///a', 'file:///A/b/a', 'copy'));
|
|
122
|
+
(0, chai_1.expect)(error, 'a case-insensitive provider allowed a copy into the source subtree').to.be.an.instanceOf(Error);
|
|
123
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('the same resource', () => {
|
|
127
|
+
it('should reject copying a resource onto itself', async () => {
|
|
128
|
+
setup();
|
|
129
|
+
const error = await rejection(validate('file:///a/b', 'file:///a/b', 'copy'));
|
|
130
|
+
(0, chai_1.expect)(error, 'copying a resource onto itself was allowed').to.be.an.instanceOf(Error);
|
|
131
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
132
|
+
});
|
|
133
|
+
it('should reject moving a resource onto itself', async () => {
|
|
134
|
+
setup();
|
|
135
|
+
const error = await rejection(validate('file:///a/b', 'file:///a/b', 'move'));
|
|
136
|
+
(0, chai_1.expect)(error, 'moving a resource onto itself was allowed').to.be.an.instanceOf(Error);
|
|
137
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('unrelated and adjacent targets', () => {
|
|
141
|
+
it('should accept an unrelated target', async () => {
|
|
142
|
+
setup();
|
|
143
|
+
const result = await validate('file:///a', 'file:///c/a', 'copy');
|
|
144
|
+
(0, chai_1.expect)(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
145
|
+
});
|
|
146
|
+
it('should accept a target whose path merely shares a prefix with the source', async () => {
|
|
147
|
+
setup();
|
|
148
|
+
const result = await validate('file:///a', 'file:///ab/a', 'copy');
|
|
149
|
+
(0, chai_1.expect)(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
150
|
+
});
|
|
151
|
+
it('should accept a target in a different file system', async () => {
|
|
152
|
+
setup();
|
|
153
|
+
const result = await validate('file:///a', 'memory:///a/b/a', 'copy');
|
|
154
|
+
(0, chai_1.expect)(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('the target containing the source', () => {
|
|
158
|
+
it('should reject moving a resource onto the existing folder that contains it', async () => {
|
|
159
|
+
setup();
|
|
160
|
+
existsStub.resolves(true);
|
|
161
|
+
const error = await rejection(validate('file:///a/b', 'file:///a', 'move'));
|
|
162
|
+
(0, chai_1.expect)(error, 'moving a resource onto its own parent folder was allowed').to.be.an.instanceOf(Error);
|
|
163
|
+
(0, chai_1.expect)(error.message).to.contain('would replace the folder it is contained in');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('case-only renames on a case-insensitive provider', () => {
|
|
167
|
+
it('should accept a case-only rename', async () => {
|
|
168
|
+
setup(false);
|
|
169
|
+
const result = await validate('file:///a/Foo', 'file:///a/foo', 'move');
|
|
170
|
+
(0, chai_1.expect)(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: true });
|
|
171
|
+
});
|
|
172
|
+
it('should accept a case-only rename of a nested resource', async () => {
|
|
173
|
+
setup(false);
|
|
174
|
+
const result = await validate('file:///a/b/Foo.ts', 'file:///a/b/foo.ts', 'move');
|
|
175
|
+
(0, chai_1.expect)(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: true });
|
|
176
|
+
});
|
|
177
|
+
it('should accept a case-only rename even when the target is reported as existing', async () => {
|
|
178
|
+
setup(false);
|
|
179
|
+
existsStub.resolves(true);
|
|
180
|
+
const result = await validate('file:///a/Foo', 'file:///a/foo', 'move');
|
|
181
|
+
(0, chai_1.expect)(result).to.deep.equal({ exists: true, isSameResourceWithDifferentPathCase: true });
|
|
182
|
+
});
|
|
183
|
+
it('should reject a case-only copy', async () => {
|
|
184
|
+
setup(false);
|
|
185
|
+
const error = await rejection(validate('file:///a/Foo', 'file:///a/foo', 'copy'));
|
|
186
|
+
(0, chai_1.expect)(error, 'a case-only copy was allowed').to.be.an.instanceOf(Error);
|
|
187
|
+
(0, chai_1.expect)(error.message).to.contain('is parent of target');
|
|
188
|
+
});
|
|
189
|
+
it('should accept a case-only rename on a case-sensitive provider as a distinct resource', async () => {
|
|
190
|
+
setup(true);
|
|
191
|
+
const result = await validate('file:///a/Foo', 'file:///a/foo', 'move');
|
|
192
|
+
(0, chai_1.expect)(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=file-service-copy.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-service-copy.spec.js","sourceRoot":"","sources":["../../src/browser/file-service-copy.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -874,7 +874,7 @@ let FileService = class FileService {
|
|
|
874
874
|
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
|
|
875
875
|
throw new Error(core_1.nls.localizeByDefault("Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
|
|
876
876
|
}
|
|
877
|
-
if (!isSameResourceWithDifferentPathCase &&
|
|
877
|
+
if (!isSameResourceWithDifferentPathCase && source.isEqualOrParent(target, isPathCaseSensitive)) {
|
|
878
878
|
throw new Error(core_1.nls.localizeByDefault("Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
|
|
879
879
|
}
|
|
880
880
|
}
|
|
@@ -885,7 +885,7 @@ let FileService = class FileService {
|
|
|
885
885
|
// it as it would delete the source as well. In this case we have to throw
|
|
886
886
|
if (sourceProvider === targetProvider) {
|
|
887
887
|
const isPathCaseSensitive = !!(sourceProvider.capabilities & 1024 /* FileSystemProviderCapabilities.PathCaseSensitive */);
|
|
888
|
-
if (
|
|
888
|
+
if (target.isEqualOrParent(source, isPathCaseSensitive)) {
|
|
889
889
|
throw new Error(core_1.nls.localizeByDefault("Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target)));
|
|
890
890
|
}
|
|
891
891
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/filesystem",
|
|
3
|
-
"version": "1.77.0-next.
|
|
3
|
+
"version": "1.77.0-next.82+71ee592ea",
|
|
4
4
|
"description": "Theia - FileSystem Extension",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@theia/core": "1.77.0-next.
|
|
6
|
+
"@theia/core": "1.77.0-next.82+71ee592ea",
|
|
7
7
|
"@types/body-parser": "^1.19.6",
|
|
8
8
|
"@types/multer": "^1.4.13",
|
|
9
9
|
"@types/tar-fs": "^1.16.3",
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"nyc": {
|
|
83
83
|
"extends": "../../configs/nyc.json"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "71ee592eab40c0e7c1d808aef884b29db956097e"
|
|
86
86
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Sahil Gupta and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
|
|
20
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
21
|
+
FrontendApplicationConfigProvider.set({});
|
|
22
|
+
|
|
23
|
+
import { Disposable, URI } from '@theia/core';
|
|
24
|
+
import { expect } from 'chai';
|
|
25
|
+
import * as sinon from 'sinon';
|
|
26
|
+
import { FileSystemProvider, FileSystemProviderCapabilities } from '../common/files';
|
|
27
|
+
import { FileService } from './file-service';
|
|
28
|
+
|
|
29
|
+
disableJSDOM();
|
|
30
|
+
|
|
31
|
+
function createMockProvider(caseSensitive: boolean): FileSystemProvider {
|
|
32
|
+
return {
|
|
33
|
+
capabilities: caseSensitive ? FileSystemProviderCapabilities.PathCaseSensitive : 0,
|
|
34
|
+
onDidChangeCapabilities: () => Disposable.NULL,
|
|
35
|
+
onDidChangeFile: () => Disposable.NULL,
|
|
36
|
+
onFileWatchError: () => Disposable.NULL,
|
|
37
|
+
watch: () => Disposable.NULL,
|
|
38
|
+
stat: () => { throw new Error('not implemented'); },
|
|
39
|
+
readdir: () => { throw new Error('not implemented'); },
|
|
40
|
+
readFile: () => { throw new Error('not implemented'); },
|
|
41
|
+
writeFile: () => { throw new Error('not implemented'); },
|
|
42
|
+
delete: () => { throw new Error('not implemented'); },
|
|
43
|
+
mkdir: () => { throw new Error('not implemented'); },
|
|
44
|
+
rename: () => { throw new Error('not implemented'); },
|
|
45
|
+
copy: () => { throw new Error('not implemented'); },
|
|
46
|
+
} as unknown as FileSystemProvider;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve to the rejection reason of the given promise, or to `undefined` if it fulfills. */
|
|
50
|
+
async function rejection(promise: Promise<unknown>): Promise<Error | undefined> {
|
|
51
|
+
try {
|
|
52
|
+
await promise;
|
|
53
|
+
return undefined;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return error as Error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('FileService move/copy validation', () => {
|
|
60
|
+
const sandbox = sinon.createSandbox();
|
|
61
|
+
let fileService: FileService;
|
|
62
|
+
let provider: FileSystemProvider;
|
|
63
|
+
let existsStub: sinon.SinonStub;
|
|
64
|
+
|
|
65
|
+
before(() => {
|
|
66
|
+
disableJSDOM = enableJSDOM();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
after(() => {
|
|
70
|
+
disableJSDOM();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
sandbox.restore();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Create a `FileService` backed by a single mock provider for both source and target. */
|
|
78
|
+
function setup(caseSensitive: boolean = true): void {
|
|
79
|
+
sandbox.restore();
|
|
80
|
+
fileService = new FileService();
|
|
81
|
+
provider = createMockProvider(caseSensitive);
|
|
82
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
83
|
+
sandbox.stub(fileService as any, 'withProvider').resolves(provider);
|
|
84
|
+
sandbox.stub(fileService as any, 'withReadProvider').resolves(provider);
|
|
85
|
+
sandbox.stub(fileService as any, 'withWriteProvider').resolves(provider);
|
|
86
|
+
// The error messages name the involved resources through the label provider.
|
|
87
|
+
(fileService as any).labelProvider = { getLongName: (resource: URI) => resource.path.toString() };
|
|
88
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
89
|
+
// By default the target does not exist yet, which is the case when pasting a folder into one of its own subfolders.
|
|
90
|
+
existsStub = sandbox.stub(fileService, 'exists').resolves(false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validate(source: string, target: string, mode: 'move' | 'copy'): Promise<unknown> {
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
return (fileService as any).doValidateMoveCopy(provider, new URI(source), provider, new URI(target), mode, false);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe('source containing the target', () => {
|
|
99
|
+
|
|
100
|
+
it('should reject copying a folder into a subfolder of itself', async () => {
|
|
101
|
+
setup();
|
|
102
|
+
const error = await rejection(validate('file:///a', 'file:///a/b/a', 'copy'));
|
|
103
|
+
expect(error, 'copying a folder into its own subfolder was allowed').to.be.an.instanceOf(Error);
|
|
104
|
+
expect(error!.message).to.contain('is parent of target');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should reject moving a folder into a subfolder of itself', async () => {
|
|
108
|
+
setup();
|
|
109
|
+
const error = await rejection(validate('file:///a', 'file:///a/b', 'move'));
|
|
110
|
+
expect(error, 'moving a folder into its own subfolder was allowed').to.be.an.instanceOf(Error);
|
|
111
|
+
expect(error!.message).to.contain('is parent of target');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should reject a deeply nested target inside the source', async () => {
|
|
115
|
+
setup();
|
|
116
|
+
const error = await rejection(validate('file:///a', 'file:///a/b/c/d/a', 'copy'));
|
|
117
|
+
expect(error, 'copying a folder deep into its own subtree was allowed').to.be.an.instanceOf(Error);
|
|
118
|
+
expect(error!.message).to.contain('is parent of target');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should reject copying a folder into its own subfolder through FileService.copy', async () => {
|
|
122
|
+
setup();
|
|
123
|
+
const error = await rejection(fileService.copy(new URI('file:///a'), new URI('file:///a/b/a'), { fromUserGesture: false }));
|
|
124
|
+
expect(error, 'FileService.copy allowed a copy into the source subtree').to.be.an.instanceOf(Error);
|
|
125
|
+
expect(error!.message).to.contain('is parent of target');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should reject moving a folder into its own subfolder through FileService.move', async () => {
|
|
129
|
+
setup();
|
|
130
|
+
const error = await rejection(fileService.move(new URI('file:///a'), new URI('file:///a/b'), { fromUserGesture: false }));
|
|
131
|
+
expect(error, 'FileService.move allowed a move into the source subtree').to.be.an.instanceOf(Error);
|
|
132
|
+
expect(error!.message).to.contain('is parent of target');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should reject a target inside the source that only differs in case on a case-insensitive provider', async () => {
|
|
136
|
+
setup(false);
|
|
137
|
+
const error = await rejection(validate('file:///a', 'file:///A/b/a', 'copy'));
|
|
138
|
+
expect(error, 'a case-insensitive provider allowed a copy into the source subtree').to.be.an.instanceOf(Error);
|
|
139
|
+
expect(error!.message).to.contain('is parent of target');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('the same resource', () => {
|
|
144
|
+
|
|
145
|
+
it('should reject copying a resource onto itself', async () => {
|
|
146
|
+
setup();
|
|
147
|
+
const error = await rejection(validate('file:///a/b', 'file:///a/b', 'copy'));
|
|
148
|
+
expect(error, 'copying a resource onto itself was allowed').to.be.an.instanceOf(Error);
|
|
149
|
+
expect(error!.message).to.contain('is parent of target');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should reject moving a resource onto itself', async () => {
|
|
153
|
+
setup();
|
|
154
|
+
const error = await rejection(validate('file:///a/b', 'file:///a/b', 'move'));
|
|
155
|
+
expect(error, 'moving a resource onto itself was allowed').to.be.an.instanceOf(Error);
|
|
156
|
+
expect(error!.message).to.contain('is parent of target');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('unrelated and adjacent targets', () => {
|
|
161
|
+
|
|
162
|
+
it('should accept an unrelated target', async () => {
|
|
163
|
+
setup();
|
|
164
|
+
const result = await validate('file:///a', 'file:///c/a', 'copy');
|
|
165
|
+
expect(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should accept a target whose path merely shares a prefix with the source', async () => {
|
|
169
|
+
setup();
|
|
170
|
+
const result = await validate('file:///a', 'file:///ab/a', 'copy');
|
|
171
|
+
expect(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should accept a target in a different file system', async () => {
|
|
175
|
+
setup();
|
|
176
|
+
const result = await validate('file:///a', 'memory:///a/b/a', 'copy');
|
|
177
|
+
expect(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('the target containing the source', () => {
|
|
182
|
+
|
|
183
|
+
it('should reject moving a resource onto the existing folder that contains it', async () => {
|
|
184
|
+
setup();
|
|
185
|
+
existsStub.resolves(true);
|
|
186
|
+
const error = await rejection(validate('file:///a/b', 'file:///a', 'move'));
|
|
187
|
+
expect(error, 'moving a resource onto its own parent folder was allowed').to.be.an.instanceOf(Error);
|
|
188
|
+
expect(error!.message).to.contain('would replace the folder it is contained in');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('case-only renames on a case-insensitive provider', () => {
|
|
193
|
+
|
|
194
|
+
it('should accept a case-only rename', async () => {
|
|
195
|
+
setup(false);
|
|
196
|
+
const result = await validate('file:///a/Foo', 'file:///a/foo', 'move');
|
|
197
|
+
expect(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: true });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should accept a case-only rename of a nested resource', async () => {
|
|
201
|
+
setup(false);
|
|
202
|
+
const result = await validate('file:///a/b/Foo.ts', 'file:///a/b/foo.ts', 'move');
|
|
203
|
+
expect(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: true });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should accept a case-only rename even when the target is reported as existing', async () => {
|
|
207
|
+
setup(false);
|
|
208
|
+
existsStub.resolves(true);
|
|
209
|
+
const result = await validate('file:///a/Foo', 'file:///a/foo', 'move');
|
|
210
|
+
expect(result).to.deep.equal({ exists: true, isSameResourceWithDifferentPathCase: true });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should reject a case-only copy', async () => {
|
|
214
|
+
setup(false);
|
|
215
|
+
const error = await rejection(validate('file:///a/Foo', 'file:///a/foo', 'copy'));
|
|
216
|
+
expect(error, 'a case-only copy was allowed').to.be.an.instanceOf(Error);
|
|
217
|
+
expect(error!.message).to.contain('is parent of target');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should accept a case-only rename on a case-sensitive provider as a distinct resource', async () => {
|
|
221
|
+
setup(true);
|
|
222
|
+
const result = await validate('file:///a/Foo', 'file:///a/foo', 'move');
|
|
223
|
+
expect(result).to.deep.equal({ exists: false, isSameResourceWithDifferentPathCase: false });
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -1282,7 +1282,7 @@ export class FileService {
|
|
|
1282
1282
|
throw new Error(nls.localizeByDefault("Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
|
|
1283
1283
|
}
|
|
1284
1284
|
|
|
1285
|
-
if (!isSameResourceWithDifferentPathCase &&
|
|
1285
|
+
if (!isSameResourceWithDifferentPathCase && source.isEqualOrParent(target, isPathCaseSensitive)) {
|
|
1286
1286
|
throw new Error(nls.localizeByDefault("Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
|
|
1287
1287
|
}
|
|
1288
1288
|
}
|
|
@@ -1295,7 +1295,7 @@ export class FileService {
|
|
|
1295
1295
|
// it as it would delete the source as well. In this case we have to throw
|
|
1296
1296
|
if (sourceProvider === targetProvider) {
|
|
1297
1297
|
const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
|
|
1298
|
-
if (
|
|
1298
|
+
if (target.isEqualOrParent(source, isPathCaseSensitive)) {
|
|
1299
1299
|
throw new Error(nls.localizeByDefault("Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target)));
|
|
1300
1300
|
}
|
|
1301
1301
|
}
|