Ensure uris are using encoded strings (#653)
This fixes a bug where if there are special characters in a database path, it is not possible to navigate to that file from the results view. Note that the results from our BQRS returned properly encoded URIs, but our paths coming from sarif were unencoded. Our path parsing handled the latter correctly (even though these are not correct URIs) and the former incorrectly. The fix here is to first ensure all uris are properly encoded. We do this by running `encodeURI` in sarif-utils (can't run encodeURIComponent or else the path separators `/` will also be encoded). Then, we ensure that when we resolve locations, we decode all file paths. This works in all cases I have tried. I still have an issue with running View AST on some of these databases, but that I believe is a separate issue.
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
## 1.3.5 - 27 October 2020
|
## 1.3.5 - 27 October 2020
|
||||||
|
|
||||||
- Fix a bug where archived source folders for databases were not showing any contents.
|
- Fix a bug where archived source folders for databases were not showing any contents.
|
||||||
|
- Fix URI encoding for databases that were created with special characters in their paths.
|
||||||
|
|
||||||
## 1.3.4 - 22 October 2020
|
## 1.3.4 - 22 October 2020
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
|
|||||||
// This lets us separate the paths, ignoring the leading slash if we added one.
|
// This lets us separate the paths, ignoring the leading slash if we added one.
|
||||||
const sourceArchiveZipPathEndIndex = sourceArchiveZipPathStartIndex + sourceArchiveZipPath.length;
|
const sourceArchiveZipPathEndIndex = sourceArchiveZipPathStartIndex + sourceArchiveZipPath.length;
|
||||||
const authority = `${sourceArchiveZipPathStartIndex}-${sourceArchiveZipPathEndIndex}`;
|
const authority = `${sourceArchiveZipPathStartIndex}-${sourceArchiveZipPathEndIndex}`;
|
||||||
return vscode.Uri.parse(zipArchiveScheme + ':/').with({
|
return vscode.Uri.parse(zipArchiveScheme + ':/', true).with({
|
||||||
path: encodedPath,
|
path: encodedPath,
|
||||||
authority,
|
authority,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ export default function fileRangeFromURI(uri: UrlValue | undefined, db: Database
|
|||||||
Math.max(0, (loc.endLine || 0) - 1),
|
Math.max(0, (loc.endLine || 0) - 1),
|
||||||
Math.max(0, (loc.endColumn || 0)));
|
Math.max(0, (loc.endColumn || 0)));
|
||||||
try {
|
try {
|
||||||
const parsed = vscode.Uri.parse(uri.uri, true);
|
if (uri.uri.startsWith('file:')) {
|
||||||
if (parsed.scheme === 'file') {
|
return new vscode.Location(db.resolveSourceFile(uri.uri), range);
|
||||||
return new vscode.Location(db.resolveSourceFile(parsed.fsPath), range);
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function getLocationsForUriString(
|
|||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
filter: (src: string, dest: string) => boolean
|
filter: (src: string, dest: string) => boolean
|
||||||
): Promise<FullLocationLink[]> {
|
): Promise<FullLocationLink[]> {
|
||||||
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
|
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString, true));
|
||||||
const sourceArchiveUri = encodeArchiveBasePath(uri.sourceArchiveZipPath);
|
const sourceArchiveUri = encodeArchiveBasePath(uri.sourceArchiveZipPath);
|
||||||
|
|
||||||
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
|
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export class TemplatePrintAstProvider {
|
|||||||
|
|
||||||
return new AstBuilder(
|
return new AstBuilder(
|
||||||
queryResults, this.cli,
|
queryResults, this.cli,
|
||||||
this.dbm.findDatabaseItem(vscode.Uri.parse(queryResults.database.databaseUri!))!,
|
this.dbm.findDatabaseItem(vscode.Uri.parse(queryResults.database.databaseUri!, true))!,
|
||||||
document.fileName
|
document.fileName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,28 +341,29 @@ export class DatabaseItemImpl implements DatabaseItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public resolveSourceFile(uri: string | undefined): vscode.Uri {
|
public resolveSourceFile(uriStr: string | undefined): vscode.Uri {
|
||||||
const sourceArchive = this.sourceArchive;
|
const sourceArchive = this.sourceArchive;
|
||||||
// Sometimes, we are passed a path, sometimes a file URI.
|
const uri = uriStr ? vscode.Uri.parse(uriStr, true) : undefined;
|
||||||
// We need to convert this to a file path that is probably inside of a zip file.
|
if (uri && uri.scheme !== 'file') {
|
||||||
const file = uri?.replace(/file:/, '');
|
throw new Error(`Invalid uri scheme in ${uriStr}. Only 'file' is allowed.`);
|
||||||
|
}
|
||||||
if (!sourceArchive) {
|
if (!sourceArchive) {
|
||||||
if (file) {
|
if (uri) {
|
||||||
// Treat it as an absolute path.
|
return uri;
|
||||||
return vscode.Uri.file(file);
|
|
||||||
} else {
|
} else {
|
||||||
return this.databaseUri;
|
return this.databaseUri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file) {
|
if (uri) {
|
||||||
const absoluteFilePath = file.replace(':', '_');
|
const relativeFilePath = decodeURI(uri.path).replace(':', '_').replace(/^\/*/, '');
|
||||||
// Strip any leading slashes from the file path, and replace `:` with `_`.
|
|
||||||
const relativeFilePath = absoluteFilePath.replace(/^\/*/, '').replace(':', '_');
|
|
||||||
if (sourceArchive.scheme === zipArchiveScheme) {
|
if (sourceArchive.scheme === zipArchiveScheme) {
|
||||||
const zipRef = decodeSourceArchiveUri(sourceArchive);
|
const zipRef = decodeSourceArchiveUri(sourceArchive);
|
||||||
|
const pathWithinSourceArchive = zipRef.pathWithinSourceArchive === '/'
|
||||||
|
? relativeFilePath
|
||||||
|
: zipRef.pathWithinSourceArchive + '/' + relativeFilePath;
|
||||||
return encodeSourceArchiveUri({
|
return encodeSourceArchiveUri({
|
||||||
pathWithinSourceArchive: zipRef.pathWithinSourceArchive + '/' + absoluteFilePath,
|
pathWithinSourceArchive,
|
||||||
sourceArchiveZipPath: zipRef.sourceArchiveZipPath,
|
sourceArchiveZipPath: zipRef.sourceArchiveZipPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -579,7 +580,7 @@ export class DatabaseManager extends DisposableObject {
|
|||||||
displayName,
|
displayName,
|
||||||
dateAdded
|
dateAdded
|
||||||
};
|
};
|
||||||
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri), undefined, fullOptions,
|
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri, true), undefined, fullOptions,
|
||||||
(event) => {
|
(event) => {
|
||||||
this._onDidChangeDatabaseItem.fire(event);
|
this._onDidChangeDatabaseItem.fire(event);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ export interface WholeFileLocation {
|
|||||||
endColumn: never;
|
endColumn: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UrlValue = LineColumnLocation | WholeFileLocation | string;
|
|
||||||
|
|
||||||
export type ResolvableLocationValue = WholeFileLocation | LineColumnLocation;
|
export type ResolvableLocationValue = WholeFileLocation | LineColumnLocation;
|
||||||
|
|
||||||
|
export type UrlValue = ResolvableLocationValue | string;
|
||||||
|
|
||||||
export type ColumnValue = EntityValue | number | string | boolean;
|
export type ColumnValue = EntityValue | number | string | boolean;
|
||||||
|
|
||||||
export type ResultRow = ColumnValue[];
|
export type ResultRow = ColumnValue[];
|
||||||
|
|||||||
@@ -72,10 +72,28 @@ export function getPathRelativeToSourceLocationPrefix(
|
|||||||
sourceLocationPrefix: string,
|
sourceLocationPrefix: string,
|
||||||
sarifRelativeUri: string
|
sarifRelativeUri: string
|
||||||
) {
|
) {
|
||||||
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
|
// convert a platform specific path into encoded path uri segments
|
||||||
return `file:${normalizedSourceLocationPrefix}/${sarifRelativeUri}`;
|
// need to be careful about drive letters and ensure that there
|
||||||
|
// is a starting '/'
|
||||||
|
let prefix = '';
|
||||||
|
if (sourceLocationPrefix[1] === ':') {
|
||||||
|
// assume this is a windows drive separator
|
||||||
|
prefix = sourceLocationPrefix.substring(0, 2);
|
||||||
|
sourceLocationPrefix = sourceLocationPrefix.substring(2);
|
||||||
|
}
|
||||||
|
const normalizedSourceLocationPrefix = prefix + sourceLocationPrefix.replace(/\\/g, '/')
|
||||||
|
.split('/')
|
||||||
|
.map(encodeURIComponent)
|
||||||
|
.join('/');
|
||||||
|
const slashPrefix = normalizedSourceLocationPrefix.startsWith('/') ? '' : '/';
|
||||||
|
return `file:${slashPrefix + normalizedSourceLocationPrefix}/${sarifRelativeUri}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param loc specifies the database-relative location of the source location
|
||||||
|
* @param sourceLocationPrefix a file path (usually a full path) to the database containing the source location.
|
||||||
|
*/
|
||||||
export function parseSarifLocation(
|
export function parseSarifLocation(
|
||||||
loc: Sarif.Location,
|
loc: Sarif.Location,
|
||||||
sourceLocationPrefix: string
|
sourceLocationPrefix: string
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||||||
queryText: encodeURIComponent(await this.getQueryText(singleItem)),
|
queryText: encodeURIComponent(await this.getQueryText(singleItem)),
|
||||||
});
|
});
|
||||||
const uri = vscode.Uri.parse(
|
const uri = vscode.Uri.parse(
|
||||||
`codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`
|
`codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`, true
|
||||||
);
|
);
|
||||||
const doc = await vscode.workspace.openTextDocument(uri);
|
const doc = await vscode.workspace.openTextDocument(uri);
|
||||||
await vscode.window.showTextDocument(doc, { preview: false });
|
await vscode.window.showTextDocument(doc, { preview: false });
|
||||||
|
|||||||
@@ -8,10 +8,30 @@ import { DatabaseItem } from '../../../databases';
|
|||||||
import { WholeFileLocation, LineColumnLocation } from '../../../pure/bqrs-cli-types';
|
import { WholeFileLocation, LineColumnLocation } from '../../../pure/bqrs-cli-types';
|
||||||
|
|
||||||
describe('fileRangeFromURI', () => {
|
describe('fileRangeFromURI', () => {
|
||||||
it('should return undefined when value is a string', () => {
|
it('should return undefined when value is not a file URI', () => {
|
||||||
expect(fileRangeFromURI('hucairz', createMockDatabaseItem())).to.be.undefined;
|
expect(fileRangeFromURI('hucairz', createMockDatabaseItem())).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail to find a location when not a file URI and a full 5 part location', () => {
|
||||||
|
expect(fileRangeFromURI({
|
||||||
|
uri: 'https://yahoo.com',
|
||||||
|
startLine: 1,
|
||||||
|
startColumn: 2,
|
||||||
|
endLine: 3,
|
||||||
|
endColumn: 4,
|
||||||
|
} as LineColumnLocation, createMockDatabaseItem())).to.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to find a location when there is a silly protocol', () => {
|
||||||
|
expect(fileRangeFromURI({
|
||||||
|
uri: 'filesilly://yahoo.com',
|
||||||
|
startLine: 1,
|
||||||
|
startColumn: 2,
|
||||||
|
endLine: 3,
|
||||||
|
endColumn: 4,
|
||||||
|
} as LineColumnLocation, createMockDatabaseItem())).to.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
it('should return undefined when value is an empty uri', () => {
|
it('should return undefined when value is an empty uri', () => {
|
||||||
expect(fileRangeFromURI({
|
expect(fileRangeFromURI({
|
||||||
uri: 'file:/',
|
uri: 'file:/',
|
||||||
@@ -46,7 +66,7 @@ describe('fileRangeFromURI', () => {
|
|||||||
|
|
||||||
function createMockDatabaseItem(): DatabaseItem {
|
function createMockDatabaseItem(): DatabaseItem {
|
||||||
return {
|
return {
|
||||||
resolveSourceFile: (file: string) => Uri.file(file)
|
resolveSourceFile: (file: string) => Uri.parse(file)
|
||||||
} as DatabaseItem;
|
} as DatabaseItem;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,39 +87,31 @@ describe('databases', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('resolveSourceFile', () => {
|
describe('resolveSourceFile', () => {
|
||||||
describe('unzipped source archive', () => {
|
it('should fail to resolve when not a uri', () => {
|
||||||
it('should resolve a source file in an unzipped database', () => {
|
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
||||||
const db = createMockDB();
|
(db as any)._contents.sourceArchiveUri = undefined;
|
||||||
const resolved = db.resolveSourceFile('abc');
|
expect(() => db.resolveSourceFile('abc')).to.throw('Scheme is missing');
|
||||||
expect(resolved.toString()).to.eq('file:///sourceArchive-uri/abc');
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve a source file in an unzipped database with trailing slash', () => {
|
it('should fail to resolve when not a file uri', () => {
|
||||||
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
||||||
const resolved = db.resolveSourceFile('abc');
|
(db as any)._contents.sourceArchiveUri = undefined;
|
||||||
expect(resolved.toString()).to.eq('file:///sourceArchive-uri/abc');
|
expect(() => db.resolveSourceFile('http://abc')).to.throw('Invalid uri scheme');
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve a source uri in an unzipped database with trailing slash', () => {
|
|
||||||
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
|
||||||
const resolved = db.resolveSourceFile('file:/abc');
|
|
||||||
expect(resolved.toString()).to.eq('file:///sourceArchive-uri/abc');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('no source archive', () => {
|
describe('no source archive', () => {
|
||||||
it('should resolve a file', () => {
|
it('should resolve undefined', () => {
|
||||||
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
||||||
(db as any)._contents.sourceArchiveUri = undefined;
|
(db as any)._contents.sourceArchiveUri = undefined;
|
||||||
const resolved = db.resolveSourceFile('abc');
|
const resolved = db.resolveSourceFile(undefined);
|
||||||
expect(resolved.toString()).to.eq('file:///abc');
|
expect(resolved.toString()).to.eq('file:///database-uri');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve an empty file', () => {
|
it('should resolve an empty file', () => {
|
||||||
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
|
||||||
(db as any)._contents.sourceArchiveUri = undefined;
|
(db as any)._contents.sourceArchiveUri = undefined;
|
||||||
const resolved = db.resolveSourceFile('file:');
|
const resolved = db.resolveSourceFile('file:');
|
||||||
expect(resolved.toString()).to.eq('file:///database-uri');
|
expect(resolved.toString()).to.eq('file:///');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,7 +152,7 @@ describe('databases', () => {
|
|||||||
pathWithinSourceArchive: 'def'
|
pathWithinSourceArchive: 'def'
|
||||||
}));
|
}));
|
||||||
const resolved = db.resolveSourceFile('file:');
|
const resolved = db.resolveSourceFile('file:');
|
||||||
expect(resolved.toString()).to.eq('codeql-zip-archive://1-18/sourceArchive-uri/def');
|
expect(resolved.toString()).to.eq('codeql-zip-archive://1-18/sourceArchive-uri/def/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -51,20 +51,24 @@ describe('parsing sarif', () => {
|
|||||||
|
|
||||||
it('should normalize source locations', () => {
|
it('should normalize source locations', () => {
|
||||||
expect(getPathRelativeToSourceLocationPrefix('C:\\a\\b', '?x=test'))
|
expect(getPathRelativeToSourceLocationPrefix('C:\\a\\b', '?x=test'))
|
||||||
.to.eq('file:C:/a/b/?x=test');
|
.to.eq('file:/C:/a/b/?x=test');
|
||||||
expect(getPathRelativeToSourceLocationPrefix('C:\\a\\b', '%3Fx%3Dtest'))
|
expect(getPathRelativeToSourceLocationPrefix('C:\\a\\b', '%3Fx%3Dtest'))
|
||||||
.to.eq('file:C:/a/b/%3Fx%3Dtest');
|
.to.eq('file:/C:/a/b/%3Fx%3Dtest');
|
||||||
|
expect(getPathRelativeToSourceLocationPrefix('C:\\a =\\b c?', '?x=test'))
|
||||||
|
.to.eq('file:/C:/a%20%3D/b%20c%3F/?x=test');
|
||||||
|
expect(getPathRelativeToSourceLocationPrefix('/a/b/c', '?x=test'))
|
||||||
|
.to.eq('file:/a/b/c/?x=test');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseSarifLocation', () => {
|
describe('parseSarifLocation', () => {
|
||||||
it('should parse a sarif location with "no location"', () => {
|
it('should parse a sarif location with "no location"', () => {
|
||||||
expect(parseSarifLocation({ }, '')).to.deep.equal({
|
expect(parseSarifLocation({}, '')).to.deep.equal({
|
||||||
hint: 'no physical location'
|
hint: 'no physical location'
|
||||||
});
|
});
|
||||||
expect(parseSarifLocation({ physicalLocation: {} }, '')).to.deep.equal({
|
expect(parseSarifLocation({ physicalLocation: {} }, '')).to.deep.equal({
|
||||||
hint: 'no artifact location'
|
hint: 'no artifact location'
|
||||||
});
|
});
|
||||||
expect(parseSarifLocation({ physicalLocation: { artifactLocation: { } } }, '')).to.deep.equal({
|
expect(parseSarifLocation({ physicalLocation: { artifactLocation: {} } }, '')).to.deep.equal({
|
||||||
hint: 'artifact location has no uri'
|
hint: 'artifact location has no uri'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -78,7 +82,7 @@ describe('parsing sarif', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
expect(parseSarifLocation(location, 'prefix')).to.deep.equal({
|
expect(parseSarifLocation(location, 'prefix')).to.deep.equal({
|
||||||
uri: 'file:prefix/abc?x=test',
|
uri: 'file:/prefix/abc?x=test',
|
||||||
userVisibleFile: 'abc?x=test'
|
userVisibleFile: 'abc?x=test'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -87,13 +91,13 @@ describe('parsing sarif', () => {
|
|||||||
const location: Sarif.Location = {
|
const location: Sarif.Location = {
|
||||||
physicalLocation: {
|
physicalLocation: {
|
||||||
artifactLocation: {
|
artifactLocation: {
|
||||||
uri: 'file:abc%3Fx%3Dtest'
|
uri: 'file:/abc%3Fx%3Dtest'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
expect(parseSarifLocation(location, 'prefix')).to.deep.equal({
|
expect(parseSarifLocation(location, 'prefix')).to.deep.equal({
|
||||||
uri: 'file:abc%3Fx%3Dtest',
|
uri: 'file:/abc%3Fx%3Dtest',
|
||||||
userVisibleFile: 'abc?x=test'
|
userVisibleFile: '/abc?x=test'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user