xref: /third_party/node/lib/internal/fs/cp/cp-sync.js (revision 1cb0ef41)
1'use strict';
2
3// This file is a modified version of the fs-extra's copySync method.
4
5const { areIdentical, isSrcSubdir } = require('internal/fs/cp/cp');
6const { codes } = require('internal/errors');
7const {
8  os: {
9    errno: {
10      EEXIST,
11      EISDIR,
12      EINVAL,
13      ENOTDIR,
14    },
15  },
16} = internalBinding('constants');
17const {
18  ERR_FS_CP_DIR_TO_NON_DIR,
19  ERR_FS_CP_EEXIST,
20  ERR_FS_CP_EINVAL,
21  ERR_FS_CP_FIFO_PIPE,
22  ERR_FS_CP_NON_DIR_TO_DIR,
23  ERR_FS_CP_SOCKET,
24  ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY,
25  ERR_FS_CP_UNKNOWN,
26  ERR_FS_EISDIR,
27  ERR_INVALID_RETURN_VALUE,
28} = codes;
29const {
30  chmodSync,
31  copyFileSync,
32  existsSync,
33  lstatSync,
34  mkdirSync,
35  opendirSync,
36  readlinkSync,
37  statSync,
38  symlinkSync,
39  unlinkSync,
40  utimesSync,
41} = require('fs');
42const {
43  dirname,
44  isAbsolute,
45  join,
46  parse,
47  resolve,
48} = require('path');
49const { isPromise } = require('util/types');
50
51function cpSyncFn(src, dest, opts) {
52  // Warn about using preserveTimestamps on 32-bit node
53  if (opts.preserveTimestamps && process.arch === 'ia32') {
54    const warning = 'Using the preserveTimestamps option in 32-bit ' +
55      'node is not recommended';
56    process.emitWarning(warning, 'TimestampPrecisionWarning');
57  }
58  const { srcStat, destStat, skipped } = checkPathsSync(src, dest, opts);
59  if (skipped) return;
60  checkParentPathsSync(src, srcStat, dest);
61  return checkParentDir(destStat, src, dest, opts);
62}
63
64function checkPathsSync(src, dest, opts) {
65  if (opts.filter) {
66    const shouldCopy = opts.filter(src, dest);
67    if (isPromise(shouldCopy)) {
68      throw new ERR_INVALID_RETURN_VALUE('boolean', 'filter', shouldCopy);
69    }
70    if (!shouldCopy) return { __proto__: null, skipped: true };
71  }
72  const { srcStat, destStat } = getStatsSync(src, dest, opts);
73
74  if (destStat) {
75    if (areIdentical(srcStat, destStat)) {
76      throw new ERR_FS_CP_EINVAL({
77        message: 'src and dest cannot be the same',
78        path: dest,
79        syscall: 'cp',
80        errno: EINVAL,
81        code: 'EINVAL',
82      });
83    }
84    if (srcStat.isDirectory() && !destStat.isDirectory()) {
85      throw new ERR_FS_CP_DIR_TO_NON_DIR({
86        message: `cannot overwrite directory ${src} ` +
87          `with non-directory ${dest}`,
88        path: dest,
89        syscall: 'cp',
90        errno: EISDIR,
91        code: 'EISDIR',
92      });
93    }
94    if (!srcStat.isDirectory() && destStat.isDirectory()) {
95      throw new ERR_FS_CP_NON_DIR_TO_DIR({
96        message: `cannot overwrite non-directory ${src} ` +
97          `with directory ${dest}`,
98        path: dest,
99        syscall: 'cp',
100        errno: ENOTDIR,
101        code: 'ENOTDIR',
102      });
103    }
104  }
105
106  if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
107    throw new ERR_FS_CP_EINVAL({
108      message: `cannot copy ${src} to a subdirectory of self ${dest}`,
109      path: dest,
110      syscall: 'cp',
111      errno: EINVAL,
112      code: 'EINVAL',
113    });
114  }
115  return { __proto__: null, srcStat, destStat, skipped: false };
116}
117
118function getStatsSync(src, dest, opts) {
119  let destStat;
120  const statFunc = opts.dereference ?
121    (file) => statSync(file, { bigint: true }) :
122    (file) => lstatSync(file, { bigint: true });
123  const srcStat = statFunc(src);
124  try {
125    destStat = statFunc(dest);
126  } catch (err) {
127    if (err.code === 'ENOENT') return { srcStat, destStat: null };
128    throw err;
129  }
130  return { srcStat, destStat };
131}
132
133function checkParentPathsSync(src, srcStat, dest) {
134  const srcParent = resolve(dirname(src));
135  const destParent = resolve(dirname(dest));
136  if (destParent === srcParent || destParent === parse(destParent).root) return;
137  let destStat;
138  try {
139    destStat = statSync(destParent, { bigint: true });
140  } catch (err) {
141    if (err.code === 'ENOENT') return;
142    throw err;
143  }
144  if (areIdentical(srcStat, destStat)) {
145    throw new ERR_FS_CP_EINVAL({
146      message: `cannot copy ${src} to a subdirectory of self ${dest}`,
147      path: dest,
148      syscall: 'cp',
149      errno: EINVAL,
150      code: 'EINVAL',
151    });
152  }
153  return checkParentPathsSync(src, srcStat, destParent);
154}
155
156function checkParentDir(destStat, src, dest, opts) {
157  const destParent = dirname(dest);
158  if (!existsSync(destParent)) mkdirSync(destParent, { recursive: true });
159  return getStats(destStat, src, dest, opts);
160}
161
162function getStats(destStat, src, dest, opts) {
163  const statSyncFn = opts.dereference ? statSync : lstatSync;
164  const srcStat = statSyncFn(src);
165
166  if (srcStat.isDirectory() && opts.recursive) {
167    return onDir(srcStat, destStat, src, dest, opts);
168  } else if (srcStat.isDirectory()) {
169    throw new ERR_FS_EISDIR({
170      message: `${src} is a directory (not copied)`,
171      path: src,
172      syscall: 'cp',
173      errno: EINVAL,
174      code: 'EISDIR',
175    });
176  } else if (srcStat.isFile() ||
177           srcStat.isCharacterDevice() ||
178           srcStat.isBlockDevice()) {
179    return onFile(srcStat, destStat, src, dest, opts);
180  } else if (srcStat.isSymbolicLink()) {
181    return onLink(destStat, src, dest, opts);
182  } else if (srcStat.isSocket()) {
183    throw new ERR_FS_CP_SOCKET({
184      message: `cannot copy a socket file: ${dest}`,
185      path: dest,
186      syscall: 'cp',
187      errno: EINVAL,
188      code: 'EINVAL',
189    });
190  } else if (srcStat.isFIFO()) {
191    throw new ERR_FS_CP_FIFO_PIPE({
192      message: `cannot copy a FIFO pipe: ${dest}`,
193      path: dest,
194      syscall: 'cp',
195      errno: EINVAL,
196      code: 'EINVAL',
197    });
198  }
199  throw new ERR_FS_CP_UNKNOWN({
200    message: `cannot copy an unknown file type: ${dest}`,
201    path: dest,
202    syscall: 'cp',
203    errno: EINVAL,
204    code: 'EINVAL',
205  });
206}
207
208function onFile(srcStat, destStat, src, dest, opts) {
209  if (!destStat) return copyFile(srcStat, src, dest, opts);
210  return mayCopyFile(srcStat, src, dest, opts);
211}
212
213function mayCopyFile(srcStat, src, dest, opts) {
214  if (opts.force) {
215    unlinkSync(dest);
216    return copyFile(srcStat, src, dest, opts);
217  } else if (opts.errorOnExist) {
218    throw new ERR_FS_CP_EEXIST({
219      message: `${dest} already exists`,
220      path: dest,
221      syscall: 'cp',
222      errno: EEXIST,
223      code: 'EEXIST',
224    });
225  }
226}
227
228function copyFile(srcStat, src, dest, opts) {
229  copyFileSync(src, dest, opts.mode);
230  if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest);
231  return setDestMode(dest, srcStat.mode);
232}
233
234function handleTimestamps(srcMode, src, dest) {
235  // Make sure the file is writable before setting the timestamp
236  // otherwise open fails with EPERM when invoked with 'r+'
237  // (through utimes call)
238  if (fileIsNotWritable(srcMode)) makeFileWritable(dest, srcMode);
239  return setDestTimestamps(src, dest);
240}
241
242function fileIsNotWritable(srcMode) {
243  return (srcMode & 0o200) === 0;
244}
245
246function makeFileWritable(dest, srcMode) {
247  return setDestMode(dest, srcMode | 0o200);
248}
249
250function setDestMode(dest, srcMode) {
251  return chmodSync(dest, srcMode);
252}
253
254function setDestTimestamps(src, dest) {
255  // The initial srcStat.atime cannot be trusted
256  // because it is modified by the read(2) system call
257  // (See https://nodejs.org/api/fs.html#fs_stat_time_values)
258  const updatedSrcStat = statSync(src);
259  return utimesSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime);
260}
261
262function onDir(srcStat, destStat, src, dest, opts) {
263  if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts);
264  return copyDir(src, dest, opts);
265}
266
267function mkDirAndCopy(srcMode, src, dest, opts) {
268  mkdirSync(dest);
269  copyDir(src, dest, opts);
270  return setDestMode(dest, srcMode);
271}
272
273function copyDir(src, dest, opts) {
274  const dir = opendirSync(src);
275
276  try {
277    let dirent;
278
279    while ((dirent = dir.readSync()) !== null) {
280      const { name } = dirent;
281      const srcItem = join(src, name);
282      const destItem = join(dest, name);
283      const { destStat, skipped } = checkPathsSync(srcItem, destItem, opts);
284      if (!skipped) getStats(destStat, srcItem, destItem, opts);
285    }
286  } finally {
287    dir.closeSync();
288  }
289}
290
291function onLink(destStat, src, dest, opts) {
292  let resolvedSrc = readlinkSync(src);
293  if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
294    resolvedSrc = resolve(dirname(src), resolvedSrc);
295  }
296  if (!destStat) {
297    return symlinkSync(resolvedSrc, dest);
298  }
299  let resolvedDest;
300  try {
301    resolvedDest = readlinkSync(dest);
302  } catch (err) {
303    // Dest exists and is a regular file or directory,
304    // Windows may throw UNKNOWN error. If dest already exists,
305    // fs throws error anyway, so no need to guard against it here.
306    if (err.code === 'EINVAL' || err.code === 'UNKNOWN') {
307      return symlinkSync(resolvedSrc, dest);
308    }
309    throw err;
310  }
311  if (!isAbsolute(resolvedDest)) {
312    resolvedDest = resolve(dirname(dest), resolvedDest);
313  }
314  if (isSrcSubdir(resolvedSrc, resolvedDest)) {
315    throw new ERR_FS_CP_EINVAL({
316      message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
317          `${resolvedDest}`,
318      path: dest,
319      syscall: 'cp',
320      errno: EINVAL,
321      code: 'EINVAL',
322    });
323  }
324  // Prevent copy if src is a subdir of dest since unlinking
325  // dest in this case would result in removing src contents
326  // and therefore a broken symlink would be created.
327  if (statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
328    throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
329      message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
330      path: dest,
331      syscall: 'cp',
332      errno: EINVAL,
333      code: 'EINVAL',
334    });
335  }
336  return copyLink(resolvedSrc, dest);
337}
338
339function copyLink(resolvedSrc, dest) {
340  unlinkSync(dest);
341  return symlinkSync(resolvedSrc, dest);
342}
343
344module.exports = { cpSyncFn };
345