@@ -13,6 +13,7 @@ import { isNonNullable } from './utilities/tsUtils'
1313import type * as fs from 'fs'
1414import type * as os from 'os'
1515import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming'
16+ import { isAutomation } from './vscode/env'
1617
1718export const errorCode = {
1819 invalidConnection : 'InvalidConnection' ,
@@ -82,7 +83,8 @@ export interface ErrorInformation {
8283 /**
8384 * A link to documentation relevant to this error.
8485 *
85- * TODO: implement this
86+ * TODO: use this throughout the codebase.
87+ * TODO: prefer `Error.error_uri` if present (from OIDC/OAuth service)?
8688 */
8789 readonly documentationUri ?: vscode . Uri
8890}
@@ -234,9 +236,13 @@ export function getErrorMsg(err: Error | undefined): string | undefined {
234236 return undefined
235237 }
236238
237- // error_description is a non-standard SDK field added by (at least) OIDC service.
238- // If present, it has better information, so prefer it to `message`.
239- // https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10
239+ // Non-standard SDK fields added by the OIDC service, to conform to the OAuth spec
240+ // (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) :
241+ // - error: code per the OAuth spec
242+ // - error_description: improved error message provided by OIDC service. Prefer this to
243+ // `message` if present.
244+ // https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10
245+ // - error_uri: not provided by OIDC currently?
240246 //
241247 // Example:
242248 //
@@ -355,16 +361,20 @@ const _preferredErrors: RegExp[] = [
355361 / ^ R e s o u r c e N o t F o u n d E x c e p t i o n $ / ,
356362 / ^ S e r v i c e Q u o t a E x c e e d e d E x c e p t i o n $ / ,
357363 / ^ A c c e s s D e n i e d E x c e p t i o n $ / ,
364+ / ^ I n v a l i d P e r m i s s i o n s $ / ,
365+ / ^ E P I P E $ / ,
366+ / ^ E P E R M $ / ,
358367]
359368
360369/**
361370 * Searches the `cause` chain (if any) for the most useful/relevant {@link AWSError} to surface to
362371 * the user, preferring "deeper" errors (lower-level, closer to the root cause) when all else is equal.
363372 *
364373 * These conditions determine precedence (in order):
365- * - required: AWSError type
366- * - `error_description` field
367- * - `code` matches one of `preferredErrors`
374+ * - is AWSError
375+ * - has `error_description` field
376+ * - has `code` matching `preferredErrors`
377+ * - is filesystem error
368378 * - cause chain depth (the deepest error wins)
369379 *
370380 * @param error Error whose `cause` chain will be searched.
@@ -375,14 +385,26 @@ const _preferredErrors: RegExp[] = [
375385export function findBestErrorInChain ( error : Error , preferredErrors = _preferredErrors ) : Error | undefined {
376386 // TODO: Base Error has 'cause' in ES2022. So does our own `ToolkitError`.
377387 // eslint-disable-next-line @typescript-eslint/naming-convention
378- let bestErr : Error & { cause ?: Error ; error_description ?: string } = error
388+ let bestErr : Error & { code ?: string ; cause ?: Error ; error_description ?: string } = error
379389 let err : typeof bestErr | undefined
380390
381- for ( let i = 0 ; ( err || i === 0 ) && i < 100 ; i ++ ) {
391+ for ( let i = 0 ; i < 100 ; i ++ ) {
382392 err = i === 0 ? bestErr . cause : err ?. cause
393+ if ( ! err ) {
394+ break
395+ }
383396
384- if ( isAwsError ( err ) ) {
385- if ( ! isAwsError ( bestErr ) ) {
397+ // const bestErrCode = bestErr.code?.trim() ?? ''
398+ // const preferBest = ...
399+ const errCode = err . code ?. trim ( ) ?? ''
400+ const prefer =
401+ ( errCode !== '' && preferredErrors . some ( re => re . test ( errCode ) ) ) ||
402+ // In priority order:
403+ isFilesystemError ( err ) ||
404+ isNoPermissionsError ( err )
405+
406+ if ( isAwsError ( err ) || ( prefer && ! isAwsError ( bestErr ) ) ) {
407+ if ( isAwsError ( err ) && ! isAwsError ( bestErr ) ) {
386408 bestErr = err // Prefer AWSError.
387409 continue
388410 }
@@ -393,11 +415,7 @@ export function findBestErrorInChain(error: Error, preferredErrors = _preferredE
393415 continue
394416 }
395417
396- // const bestErrCode = bestErr.code?.trim() ?? ''
397- // const bestErrMatches = bestErrCode !== '' && preferredErrors.some(re => re.test(bestErrCode))
398- const errCode = err . code ?. trim ( ) ?? ''
399- const errMatches = errCode !== '' && preferredErrors . some ( re => re . test ( errCode ) )
400- if ( ! bestErr . error_description && errMatches ) {
418+ if ( ! bestErr . error_description && prefer ) {
401419 bestErr = err
402420 }
403421 }
@@ -469,6 +487,41 @@ export function getRequestId(err: unknown): string | undefined {
469487 }
470488}
471489
490+ export function isFilesystemError ( err : unknown ) : boolean {
491+ if (
492+ err instanceof vscode . FileSystemError ||
493+ ( hasCode ( err ) &&
494+ ( err . code === 'EEXIST' ||
495+ err . code === 'EISDIR' ||
496+ err . code === 'ENOTDIR' ||
497+ err . code === 'EMFILE' ||
498+ err . code === 'ENOENT' ||
499+ err . code === 'ENOTEMPTY' ) )
500+ ) {
501+ return true
502+ }
503+
504+ return false
505+ }
506+
507+ // export function isIsDirError(err: unknown): boolean {
508+ // if (err instanceof vscode.FileSystemError) {
509+ // return err.code === vscode.FileSystemError.FileIsADirectory().code
510+ // } else if (hasCode(err)) {
511+ // return err.code === 'EISDIR'
512+ // }
513+ // return false
514+ // }
515+ //
516+ // export function isFileExistsError(err: unknown): boolean {
517+ // if (err instanceof vscode.FileSystemError) {
518+ // return err.code === vscode.FileSystemError.FileExists().code
519+ // } else if (hasCode(err)) {
520+ // return err.code === 'EEXIST'
521+ // }
522+ // return false
523+ // }
524+
472525export function isFileNotFoundError ( err : unknown ) : boolean {
473526 if ( err instanceof vscode . FileSystemError ) {
474527 return err . code === vscode . FileSystemError . FileNotFound ( ) . code
@@ -487,16 +540,33 @@ export function isNoPermissionsError(err: unknown): boolean {
487540 ( err . code === 'Unknown' && err . message . includes ( 'EACCES: permission denied' ) )
488541 )
489542 } else if ( hasCode ( err ) ) {
543+ // " Some operating systems report EPERM in situations unrelated to permissions."
544+ // || err.code === 'EPERM'
490545 return err . code === 'EACCES'
491546 }
492547
493548 return false
494549}
495550
496- const modeToString = ( mode : number ) =>
497- Array . from ( 'rwxrwxrwx' )
551+ function modeToString ( mode : number ) {
552+ return Array . from ( 'rwxrwxrwx' )
498553 . map ( ( c , i , a ) => ( ( mode >> ( a . length - ( i + 1 ) ) ) & 1 ? c : '-' ) )
499554 . join ( '' )
555+ }
556+
557+ function vscodeModeToString ( mode : vscode . FileStat [ 'permissions' ] ) {
558+ // XXX: vscode.FileStat.permissions only indicates "readonly" or nothing (aka "writable").
559+ if ( mode === undefined ) {
560+ return 'rwx------'
561+ } else if ( mode === vscode . FilePermission . Readonly ) {
562+ return 'r-x------'
563+ }
564+
565+ // XXX: future-proof in case vscode.FileStat.permissions gains more granularity.
566+ if ( isAutomation ( ) ) {
567+ throw new Error ( 'vscode.FileStat.permissions gained new fields, update this logic' )
568+ }
569+ }
500570
501571function getEffectivePerms ( uid : number , gid : number , stats : fs . Stats ) {
502572 const mode = stats . mode
@@ -529,38 +599,71 @@ export type PermissionsTriplet = `${'r' | '-' | '*'}${'w' | '-' | '*'}${'x' | '-
529599export class PermissionsError extends ToolkitError {
530600 public readonly actual : string // This is a resolved triplet, _not_ the full bits
531601
602+ static fromNodeFileStats ( stats : fs . Stats , userInfo : os . UserInfo < string > , expected : string , source : unknown ) {
603+ const mode = `${ stats . isDirectory ( ) ? 'd' : '-' } ${ modeToString ( stats . mode ) } `
604+ const owner = stats . uid === userInfo . uid ? ( stats . uid === - 1 ? '' : userInfo . username ) : String ( stats . uid )
605+ const group = String ( stats . gid )
606+ const { effective, isAmbiguous } = getEffectivePerms ( userInfo . uid , userInfo . gid , stats )
607+ const actual = modeToString ( effective ) . slice ( - 3 )
608+ const isOwner = stats . uid === - 1 ? 'unknown' : userInfo . uid === stats . uid
609+
610+ return { mode, owner, group, actual, isAmbiguous, isOwner }
611+ }
612+
613+ static fromVscodeFileStats (
614+ stats : vscode . FileStat ,
615+ userInfo : os . UserInfo < string > ,
616+ expected : string ,
617+ source : unknown
618+ ) {
619+ const isDir = ! ! ( stats . type & vscode . FileType . Directory )
620+ const mode = `${ isDir ? 'd' : '-' } ${ vscodeModeToString ( stats . permissions ) } `
621+ const owner = '' // vscode.FileStat does not currently provide file owner.
622+ const group = '' // vscode.FileStat does not currently provide file group.
623+ const isAmbiguous = true // vscode.workspace.fs.stat() is currently always ambiguous.
624+ const actual = mode
625+ const isOwner = 'unknown' // vscode.FileStat does not currently provide file owner.
626+
627+ return { mode, owner, group, actual, isAmbiguous, isOwner }
628+ }
629+
630+ /**
631+ * Creates a PermissionsError from a file stat() result.
632+ *
633+ * Note: pass `fs.Stats` when possible (in a nodejs context), because it gives much better info.
634+ */
532635 public constructor (
533636 public readonly uri : vscode . Uri ,
534- public readonly stats : fs . Stats ,
637+ public readonly stats : fs . Stats | vscode . FileStat ,
535638 public readonly userInfo : os . UserInfo < string > ,
536639 public readonly expected : PermissionsTriplet ,
537640 source ?: unknown
538641 ) {
539- const mode = ` ${ stats . isDirectory ( ) ? 'd' : '-' } ${ modeToString ( stats . mode ) } `
540- const owner = stats . uid === userInfo . uid ? userInfo . username : stats . uid
541- const { effective , isAmbiguous } = getEffectivePerms ( userInfo . uid , userInfo . gid , stats )
542- const actual = modeToString ( effective ) . slice ( - 3 )
642+ const o = ( stats as any ) . type
643+ ? PermissionsError . fromVscodeFileStats ( stats as vscode . FileStat , userInfo , expected , source )
644+ : PermissionsError . fromNodeFileStats ( stats as fs . Stats , userInfo , expected , source )
645+
543646 const resolvedExpected = Array . from ( expected )
544- . map ( ( c , i ) => ( c === '*' ? actual [ i ] : c ) )
647+ . map ( ( c , i ) => ( c === '*' ? o . actual [ i ] : c ) )
545648 . join ( '' )
546- const actualText = ! isAmbiguous ? actual : `${ mode . slice ( - 6 , - 3 ) } & ${ mode . slice ( - 3 ) } (ambiguous)`
649+ const actualText = ! o . isAmbiguous ? o . actual : `${ o . mode . slice ( - 6 , - 3 ) } & ${ o . mode . slice ( - 3 ) } (ambiguous)`
547650
548651 // Guard against surfacing confusing error messages. If the actual perms equal the resolved
549652 // perms then odds are it wasn't really a permissions error. Some operating systems report EPERM
550653 // in situations that aren't related to permissions at all.
551- if ( actual === resolvedExpected && ! isAmbiguous && source !== undefined ) {
654+ if ( o . actual === resolvedExpected && ! o . isAmbiguous && source !== undefined ) {
552655 throw source
553656 }
554657
555658 super ( `${ uri . fsPath } has incorrect permissions. Expected ${ resolvedExpected } , found ${ actualText } .` , {
556659 code : 'InvalidPermissions' ,
557660 details : {
558- isOwner : stats . uid === - 1 ? 'unknown' : userInfo . uid === stats . uid ,
559- mode : `${ mode } ${ stats . uid === - 1 ? '' : ` ${ owner } ` } ${ stats . gid === - 1 ? '' : ` ${ stats . gid } ` } ` ,
661+ isOwner : o . isOwner ,
662+ mode : `${ o . mode } ${ o . owner === '' ? '' : ` ${ o . owner } ` } ${ o . group === '' ? '' : ` ${ o . group } ` } ` ,
560663 } ,
561664 } )
562665
563- this . actual = actual
666+ this . actual = o . actual
564667 }
565668}
566669
0 commit comments