Skip to content

Commit c7ec7ea

Browse files
committed
More tests
1 parent 0343b59 commit c7ec7ea

File tree

4 files changed

+92
-86
lines changed

4 files changed

+92
-86
lines changed

src/client/pythonEnvironments/common/environmentIdentifier.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { isPyenvEnvironment } from '../discovery/locators/services/pyenvLocator'
77
import { isVenvEnvironment } from '../discovery/locators/services/venvLocator';
88
import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator';
99
import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator';
10+
import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator';
1011
import { EnvironmentType } from '../info';
11-
import { isWindowsStoreEnvironment } from './windowsUtils';
1212

1313
/**
1414
* Gets a prioritized list of environment types for identification.

src/client/pythonEnvironments/common/windowsUtils.ts

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Licensed under the MIT License.
33

44
import * as path from 'path';
5-
import { traceWarning } from '../../common/logger';
6-
import { getEnvironmentVariable } from '../../common/utils/platform';
75

86
/**
97
* Checks if a given path ends with python*.exe
@@ -22,77 +20,3 @@ export function isWindowsPythonExe(interpreterPath:string): boolean {
2220

2321
return windowsPythonExes.test(path.basename(interpreterPath));
2422
}
25-
26-
/**
27-
* Gets path to the Windows Apps directory.
28-
* @returns {string} : Returns path to the Windows Apps directory under
29-
* `%LOCALAPPDATA%/Microsoft/WindowsApps`.
30-
*/
31-
export function getWindowsStoreAppsRoot(): string {
32-
const localAppData = getEnvironmentVariable('LOCALAPPDATA') || '';
33-
return path.join(localAppData, 'Microsoft', 'WindowsApps');
34-
}
35-
36-
/**
37-
* Checks if a given path is under the forbidden windows store directory.
38-
* @param {string} interpreterPath : Absolute path to the python interpreter.
39-
* @returns {boolean} : Returns true if `interpreterPath` is under
40-
* `%ProgramFiles%/WindowsApps`.
41-
*/
42-
export function isForbiddenStorePath(interpreterPath:string):boolean {
43-
const programFilesStorePath = path
44-
.join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps')
45-
.normalize()
46-
.toUpperCase();
47-
return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath);
48-
}
49-
50-
/**
51-
* Checks if the given interpreter belongs to Windows Store Python environment.
52-
* @param interpreterPath: Absolute path to any python interpreter.
53-
*
54-
* Remarks:
55-
* 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is
56-
* NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search
57-
* path. It is possible to get a false positive for that path. So the comparison should check if the
58-
* absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to
59-
* 'WindowsApps' is not a valid path to access, Windows Store Python.
60-
*
61-
* 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows.
62-
*
63-
* 3. A limitation of the checks here is that they don't handle 8.3 style windows paths.
64-
* For example,
65-
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
66-
* is the shortened form of
67-
* `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe`
68-
*
69-
* The correct way to compare these would be to always convert given paths to long path (or to short path).
70-
* For either approach to work correctly you need actual file to exist, and accessible from the user's
71-
* account.
72-
*
73-
* To convert to short path without using N-API in node would be to use this command. This is very expensive:
74-
* `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA`
75-
* The above command will print out this:
76-
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
77-
*
78-
* If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from,
79-
* Kernel32 to convert between the two path variants.
80-
*
81-
*/
82-
export async function isWindowsStoreEnvironment(interpreterPath: string): Promise<boolean> {
83-
const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase();
84-
const localAppDataStorePath = path
85-
.normalize(getWindowsStoreAppsRoot())
86-
.toUpperCase();
87-
if (pythonPathToCompare.includes(localAppDataStorePath)) {
88-
return true;
89-
}
90-
91-
// Program Files store path is a forbidden path. Only admins and system has access this path.
92-
// We should never have to look at this path or even execute python from this path.
93-
if (isForbiddenStorePath(pythonPathToCompare)) {
94-
traceWarning('isWindowsStoreEnvironment called with Program Files store path.');
95-
return true;
96-
}
97-
return false;
98-
}

src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,92 @@
44
import * as fsapi from 'fs-extra';
55
import * as path from 'path';
66
import { Event, EventEmitter } from 'vscode';
7-
import { Architecture } from '../../../../common/utils/platform';
7+
import { traceWarning } from '../../../../common/logger';
8+
import { Architecture, getEnvironmentVariable } from '../../../../common/utils/platform';
89
import {
910
PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion,
1011
} from '../../../base/info';
1112
import { parseVersion } from '../../../base/info/pythonVersion';
1213
import { ILocator, IPythonEnvsIterator } from '../../../base/locator';
1314
import { PythonEnvsChangedEvent } from '../../../base/watcher';
1415
import { getFileInfo } from '../../../common/externalDependencies';
15-
import { getWindowsStoreAppsRoot, isWindowsPythonExe, isWindowsStoreEnvironment } from '../../../common/windowsUtils';
16+
import { isWindowsPythonExe } from '../../../common/windowsUtils';
1617
import { IEnvironmentInfoService } from '../../../info/environmentInfoService';
1718

