1'use strict'
2// wrapper around mkdirp for tar's needs.
3
4// TODO: This should probably be a class, not functionally
5// passing around state in a gazillion args.
6
7const mkdirp = require('mkdirp')
8const fs = require('fs')
9const path = require('path')
10const chownr = require('chownr')
11const normPath = require('./normalize-windows-path.js')
12
13class SymlinkError extends Error {
14  constructor (symlink, path) {
15    super('Cannot extract through symbolic link')
16    this.path = path
17    this.symlink = symlink
18  }
19
20  get name () {
21    return 'SylinkError'
22  }
23}
24
25class CwdError extends Error {
26  constructor (path, code) {
27    super(code + ': Cannot cd into \'' + path + '\'')
28    this.path = path
29    this.code = code
30  }
31
32  get name () {
33    return 'CwdError'
34  }
35}
36
37const cGet = (cache, key) => cache.get(normPath(key))
38const cSet = (cache, key, val) => cache.set(normPath(key), val)
39
40const checkCwd = (dir, cb) => {
41  fs.stat(dir, (er, st) => {
42    if (er || !st.isDirectory()) {
43      er = new CwdError(dir, er && er.code || 'ENOTDIR')
44    }
45    cb(er)
46  })
47}
48
49module.exports = (dir, opt, cb) => {
50  dir = normPath(dir)
51
52  // if there's any overlap between mask and mode,
53  // then we'll need an explicit chmod
54  const umask = opt.umask
55  const mode = opt.mode | 0o0700
56  const needChmod = (mode & umask) !== 0
57
58  const uid = opt.uid
59  const gid = opt.gid
60  const doChown = typeof uid === 'number' &&
61    typeof gid === 'number' &&
62    (uid !== opt.processUid || gid !== opt.processGid)
63
64  const preserve = opt.preserve
65  const unlink = opt.unlink
66  const cache = opt.cache
67  const cwd = normPath(opt.cwd)
68
69  const done = (er, created) => {
70    if (er) {
71      cb(er)
72    } else {
73      cSet(cache, dir, true)
74      if (created && doChown) {
75        chownr(created, uid, gid, er => done(er))
76      } else if (needChmod) {
77        fs.chmod(dir, mode, cb)
78      } else {
79        cb()
80      }
81    }
82  }
83
84  if (cache && cGet(cache, dir) === true) {
85    return done()
86  }
87
88  if (dir === cwd) {
89    return checkCwd(dir, done)
90  }
91
92  if (preserve) {
93    return mkdirp(dir, { mode }).then(made => done(null, made), done)
94  }
95
96  const sub = normPath(path.relative(cwd, dir))
97  const parts = sub.split('/')
98  mkdir_(cwd, parts, mode, cache, unlink, cwd, null, done)
99}
100
101const mkdir_ = (base, parts, mode, cache, unlink, cwd, created, cb) => {
102  if (!parts.length) {
103    return cb(null, created)
104  }
105  const p = parts.shift()
106  const part = normPath(path.resolve(base + '/' + p))
107  if (cGet(cache, part)) {
108    return mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
109  }
110  fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb))
111}
112
113const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => {
114  if (er) {
115    fs.lstat(part, (statEr, st) => {
116      if (statEr) {
117        statEr.path = statEr.path && normPath(statEr.path)
118        cb(statEr)
119      } else if (st.isDirectory()) {
120        mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
121      } else if (unlink) {
122        fs.unlink(part, er => {
123          if (er) {
124            return cb(er)
125          }
126          fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb))
127        })
128      } else if (st.isSymbolicLink()) {
129        return cb(new SymlinkError(part, part + '/' + parts.join('/')))
130      } else {
131        cb(er)
132      }
133    })
134  } else {
135    created = created || part
136    mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
137  }
138}
139
140const checkCwdSync = dir => {
141  let ok = false
142  let code = 'ENOTDIR'
143  try {
144    ok = fs.statSync(dir).isDirectory()
145  } catch (er) {
146    code = er.code
147  } finally {
148    if (!ok) {
149      throw new CwdError(dir, code)
150    }
151  }
152}
153
154module.exports.sync = (dir, opt) => {
155  dir = normPath(dir)
156  // if there's any overlap between mask and mode,
157  // then we'll need an explicit chmod
158  const umask = opt.umask
159  const mode = opt.mode | 0o0700
160  const needChmod = (mode & umask) !== 0
161
162  const uid = opt.uid
163  const gid = opt.gid
164  const doChown = typeof uid === 'number' &&
165    typeof gid === 'number' &&
166    (uid !== opt.processUid || gid !== opt.processGid)
167
168  const preserve = opt.preserve
169  const unlink = opt.unlink
170  const cache = opt.cache
171  const cwd = normPath(opt.cwd)
172
173  const done = (created) => {
174    cSet(cache, dir, true)
175    if (created && doChown) {
176      chownr.sync(created, uid, gid)
177    }
178    if (needChmod) {
179      fs.chmodSync(dir, mode)
180    }
181  }
182
183  if (cache && cGet(cache, dir) === true) {
184    return done()
185  }
186
187  if (dir === cwd) {
188    checkCwdSync(cwd)
189    return done()
190  }
191
192  if (preserve) {
193    return done(mkdirp.sync(dir, mode))
194  }
195
196  const sub = normPath(path.relative(cwd, dir))
197  const parts = sub.split('/')
198  let created = null
199  for (let p = parts.shift(), part = cwd;
200    p && (part += '/' + p);
201    p = parts.shift()) {
202    part = normPath(path.resolve(part))
203    if (cGet(cache, part)) {
204      continue
205    }
206
207    try {
208      fs.mkdirSync(part, mode)
209      created = created || part
210      cSet(cache, part, true)
211    } catch (er) {
212      const st = fs.lstatSync(part)
213      if (st.isDirectory()) {
214        cSet(cache, part, true)
215        continue
216      } else if (unlink) {
217        fs.unlinkSync(part)
218        fs.mkdirSync(part, mode)
219        created = created || part
220        cSet(cache, part, true)
221        continue
222      } else if (st.isSymbolicLink()) {
223        return new SymlinkError(part, part + '/' + parts.join('/'))
224      }
225    }
226  }
227
228  return done(created)
229}
230