|
| 1 | +// Exported for testing |
| 2 | +import * as cli from "../../codeql-cli/cli"; |
| 3 | +import vscode from "vscode"; |
| 4 | +import { FullDatabaseOptions } from "./database-options"; |
| 5 | +import { basename, dirname, join, relative } from "path"; |
| 6 | +import { asError } from "../../pure/helpers-pure"; |
| 7 | +import { |
| 8 | + decodeSourceArchiveUri, |
| 9 | + encodeArchiveBasePath, |
| 10 | + encodeSourceArchiveUri, |
| 11 | + zipArchiveScheme, |
| 12 | +} from "../../common/vscode/archive-filesystem-provider"; |
| 13 | +import { DatabaseItem, PersistedDatabaseItem } from "./database-item"; |
| 14 | +import { isLikelyDatabaseRoot } from "../../helpers"; |
| 15 | +import { stat } from "fs-extra"; |
| 16 | +import { pathsEqual } from "../../pure/files"; |
| 17 | +import { DatabaseContents } from "./database-contents"; |
| 18 | +import { DatabaseResolver } from "./database-resolver"; |
| 19 | +import { DatabaseChangedEvent, DatabaseEventKind } from "./database-events"; |
| 20 | + |
| 21 | +export class DatabaseItemImpl implements DatabaseItem { |
| 22 | + private _error: Error | undefined = undefined; |
| 23 | + private _contents: DatabaseContents | undefined; |
| 24 | + /** A cache of database info */ |
| 25 | + private _dbinfo: cli.DbInfo | undefined; |
| 26 | + |
| 27 | + public constructor( |
| 28 | + public readonly databaseUri: vscode.Uri, |
| 29 | + contents: DatabaseContents | undefined, |
| 30 | + private options: FullDatabaseOptions, |
| 31 | + private readonly onChanged: (event: DatabaseChangedEvent) => void, |
| 32 | + ) { |
| 33 | + this._contents = contents; |
| 34 | + } |
| 35 | + |
| 36 | + public get name(): string { |
| 37 | + if (this.options.displayName) { |
| 38 | + return this.options.displayName; |
| 39 | + } else if (this._contents) { |
| 40 | + return this._contents.name; |
| 41 | + } else { |
| 42 | + return basename(this.databaseUri.fsPath); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + public set name(newName: string) { |
| 47 | + this.options.displayName = newName; |
| 48 | + } |
| 49 | + |
| 50 | + public get sourceArchive(): vscode.Uri | undefined { |
| 51 | + if (this.options.ignoreSourceArchive || this._contents === undefined) { |
| 52 | + return undefined; |
| 53 | + } else { |
| 54 | + return this._contents.sourceArchiveUri; |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + public get contents(): DatabaseContents | undefined { |
| 59 | + return this._contents; |
| 60 | + } |
| 61 | + |
| 62 | + public get dateAdded(): number | undefined { |
| 63 | + return this.options.dateAdded; |
| 64 | + } |
| 65 | + |
| 66 | + public get error(): Error | undefined { |
| 67 | + return this._error; |
| 68 | + } |
| 69 | + |
| 70 | + public async refresh(): Promise<void> { |
| 71 | + try { |
| 72 | + try { |
| 73 | + this._contents = await DatabaseResolver.resolveDatabaseContents( |
| 74 | + this.databaseUri, |
| 75 | + ); |
| 76 | + this._error = undefined; |
| 77 | + } catch (e) { |
| 78 | + this._contents = undefined; |
| 79 | + this._error = asError(e); |
| 80 | + throw e; |
| 81 | + } |
| 82 | + } finally { |
| 83 | + this.onChanged({ |
| 84 | + kind: DatabaseEventKind.Refresh, |
| 85 | + item: this, |
| 86 | + }); |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + public resolveSourceFile(uriStr: string | undefined): vscode.Uri { |
| 91 | + const sourceArchive = this.sourceArchive; |
| 92 | + const uri = uriStr ? vscode.Uri.parse(uriStr, true) : undefined; |
| 93 | + if (uri && uri.scheme !== "file") { |
| 94 | + throw new Error( |
| 95 | + `Invalid uri scheme in ${uriStr}. Only 'file' is allowed.`, |
| 96 | + ); |
| 97 | + } |
| 98 | + if (!sourceArchive) { |
| 99 | + if (uri) { |
| 100 | + return uri; |
| 101 | + } else { |
| 102 | + return this.databaseUri; |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + if (uri) { |
| 107 | + const relativeFilePath = decodeURI(uri.path) |
| 108 | + .replace(":", "_") |
| 109 | + .replace(/^\/*/, ""); |
| 110 | + if (sourceArchive.scheme === zipArchiveScheme) { |
| 111 | + const zipRef = decodeSourceArchiveUri(sourceArchive); |
| 112 | + const pathWithinSourceArchive = |
| 113 | + zipRef.pathWithinSourceArchive === "/" |
| 114 | + ? relativeFilePath |
| 115 | + : `${zipRef.pathWithinSourceArchive}/${relativeFilePath}`; |
| 116 | + return encodeSourceArchiveUri({ |
| 117 | + pathWithinSourceArchive, |
| 118 | + sourceArchiveZipPath: zipRef.sourceArchiveZipPath, |
| 119 | + }); |
| 120 | + } else { |
| 121 | + let newPath = sourceArchive.path; |
| 122 | + if (!newPath.endsWith("/")) { |
| 123 | + // Ensure a trailing slash. |
| 124 | + newPath += "/"; |
| 125 | + } |
| 126 | + newPath += relativeFilePath; |
| 127 | + |
| 128 | + return sourceArchive.with({ path: newPath }); |
| 129 | + } |
| 130 | + } else { |
| 131 | + return sourceArchive; |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + /** |
| 136 | + * Gets the state of this database, to be persisted in the workspace state. |
| 137 | + */ |
| 138 | + public getPersistedState(): PersistedDatabaseItem { |
| 139 | + return { |
| 140 | + uri: this.databaseUri.toString(true), |
| 141 | + options: this.options, |
| 142 | + }; |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Holds if the database item refers to an exported snapshot |
| 147 | + */ |
| 148 | + public async hasMetadataFile(): Promise<boolean> { |
| 149 | + return await isLikelyDatabaseRoot(this.databaseUri.fsPath); |
| 150 | + } |
| 151 | + |
| 152 | + /** |
| 153 | + * Returns information about a database. |
| 154 | + */ |
| 155 | + private async getDbInfo(server: cli.CodeQLCliServer): Promise<cli.DbInfo> { |
| 156 | + if (this._dbinfo === undefined) { |
| 157 | + this._dbinfo = await server.resolveDatabase(this.databaseUri.fsPath); |
| 158 | + } |
| 159 | + return this._dbinfo; |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * Returns `sourceLocationPrefix` of database. Requires that the database |
| 164 | + * has a `.dbinfo` file, which is the source of the prefix. |
| 165 | + */ |
| 166 | + public async getSourceLocationPrefix( |
| 167 | + server: cli.CodeQLCliServer, |
| 168 | + ): Promise<string> { |
| 169 | + const dbInfo = await this.getDbInfo(server); |
| 170 | + return dbInfo.sourceLocationPrefix; |
| 171 | + } |
| 172 | + |
| 173 | + /** |
| 174 | + * Returns path to dataset folder of database. |
| 175 | + */ |
| 176 | + public async getDatasetFolder(server: cli.CodeQLCliServer): Promise<string> { |
| 177 | + const dbInfo = await this.getDbInfo(server); |
| 178 | + return dbInfo.datasetFolder; |
| 179 | + } |
| 180 | + |
| 181 | + public get language() { |
| 182 | + return this.options.language || ""; |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * Returns the root uri of the virtual filesystem for this database's source archive. |
| 187 | + */ |
| 188 | + public getSourceArchiveExplorerUri(): vscode.Uri { |
| 189 | + const sourceArchive = this.sourceArchive; |
| 190 | + if (sourceArchive === undefined || !sourceArchive.fsPath.endsWith(".zip")) { |
| 191 | + throw new Error(this.verifyZippedSources()); |
| 192 | + } |
| 193 | + return encodeArchiveBasePath(sourceArchive.fsPath); |
| 194 | + } |
| 195 | + |
| 196 | + public verifyZippedSources(): string | undefined { |
| 197 | + const sourceArchive = this.sourceArchive; |
| 198 | + if (sourceArchive === undefined) { |
| 199 | + return `${this.name} has no source archive.`; |
| 200 | + } |
| 201 | + |
| 202 | + if (!sourceArchive.fsPath.endsWith(".zip")) { |
| 203 | + return `${this.name} has a source folder that is unzipped.`; |
| 204 | + } |
| 205 | + return; |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * Holds if `uri` belongs to this database's source archive. |
| 210 | + */ |
| 211 | + public belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean { |
| 212 | + if (this.sourceArchive === undefined) return false; |
| 213 | + return ( |
| 214 | + uri.scheme === zipArchiveScheme && |
| 215 | + decodeSourceArchiveUri(uri).sourceArchiveZipPath === |
| 216 | + this.sourceArchive.fsPath |
| 217 | + ); |
| 218 | + } |
| 219 | + |
| 220 | + public async isAffectedByTest(testPath: string): Promise<boolean> { |
| 221 | + const databasePath = this.databaseUri.fsPath; |
| 222 | + if (!databasePath.endsWith(".testproj")) { |
| 223 | + return false; |
| 224 | + } |
| 225 | + try { |
| 226 | + const stats = await stat(testPath); |
| 227 | + if (stats.isDirectory()) { |
| 228 | + return !relative(testPath, databasePath).startsWith(".."); |
| 229 | + } else { |
| 230 | + // database for /one/two/three/test.ql is at /one/two/three/three.testproj |
| 231 | + const testdir = dirname(testPath); |
| 232 | + const testdirbase = basename(testdir); |
| 233 | + return pathsEqual( |
| 234 | + databasePath, |
| 235 | + join(testdir, `${testdirbase}.testproj`), |
| 236 | + process.platform, |
| 237 | + ); |
| 238 | + } |
| 239 | + } catch { |
| 240 | + // No information available for test path - assume database is unaffected. |
| 241 | + return false; |
| 242 | + } |
| 243 | + } |
| 244 | +} |
0 commit comments