19+
/**
20+
* Gets path to the Windows Apps directory.
21+
* @returns {string} : Returns path to the Windows Apps directory under
22+
* `%LOCALAPPDATA%/Microsoft/WindowsApps`.
23+
*/
24+
export function getWindowsStoreAppsRoot(): string {
25+
const localAppData = getEnvironmentVariable('LOCALAPPDATA') || '';
26+
return path.join(localAppData, 'Microsoft', 'WindowsApps');
27+
}
28+
29+
/**
30+
* Checks if a given path is under the forbidden windows store directory.
31+
* @param {string} interpreterPath : Absolute path to the python interpreter.
32+
* @returns {boolean} : Returns true if `interpreterPath` is under
33+
* `%ProgramFiles%/WindowsApps`.
34+
*/
35+
export function isForbiddenStorePath(interpreterPath:string):boolean {
36+
const programFilesStorePath = path
37+
.join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps')
38+
.normalize()
39+
.toUpperCase();
40+
return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath);
41+
}
42+
43+
/**
44+
* Checks if the given interpreter belongs to Windows Store Python environment.
45+
* @param interpreterPath: Absolute path to any python interpreter.
46+
*
47+
* Remarks:
48+
* 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is
49+
* NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search
50+
* path. It is possible to get a false positive for that path. So the comparison should check if the
51+
* absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to
52+
* 'WindowsApps' is not a valid path to access, Windows Store Python.
53+
*
54+
* 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows.
55+
*
56+
* 3. A limitation of the checks here is that they don't handle 8.3 style windows paths.
57+
* For example,
58+
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
59+
* is the shortened form of
60+
* `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe`
61+
*
62+
* The correct way to compare these would be to always convert given paths to long path (or to short path).
63+
* For either approach to work correctly you need actual file to exist, and accessible from the user's
64+
* account.
65+
*
66+
* To convert to short path without using N-API in node would be to use this command. This is very expensive:
67+
* `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA`
68+
* The above command will print out this:
69+
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
70+
*
71+
* If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from,
72+
* Kernel32 to convert between the two path variants.
73+
*
74+
*/
75+
export async function isWindowsStoreEnvironment(interpreterPath: string): Promise<boolean> {
76+
const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase();
77+
const localAppDataStorePath = path
78+
.normalize(getWindowsStoreAppsRoot())
79+
.toUpperCase();
80+
if (pythonPathToCompare.includes(localAppDataStorePath)) {
81+
return true;
82+
}
83+
84+
// Program Files store path is a forbidden path. Only admins and system has access this path.
85+
// We should never have to look at this path or even execute python from this path.
86+
if (isForbiddenStorePath(pythonPathToCompare)) {
87+
traceWarning('isWindowsStoreEnvironment called with Program Files store path.');
88+
return true;
89+
}
90+
return false;
91+
}
92+
1893
/**
1994
* Gets paths to the Python executable under Windows Store apps.
2095
* @returns: Returns python*.exe for the windows store app root directory.
@@ -59,7 +134,7 @@ export class WindowsStoreLocator implements ILocator {
59134

60135
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
61136
const executablePath = typeof env === 'string' ? env : env.executable.filename;
62-
if (isWindowsStoreEnvironment(executablePath)) {
137+
if (await isWindowsStoreEnvironment(executablePath)) {
63138
const interpreterInfo = await this.envService.getEnvironmentInfo(executablePath);
64139
if (interpreterInfo) {
65140
const data = await getFileInfo(executablePath);
@@ -68,7 +143,6 @@ export class WindowsStoreLocator implements ILocator {
68143
...data,
69144
};
70145
return Promise.resolve({
71-
id: '',
72146
name: '',
73147
location: '',
74148
kind: this.kind,
@@ -100,7 +174,6 @@ export class WindowsStoreLocator implements ILocator {
100174
};
101175
}
102176
return {
103-
id: '',
104177
name: '',
105178
location: '',
106179
kind: this.kind,

src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ suite('Windows Store', () => {
156156
const data = pathToData.get(k);
157157
if (data) {
158158
return {
159-
id: '',
159+
160160
name: '',
161161
location: '',
162162
kind: PythonEnvKind.WindowsStore,
@@ -182,7 +182,7 @@ suite('Windows Store', () => {
182182
test('resolveEnv(string)', async () => {
183183
const python38path = path.join(testStoreAppRoot, 'python3.8.exe');
184184
const expected = {
185-
id: '',
185+
186186
name: '',
187187
location: '',
188188
kind: PythonEnvKind.WindowsStore,
@@ -200,7 +200,7 @@ suite('Windows Store', () => {
200200
test('resolveEnv(PythonEnvInfo)', async () => {
201201
const python38path = path.join(testStoreAppRoot, 'python3.8.exe');
202202
const expected = {
203-
id: '',
203+
204204
name: '',
205205
location: '',
206206
kind: PythonEnvKind.WindowsStore,
@@ -210,7 +210,6 @@ suite('Windows Store', () => {
210210

211211
// Partially filled in env info object
212212
const input:PythonEnvInfo = {
213-
id: '',
214213
name: '',
215214
location: '',
216215
kind: PythonEnvKind.WindowsStore,
@@ -236,5 +235,15 @@ suite('Windows Store', () => {
236235

237236
assertEnvEqual(actual, expected);
238237
});
238+
test('resolveEnv(string): Non store python', async () => {
239+
// Use a non store root path
240+
const python38path = path.join(testLocalAppData, 'python3.8.exe');
241+
242+
const envService = new EnvironmentInfoService();
243+
const locator = new WindowsStoreLocator(envService);
244+
const actual = await locator.resolveEnv(python38path);
245+
246+
assert.deepStrictEqual(actual, undefined);
247+
});
239248
});
240249
});

0 commit comments

Comments
 (0)