diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index 9cef597a48..b76fac1a58 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -1284,8 +1284,10 @@ declare namespace Deno { * * Deno.renameSync("old/path", "new/path"); * - * Throws error if attempting to rename to a directory which exists and is not - * empty. + * On Unix, this operation does not follow symlinks at either path. + * + * It varies between platforms when the operation throws errors, and if so what + * they are. It's always an error to rename anything to a non-empty directory. * * Requires `allow-read` and `allow-write` permissions. */ export function renameSync(oldpath: string, newpath: string): void; @@ -1297,10 +1299,12 @@ declare namespace Deno { * * await Deno.rename("old/path", "new/path"); * - * Throws error if attempting to rename to a directory which exists and is not - * empty. + * On Unix, this operation does not follow symlinks at either path. * - * Requires `allow-read` and `allow-write`. */ + * It varies between platforms when the operation throws errors, and if so what + * they are. It's always an error to rename anything to a non-empty directory. + * + * Requires `allow-read` and `allow-write` permission. */ export function rename(oldpath: string, newpath: string): Promise; /** Synchronously reads and returns the entire contents of a file as an array diff --git a/cli/js/tests/rename_test.ts b/cli/js/tests/rename_test.ts index 288f24bd70..45f6d709e2 100644 --- a/cli/js/tests/rename_test.ts +++ b/cli/js/tests/rename_test.ts @@ -1,5 +1,31 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { unitTest, assert, assertEquals } from "./test_util.ts"; +import { unitTest, assert, assertEquals, assertThrows } from "./test_util.ts"; + +function assertMissing(path: string): void { + let caughtErr = false; + let info; + try { + info = Deno.lstatSync(path); + } catch (e) { + caughtErr = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtErr); + assertEquals(info, undefined); +} + +function assertFile(path: string): void { + const info = Deno.lstatSync(path); + assert(info.isFile()); +} + +function assertDirectory(path: string, mode?: number): void { + const info = Deno.lstatSync(path); + assert(info.isDirectory()); + if (Deno.build.os !== "win" && mode !== undefined) { + assertEquals(info.mode! & 0o777, mode & ~Deno.umask()); + } +} unitTest( { perms: { read: true, write: true } }, @@ -9,20 +35,8 @@ unitTest( const newpath = testDir + "/newpath"; Deno.mkdirSync(oldpath); Deno.renameSync(oldpath, newpath); - const newPathInfo = Deno.statSync(newpath); - assert(newPathInfo.isDirectory()); - - let caughtErr = false; - let oldPathInfo; - - try { - oldPathInfo = Deno.statSync(oldpath); - } catch (e) { - caughtErr = true; - assert(e instanceof Deno.errors.NotFound); - } - assert(caughtErr); - assertEquals(oldPathInfo, undefined); + assertDirectory(newpath); + assertMissing(oldpath); } ); @@ -66,19 +80,137 @@ unitTest( const newpath = testDir + "/newpath"; Deno.mkdirSync(oldpath); await Deno.rename(oldpath, newpath); - const newPathInfo = Deno.statSync(newpath); - assert(newPathInfo.isDirectory()); - - let caughtErr = false; - let oldPathInfo; - - try { - oldPathInfo = Deno.statSync(oldpath); - } catch (e) { - caughtErr = true; - assert(e instanceof Deno.errors.NotFound); - } - assert(caughtErr); - assertEquals(oldPathInfo, undefined); + assertDirectory(newpath); + assertMissing(oldpath); + } +); + +function readFileString(filename: string): string { + const dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + return dec.decode(dataRead); +} + +function writeFileString(filename: string, s: string): void { + const enc = new TextEncoder(); + const data = enc.encode(s); + Deno.writeFileSync(filename, data, { mode: 0o666 }); +} + +unitTest( + { ignore: Deno.build.os === "win", perms: { read: true, write: true } }, + function renameSyncErrorsUnix(): void { + const testDir = Deno.makeTempDirSync(); + const oldfile = testDir + "/oldfile"; + const olddir = testDir + "/olddir"; + const emptydir = testDir + "/empty"; + const fulldir = testDir + "/dir"; + const file = fulldir + "/file"; + writeFileString(oldfile, "Hello"); + Deno.mkdirSync(olddir); + Deno.mkdirSync(emptydir); + Deno.mkdirSync(fulldir); + writeFileString(file, "world"); + + assertThrows( + (): void => { + Deno.renameSync(oldfile, emptydir); + }, + Error, + "Is a directory" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, fulldir); + }, + Error, + "Directory not empty" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, file); + }, + Error, + "Not a directory" + ); + + const fileLink = testDir + "/fileLink"; + const dirLink = testDir + "/dirLink"; + const danglingLink = testDir + "/danglingLink"; + Deno.symlinkSync(file, fileLink); + Deno.symlinkSync(emptydir, dirLink); + Deno.symlinkSync(testDir + "/nonexistent", danglingLink); + + assertThrows( + (): void => { + Deno.renameSync(olddir, fileLink); + }, + Error, + "Not a directory" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, dirLink); + }, + Error, + "Not a directory" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, danglingLink); + }, + Error, + "Not a directory" + ); + + // should succeed on Unix + Deno.renameSync(olddir, emptydir); + Deno.renameSync(oldfile, dirLink); + Deno.renameSync(dirLink, danglingLink); + assertFile(danglingLink); + assertEquals("Hello", readFileString(danglingLink)); + } +); + +unitTest( + { ignore: Deno.build.os !== "win", perms: { read: true, write: true } }, + function renameSyncErrorsWin(): void { + const testDir = Deno.makeTempDirSync(); + const oldfile = testDir + "/oldfile"; + const olddir = testDir + "/olddir"; + const emptydir = testDir + "/empty"; + const fulldir = testDir + "/dir"; + const file = fulldir + "/file"; + writeFileString(oldfile, "Hello"); + Deno.mkdirSync(olddir); + Deno.mkdirSync(emptydir); + Deno.mkdirSync(fulldir); + writeFileString(file, "world"); + + assertThrows( + (): void => { + Deno.renameSync(oldfile, emptydir); + }, + Deno.errors.PermissionDenied, + "Access is denied" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, fulldir); + }, + Deno.errors.PermissionDenied, + "Access is denied" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, emptydir); + }, + Deno.errors.PermissionDenied, + "Access is denied" + ); + + // should succeed on Windows + Deno.renameSync(olddir, file); + assertDirectory(file); } );