|
3 | 3 | * SPDX-License-Identifier: Apache-2.0
|
4 | 4 | */
|
5 | 5 |
|
6 |
| -import { Uri } from 'vscode' |
| 6 | +import * as vscode from 'vscode' |
7 | 7 | import { AWSError } from 'aws-sdk'
|
8 | 8 | import { ServiceException } from '@aws-sdk/smithy-client'
|
9 | 9 | import { isThrottlingError, isTransientError } from '@aws-sdk/service-error-classification'
|
10 | 10 | import { Result } from './telemetry/telemetry'
|
11 | 11 | import { CancellationError } from './utilities/timeoutUtils'
|
12 |
| -import { isNonNullable } from './utilities/tsUtils' |
| 12 | +import { hasKey, isNonNullable } from './utilities/tsUtils' |
| 13 | +import type * as fs from 'fs' |
| 14 | +import type * as os from 'os' |
13 | 15 |
|
14 | 16 | export const errorCode = {
|
15 | 17 | invalidConnection: 'InvalidConnection',
|
@@ -81,7 +83,7 @@ export interface ErrorInformation {
|
81 | 83 | *
|
82 | 84 | * TODO: implement this
|
83 | 85 | */
|
84 |
| - readonly documentationUri?: Uri |
| 86 | + readonly documentationUri?: vscode.Uri |
85 | 87 | }
|
86 | 88 |
|
87 | 89 | /**
|
@@ -397,3 +399,90 @@ export function getRequestId(error: unknown): string | undefined {
|
397 | 399 | return error.$metadata.requestId
|
398 | 400 | }
|
399 | 401 | }
|
| 402 | + |
| 403 | +export function isFileNotFoundError(err: unknown): boolean { |
| 404 | + if (err instanceof vscode.FileSystemError) { |
| 405 | + return err.code === vscode.FileSystemError.FileNotFound().code |
| 406 | + } else if (isNonNullable(err) && typeof err === 'object' && hasKey(err, 'code')) { |
| 407 | + return err.code === 'ENOENT' |
| 408 | + } |
| 409 | + |
| 410 | + return false |
| 411 | +} |
| 412 | + |
| 413 | +export function isNoPermissionsError(err: unknown): boolean { |
| 414 | + if (err instanceof vscode.FileSystemError) { |
| 415 | + return ( |
| 416 | + err.code === vscode.FileSystemError.NoPermissions().code || |
| 417 | + // The code _should_ be `NoPermissions`, maybe this is a bug? |
| 418 | + (err.code === 'Unknown' && err.message.includes('EACCES: permission denied')) |
| 419 | + ) |
| 420 | + } else if (isNonNullable(err) && typeof err === 'object' && hasKey(err, 'code')) { |
| 421 | + return err.code === 'EACCES' |
| 422 | + } |
| 423 | + |
| 424 | + return false |
| 425 | +} |
| 426 | + |
| 427 | +const modeToString = (mode: number) => |
| 428 | + Array.from('rwxrwxrwx') |
| 429 | + .map((c, i, a) => ((mode >> (a.length - (i + 1))) & 1 ? c : '-')) |
| 430 | + .join('') |
| 431 | + |
| 432 | +function getEffectivePerms(uid: number, gid: number, stats: fs.Stats) { |
| 433 | + const mode = stats.mode |
| 434 | + const isOwner = uid === stats.uid |
| 435 | + const isGroup = gid === stats.gid && !isOwner |
| 436 | + |
| 437 | + // Many unix systems support multiple groups but we only know the primary |
| 438 | + // The user can still have group permissions despite not having the same `gid` |
| 439 | + // These situations are ambiguous, so the effective permissions are the |
| 440 | + // intersection of the two bitfields |
| 441 | + if (!isOwner && !isGroup) { |
| 442 | + return { |
| 443 | + isAmbiguous: true, |
| 444 | + effective: mode & 0o007 & ((mode & 0o070) >> 3), |
| 445 | + } |
| 446 | + } |
| 447 | + |
| 448 | + const ownerMask = isOwner ? 0o700 : 0 |
| 449 | + const groupMask = isGroup ? 0o070 : 0 |
| 450 | + |
| 451 | + return { |
| 452 | + isAmbiguous: false, |
| 453 | + effective: ((mode & groupMask) >> 3) | ((mode & ownerMask) >> 6), |
| 454 | + } |
| 455 | +} |
| 456 | + |
| 457 | +// The wildcard (`*`) symbol is non-standard. It's used to represent "don't cares" and takes |
| 458 | +// on the actual flag once known. |
| 459 | +export type PermissionsTriplet = `${'r' | '-' | '*'}${'w' | '-' | '*'}${'x' | '-' | '*'}` |
| 460 | +export class PermissionsError extends ToolkitError { |
| 461 | + public readonly actual: string // This is a resolved triplet, _not_ the full bits |
| 462 | + |
| 463 | + public constructor( |
| 464 | + public readonly uri: vscode.Uri, |
| 465 | + public readonly stats: fs.Stats, |
| 466 | + public readonly userInfo: os.UserInfo<string>, |
| 467 | + public readonly expected: PermissionsTriplet |
| 468 | + ) { |
| 469 | + const mode = `${stats.isDirectory() ? 'd' : '-'}${modeToString(stats.mode)}` |
| 470 | + const owner = stats.uid === userInfo.uid ? userInfo.username : stats.uid |
| 471 | + const { effective, isAmbiguous } = getEffectivePerms(userInfo.uid, userInfo.gid, stats) |
| 472 | + const actual = modeToString(effective).slice(-3) |
| 473 | + const resolvedExpected = Array.from(expected) |
| 474 | + .map((c, i) => (c === '*' ? actual[i] : c)) |
| 475 | + .join('') |
| 476 | + const actualText = !isAmbiguous ? actual : `${mode.slice(-6, -3)} & ${mode.slice(-3)} (ambiguous)` |
| 477 | + |
| 478 | + super(`${uri.fsPath} has incorrect permissions. Expected ${resolvedExpected}, found ${actualText}.`, { |
| 479 | + code: 'InvalidPermissions', |
| 480 | + details: { |
| 481 | + isOwner: stats.uid === -1 ? 'unknown' : userInfo.uid == stats.uid, |
| 482 | + mode: `${mode}${stats.uid === -1 ? '' : ` ${owner}`}${stats.gid === -1 ? '' : ` ${stats.gid}`}`, |
| 483 | + }, |
| 484 | + }) |
| 485 | + |
| 486 | + this.actual = actual |
| 487 | + } |
| 488 | +} |
0 commit comments