|
1 | | -import { RawResultSet } from './adapt'; |
2 | | -import { ResultSetSchema } from 'semmle-bqrs'; |
3 | | -import { Interpretation } from './interface-types'; |
| 1 | +import * as crypto from "crypto"; |
| 2 | +import { |
| 3 | + Uri, |
| 4 | + Location, |
| 5 | + Range, |
| 6 | + WebviewPanel, |
| 7 | + Webview, |
| 8 | + workspace, |
| 9 | + window as Window, |
| 10 | + ViewColumn, |
| 11 | + Selection, |
| 12 | + TextEditorRevealType, |
| 13 | + ThemeColor, |
| 14 | +} from "vscode"; |
| 15 | +import { |
| 16 | + FivePartLocation, |
| 17 | + LocationStyle, |
| 18 | + LocationValue, |
| 19 | + tryGetResolvableLocation, |
| 20 | + WholeFileLocation, |
| 21 | + ResolvableLocationValue, |
| 22 | +} from "semmle-bqrs"; |
| 23 | +import { DatabaseItem, DatabaseManager } from "./databases"; |
| 24 | +import { ViewSourceFileMsg } from "./interface-types"; |
| 25 | +import { Logger } from "./logging"; |
4 | 26 |
|
5 | | -export const SELECT_TABLE_NAME = "#select"; |
6 | | -export const ALERTS_TABLE_NAME = "alerts"; |
| 27 | +/** |
| 28 | + * This module contains functions and types that are sharedd between |
| 29 | + * interface.ts and compare-interface.ts. |
| 30 | + */ |
7 | 31 |
|
8 | | -export type RawTableResultSet = { t: "RawResultSet" } & RawResultSet; |
9 | | -export type PathTableResultSet = { |
10 | | - t: "SarifResultSet"; |
11 | | - readonly schema: ResultSetSchema; |
12 | | - name: string; |
13 | | -} & Interpretation; |
| 32 | +/** Gets a nonce string created with 128 bits of entropy. */ |
| 33 | +export function getNonce(): string { |
| 34 | + return crypto.randomBytes(16).toString("base64"); |
| 35 | +} |
| 36 | + |
| 37 | +/** |
| 38 | + * Whether to force webview to reveal |
| 39 | + */ |
| 40 | +export enum WebviewReveal { |
| 41 | + Forced, |
| 42 | + NotForced, |
| 43 | +} |
| 44 | + |
| 45 | +/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */ |
| 46 | +export function fileUriToWebviewUri( |
| 47 | + panel: WebviewPanel, |
| 48 | + fileUriOnDisk: Uri |
| 49 | +): string { |
| 50 | + return panel.webview.asWebviewUri(fileUriOnDisk).toString(); |
| 51 | +} |
14 | 52 |
|
15 | | -export type ResultSet = RawTableResultSet | PathTableResultSet; |
| 53 | +/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */ |
| 54 | +export function webviewUriToFileUri(webviewUri: string): Uri { |
| 55 | + // Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI. |
| 56 | + const path = Uri.parse(webviewUri).path; |
| 57 | + // For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform. |
| 58 | + return Uri.file(path); |
| 59 | +} |
16 | 60 |
|
17 | | -export function getDefaultResultSet(resultSets: readonly ResultSet[]): string { |
18 | | - return getDefaultResultSetName( |
19 | | - resultSets.map((resultSet) => resultSet.schema.name) |
| 61 | +/** |
| 62 | + * Resolves the specified CodeQL location to a URI into the source archive. |
| 63 | + * @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`. |
| 64 | + * @param databaseItem Database in which to resolve the file location. |
| 65 | + */ |
| 66 | +function resolveFivePartLocation( |
| 67 | + loc: FivePartLocation, |
| 68 | + databaseItem: DatabaseItem |
| 69 | +): Location { |
| 70 | + // `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and |
| 71 | + // are one-based. Adjust accordingly. |
| 72 | + const range = new Range( |
| 73 | + Math.max(0, loc.lineStart - 1), |
| 74 | + Math.max(0, loc.colStart - 1), |
| 75 | + Math.max(0, loc.lineEnd - 1), |
| 76 | + Math.max(0, loc.colEnd) |
20 | 77 | ); |
| 78 | + |
| 79 | + return new Location(databaseItem.resolveSourceFile(loc.file), range); |
| 80 | +} |
| 81 | + |
| 82 | +/** |
| 83 | + * Resolves the specified CodeQL filesystem resource location to a URI into the source archive. |
| 84 | + * @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`. |
| 85 | + * @param databaseItem Database in which to resolve the filesystem resource location. |
| 86 | + */ |
| 87 | +function resolveWholeFileLocation( |
| 88 | + loc: WholeFileLocation, |
| 89 | + databaseItem: DatabaseItem |
| 90 | +): Location { |
| 91 | + // A location corresponding to the start of the file. |
| 92 | + const range = new Range(0, 0, 0, 0); |
| 93 | + return new Location(databaseItem.resolveSourceFile(loc.file), range); |
21 | 94 | } |
22 | 95 |
|
23 | | -export function getDefaultResultSetName( |
24 | | - resultSetNames: readonly string[] |
| 96 | +/** |
| 97 | + * Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location |
| 98 | + * can be resolved, returns `undefined`. |
| 99 | + * @param loc CodeQL location to resolve |
| 100 | + * @param databaseItem Database in which to resolve the file location. |
| 101 | + */ |
| 102 | +export function tryResolveLocation( |
| 103 | + loc: LocationValue | undefined, |
| 104 | + databaseItem: DatabaseItem |
| 105 | +): Location | undefined { |
| 106 | + const resolvableLoc = tryGetResolvableLocation(loc); |
| 107 | + if (resolvableLoc === undefined) { |
| 108 | + return undefined; |
| 109 | + } |
| 110 | + switch (resolvableLoc.t) { |
| 111 | + case LocationStyle.FivePart: |
| 112 | + return resolveFivePartLocation(resolvableLoc, databaseItem); |
| 113 | + case LocationStyle.WholeFile: |
| 114 | + return resolveWholeFileLocation(resolvableLoc, databaseItem); |
| 115 | + default: |
| 116 | + return undefined; |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +/** |
| 121 | + * Returns HTML to populate the given webview. |
| 122 | + * Uses a content security policy that only loads the given script. |
| 123 | + */ |
| 124 | +export function getHtmlForWebview( |
| 125 | + webview: Webview, |
| 126 | + scriptUriOnDisk: Uri, |
| 127 | + stylesheetUriOnDisk: Uri |
25 | 128 | ): string { |
26 | | - // Choose first available result set from the array |
27 | | - return [ |
28 | | - ALERTS_TABLE_NAME, |
29 | | - SELECT_TABLE_NAME, |
30 | | - resultSetNames[0], |
31 | | - ].filter((resultSetName) => resultSetNames.includes(resultSetName))[0]; |
| 129 | + // Convert the on-disk URIs into webview URIs. |
| 130 | + const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk); |
| 131 | + const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk); |
| 132 | + // Use a nonce in the content security policy to uniquely identify the above resources. |
| 133 | + const nonce = getNonce(); |
| 134 | + /* |
| 135 | + * Content security policy: |
| 136 | + * default-src: allow nothing by default. |
| 137 | + * script-src: allow only the given script, using the nonce. |
| 138 | + * style-src: allow only the given stylesheet, using the nonce. |
| 139 | + * connect-src: only allow fetch calls to webview resource URIs |
| 140 | + * (this is used to load BQRS result files). |
| 141 | + */ |
| 142 | + return ` |
| 143 | +<html> |
| 144 | + <head> |
| 145 | + <meta http-equiv="Content-Security-Policy" |
| 146 | + content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};"> |
| 147 | + <link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}"> |
| 148 | + </head> |
| 149 | + <body> |
| 150 | + <div id=root> |
| 151 | + </div> |
| 152 | + <script nonce="${nonce}" src="${scriptWebviewUri}"> |
| 153 | + </script> |
| 154 | + </body> |
| 155 | +</html>`; |
| 156 | +} |
| 157 | + |
| 158 | +export async function showLocation( |
| 159 | + loc: ResolvableLocationValue, |
| 160 | + databaseItem: DatabaseItem |
| 161 | +): Promise<void> { |
| 162 | + const resolvedLocation = tryResolveLocation(loc, databaseItem); |
| 163 | + if (resolvedLocation) { |
| 164 | + const doc = await workspace.openTextDocument(resolvedLocation.uri); |
| 165 | + const editorsWithDoc = Window.visibleTextEditors.filter( |
| 166 | + (e) => e.document === doc |
| 167 | + ); |
| 168 | + const editor = |
| 169 | + editorsWithDoc.length > 0 |
| 170 | + ? editorsWithDoc[0] |
| 171 | + : await Window.showTextDocument(doc, ViewColumn.One); |
| 172 | + const range = resolvedLocation.range; |
| 173 | + // When highlighting the range, vscode's occurrence-match and bracket-match highlighting will |
| 174 | + // trigger based on where we place the cursor/selection, and will compete for the user's attention. |
| 175 | + // For reference: |
| 176 | + // - Occurences are highlighted when the cursor is next to or inside a word or a whole word is selected. |
| 177 | + // - Brackets are highlighted when the cursor is next to a bracket and there is an empty selection. |
| 178 | + // - Multi-line selections explicitly highlight line-break characters, but multi-line decorators do not. |
| 179 | + // |
| 180 | + // For single-line ranges, select the whole range, mainly to disable bracket highlighting. |
| 181 | + // For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks. |
| 182 | + // Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting. |
| 183 | + const selectionEnd = |
| 184 | + range.start.line === range.end.line ? range.end : range.start; |
| 185 | + editor.selection = new Selection(range.start, selectionEnd); |
| 186 | + editor.revealRange(range, TextEditorRevealType.InCenter); |
| 187 | + editor.setDecorations(shownLocationDecoration, [range]); |
| 188 | + editor.setDecorations(shownLocationLineDecoration, [range]); |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +const findMatchBackground = new ThemeColor("editor.findMatchBackground"); |
| 193 | +const findRangeHighlightBackground = new ThemeColor( |
| 194 | + "editor.findRangeHighlightBackground" |
| 195 | +); |
| 196 | + |
| 197 | +export const shownLocationDecoration = Window.createTextEditorDecorationType({ |
| 198 | + backgroundColor: findMatchBackground, |
| 199 | +}); |
| 200 | + |
| 201 | +export const shownLocationLineDecoration = Window.createTextEditorDecorationType( |
| 202 | + { |
| 203 | + backgroundColor: findRangeHighlightBackground, |
| 204 | + isWholeLine: true, |
| 205 | + } |
| 206 | +); |
| 207 | + |
| 208 | +export async function jumpToLocation( |
| 209 | + msg: ViewSourceFileMsg, |
| 210 | + databaseManager: DatabaseManager, |
| 211 | + logger: Logger |
| 212 | +) { |
| 213 | + const databaseItem = databaseManager.findDatabaseItem( |
| 214 | + Uri.parse(msg.databaseUri) |
| 215 | + ); |
| 216 | + if (databaseItem !== undefined) { |
| 217 | + try { |
| 218 | + await showLocation(msg.loc, databaseItem); |
| 219 | + } catch (e) { |
| 220 | + if (e instanceof Error) { |
| 221 | + if (e.message.match(/File not found/)) { |
| 222 | + Window.showErrorMessage( |
| 223 | + `Original file of this result is not in the database's source archive.` |
| 224 | + ); |
| 225 | + } else { |
| 226 | + logger.log(`Unable to handleMsgFromView: ${e.message}`); |
| 227 | + } |
| 228 | + } else { |
| 229 | + logger.log(`Unable to handleMsgFromView: ${e}`); |
| 230 | + } |
| 231 | + } |
| 232 | + } |
32 | 233 | } |
0 commit comments