@@ -18,10 +18,12 @@ const wc = require('./winchars.js')
18
18
const stripAbsolutePath = require ( './strip-absolute-path.js' )
19
19
const pathReservations = require ( './path-reservations.js' )
20
20
const normPath = require ( './normalize-windows-path.js' )
21
+ const stripSlash = require ( './strip-trailing-slashes.js' )
21
22
22
23
const ONENTRY = Symbol ( 'onEntry' )
23
24
const CHECKFS = Symbol ( 'checkFs' )
24
25
const CHECKFS2 = Symbol ( 'checkFs2' )
26
+ const PRUNECACHE = Symbol ( 'pruneCache' )
25
27
const ISREUSABLE = Symbol ( 'isReusable' )
26
28
const MAKEFS = Symbol ( 'makeFs' )
27
29
const FILE = Symbol ( 'file' )
@@ -45,6 +47,8 @@ const UID = Symbol('uid')
45
47
const GID = Symbol ( 'gid' )
46
48
const CHECKED_CWD = Symbol ( 'checkedCwd' )
47
49
const crypto = require ( 'crypto' )
50
+ const platform = process . env . TESTING_TAR_FAKE_PLATFORM || process . platform
51
+ const isWindows = platform === 'win32'
48
52
49
53
// Unlinks on Windows are not atomic.
50
54
//
@@ -63,7 +67,7 @@ const crypto = require('crypto')
63
67
// See: https://github.com/npm/node-tar/issues/183
64
68
/* istanbul ignore next */
65
69
const unlinkFile = ( path , cb ) => {
66
- if ( process . platform !== 'win32' )
70
+ if ( ! isWindows )
67
71
return fs . unlink ( path , cb )
68
72
69
73
const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -76,7 +80,7 @@ const unlinkFile = (path, cb) => {
76
80
77
81
/* istanbul ignore next */
78
82
const unlinkFileSync = path => {
79
- if ( process . platform !== 'win32' )
83
+ if ( ! isWindows )
80
84
return fs . unlinkSync ( path )
81
85
82
86
const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -90,17 +94,33 @@ const uint32 = (a, b, c) =>
90
94
: b === b >>> 0 ? b
91
95
: c
92
96
97
+ // clear the cache if it's a case-insensitive unicode-squashing match.
98
+ // we can't know if the current file system is case-sensitive or supports
99
+ // unicode fully, so we check for similarity on the maximally compatible
100
+ // representation. Err on the side of pruning, since all it's doing is
101
+ // preventing lstats, and it's not the end of the world if we get a false
102
+ // positive.
103
+ // Note that on windows, we always drop the entire cache whenever a
104
+ // symbolic link is encountered, because 8.3 filenames are impossible
105
+ // to reason about, and collisions are hazards rather than just failures.
106
+ const cacheKeyNormalize = path => stripSlash ( normPath ( path ) )
107
+ . normalize ( 'NFKD' )
108
+ . toLowerCase ( )
109
+
93
110
const pruneCache = ( cache , abs ) => {
94
- // clear the cache if it's a case-insensitive match, since we can't
95
- // know if the current file system is case-sensitive or not.
96
- abs = normPath ( abs ) . toLowerCase ( )
111
+ abs = cacheKeyNormalize ( abs )
97
112
for ( const path of cache . keys ( ) ) {
98
- const plower = path . toLowerCase ( )
99
- if ( plower === abs || plower . toLowerCase ( ) . indexOf ( abs + '/' ) === 0 )
113
+ const pnorm = cacheKeyNormalize ( path )
114
+ if ( pnorm === abs || pnorm . indexOf ( abs + '/' ) === 0 )
100
115
cache . delete ( path )
101
116
}
102
117
}
103
118
119
+ const dropCache = cache => {
120
+ for ( const key of cache . keys ( ) )
121
+ cache . delete ( key )
122
+ }
123
+
104
124
class Unpack extends Parser {
105
125
constructor ( opt ) {
106
126
if ( ! opt )
@@ -159,7 +179,7 @@ class Unpack extends Parser {
159
179
this . forceChown = opt . forceChown === true
160
180
161
181
// turn ><?| in filenames into 0xf000-higher encoded forms
162
- this . win32 = ! ! opt . win32 || process . platform === 'win32'
182
+ this . win32 = ! ! opt . win32 || isWindows
163
183
164
184
// do not unpack over files that are newer than what's in the archive
165
185
this . newer = ! ! opt . newer
@@ -470,7 +490,7 @@ class Unpack extends Parser {
470
490
! this . unlink &&
471
491
st . isFile ( ) &&
472
492
st . nlink <= 1 &&
473
- process . platform !== 'win32'
493
+ ! isWindows
474
494
}
475
495
476
496
// check if a thing is there, and if so, try to clobber it
@@ -481,13 +501,31 @@ class Unpack extends Parser {
481
501
paths . push ( entry . linkpath )
482
502
this . reservations . reserve ( paths , done => this [ CHECKFS2 ] ( entry , done ) )
483
503
}
484
- [ CHECKFS2 ] ( entry , done ) {
504
+
505
+ [ PRUNECACHE ] ( entry ) {
485
506
// if we are not creating a directory, and the path is in the dirCache,
486
507
// then that means we are about to delete the directory we created
487
508
// previously, and it is no longer going to be a directory, and neither
488
509
// is any of its children.
489
- if ( entry . type !== 'Directory' )
510
+ // If a symbolic link is encountered on Windows, all bets are off.
511
+ // There is no reasonable way to sanitize the cache in such a way
512
+ // we will be able to avoid having filesystem collisions. If this
513
+ // happens with a non-symlink entry, it'll just fail to unpack,
514
+ // but a symlink to a directory, using an 8.3 shortname, can evade
515
+ // detection and lead to arbitrary writes to anywhere on the system.
516
+ if ( isWindows && entry . type === 'SymbolicLink' )
517
+ dropCache ( this . dirCache )
518
+ else if ( entry . type !== 'Directory' )
490
519
pruneCache ( this . dirCache , entry . absolute )
520
+ }
521
+
522
+ [ CHECKFS2 ] ( entry , fullyDone ) {
523
+ this [ PRUNECACHE ] ( entry )
524
+
525
+ const done = er => {
526
+ this [ PRUNECACHE ] ( entry )
527
+ fullyDone ( er )
528
+ }
491
529
492
530
const checkCwd = ( ) => {
493
531
this [ MKDIR ] ( this . cwd , this . dmode , er => {
@@ -538,7 +576,13 @@ class Unpack extends Parser {
538
576
return afterChmod ( )
539
577
return fs . chmod ( entry . absolute , entry . mode , afterChmod )
540
578
}
541
- // not a dir entry, have to remove it.
579
+ // Not a dir entry, have to remove it.
580
+ // NB: the only way to end up with an entry that is the cwd
581
+ // itself, in such a way that == does not detect, is a
582
+ // tricky windows absolute path with UNC or 8.3 parts (and
583
+ // preservePaths:true, or else it will have been stripped).
584
+ // In that case, the user has opted out of path protections
585
+ // explicitly, so if they blow away the cwd, c'est la vie.
542
586
if ( entry . absolute !== this . cwd ) {
543
587
return fs . rmdir ( entry . absolute , er =>
544
588
this [ MAKEFS ] ( er , entry , done ) )
@@ -608,8 +652,7 @@ class UnpackSync extends Unpack {
608
652
}
609
653
610
654
[ CHECKFS ] ( entry ) {
611
- if ( entry . type !== 'Directory' )
612
- pruneCache ( this . dirCache , entry . absolute )
655
+ this [ PRUNECACHE ] ( entry )
613
656
614
657
if ( ! this [ CHECKED_CWD ] ) {
615
658
const er = this [ MKDIR ] ( this . cwd , this . dmode )
@@ -658,13 +701,19 @@ class UnpackSync extends Unpack {
658
701
this [ MAKEFS ] ( er , entry )
659
702
}
660
703
661
- [ FILE ] ( entry , _ ) {
704
+ [ FILE ] ( entry , done ) {
662
705
const mode = entry . mode & 0o7777 || this . fmode
663
706
664
707
const oner = er => {
665
- try { fs . closeSync ( fd ) } catch ( _ ) { }
666
- if ( er )
667
- this [ ONERROR ] ( er , entry )
708
+ let closeError
709
+ try {
710
+ fs . closeSync ( fd )
711
+ } catch ( e ) {
712
+ closeError = e
713
+ }
714
+ if ( er || closeError )
715
+ this [ ONERROR ] ( er || closeError , entry )
716
+ done ( )
668
717
}
669
718
670
719
let stream
@@ -725,11 +774,14 @@ class UnpackSync extends Unpack {
725
774
} )
726
775
}
727
776
728
- [ DIRECTORY ] ( entry , _ ) {
777
+ [ DIRECTORY ] ( entry , done ) {
729
778
const mode = entry . mode & 0o7777 || this . dmode
730
779
const er = this [ MKDIR ] ( entry . absolute , mode )
731
- if ( er )
732
- return this [ ONERROR ] ( er , entry )
780
+ if ( er ) {
781
+ this [ ONERROR ] ( er , entry )
782
+ done ( )
783
+ return
784
+ }
733
785
if ( entry . mtime && ! this . noMtime ) {
734
786
try {
735
787
fs . utimesSync ( entry . absolute , entry . atime || new Date ( ) , entry . mtime )
@@ -740,6 +792,7 @@ class UnpackSync extends Unpack {
740
792
fs . chownSync ( entry . absolute , this [ UID ] ( entry ) , this [ GID ] ( entry ) )
741
793
} catch ( er ) { }
742
794
}
795
+ done ( )
743
796
entry . resume ( )
744
797
}
745
798
@@ -762,9 +815,10 @@ class UnpackSync extends Unpack {
762
815
}
763
816
}
764
817
765
- [ LINK ] ( entry , linkpath , link , _ ) {
818
+ [ LINK ] ( entry , linkpath , link , done ) {
766
819
try {
767
820
fs [ link + 'Sync' ] ( linkpath , entry . absolute )
821
+ done ( )
768
822
entry . resume ( )
769
823
} catch ( er ) {
770
824
return this [ ONERROR ] ( er , entry )
0 commit comments