[io/doc/test] Modify the Windows symlink resolution behavior so that resolving a link that points to a non-existent file results in a type of notFound, which is consistent with all other platforms.

Bug:https://github.com/dart-lang/sdk/issues/53684
Change-Id: I1b594e1a85906d1f510358eec71792ea15ab801b
CoreLibraryReviewExempt: VM- and doc-only
Tested: unit tests
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/331841
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Brian Quinlan <bquinlan@google.com>
This commit is contained in:
Brian Quinlan 2023-10-25 23:14:38 +00:00 committed by Commit Queue
parent 10a5df43af
commit 097c84f11b
8 changed files with 125 additions and 24 deletions

View file

@ -1130,7 +1130,12 @@ File::Type File::GetType(Namespace* namespc,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (target_handle == INVALID_HANDLE_VALUE) {
result = File::kIsLink;
DWORD last_error = GetLastError();
if ((last_error == ERROR_FILE_NOT_FOUND) ||
(last_error == ERROR_PATH_NOT_FOUND)) {
return kDoesNotExist;
}
result = kIsLink;
} else {
BY_HANDLE_FILE_INFORMATION info;
if (!GetFileInformationByHandle(target_handle, &info)) {
@ -1139,8 +1144,8 @@ File::Type File::GetType(Namespace* namespc,
}
CloseHandle(target_handle);
return ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
? File::kIsDirectory
: File::kIsFile;
? kIsDirectory
: kIsFile;
}
} else {
result = kIsLink;

View file

@ -308,6 +308,10 @@ abstract class FileSystemEntity {
/// ```
/// since `Uri.resolve` removes `..` segments. This will result in the Windows
/// behavior.
///
/// On Windows, attempting to resolve a symbolic link where the link type
/// does not match the type of the resolved file system object will cause the
/// Future to complete with a [PathAccessException] error.
Future<String> resolveSymbolicLinks() {
return _File._dispatchWithNamespace(
_IOService.fileResolveSymbolicLinks, [null, _rawPath]).then((response) {
@ -343,6 +347,11 @@ abstract class FileSystemEntity {
/// ```
/// since `Uri.resolve` removes `..` segments. This will result in the Windows
/// behavior.
///
/// On Windows, a symbolic link is created as either a file link or a
/// directory link. Attempting to resolve such a symbolic link requires the
/// link type to match the type of the file system object that it points to,
/// otherwise it throws a [PathAccessException].
String resolveSymbolicLinksSync() {
var result = _resolveSymbolicLinks(_Namespace._namespace, _rawPath);
_throwIfError(result, "Cannot resolve symbolic links", path);
@ -685,11 +694,15 @@ abstract class FileSystemEntity {
/// Synchronously finds the type of file system object that a path points to.
///
/// Returns [FileSystemEntityType.link] only if [followLinks] is `false` and if
/// [path] points to a link.
///
/// Returns [FileSystemEntityType.notFound] if [path] does not point to a file
/// system object or if any other error occurs in looking up the path.
///
/// If [path] points to a link and [followLinks] is `true` then the result
/// will be for the file system object that the link points to. If that
/// object does not exist then the result will be
/// [FileSystemEntityType.notFound]. If [path] points to a link and
/// [followLinks] is `false` then the result will be
/// [FileSystemEntityType.link].
static FileSystemEntityType typeSync(String path, {bool followLinks = true}) {
return _getTypeSync(_toUtf8Array(path), followLinks);
}

View file

@ -48,10 +48,20 @@ abstract interface class Link implements FileSystemEntity {
/// not affected, unless they are also in [path].
///
/// On the Windows platform, this call will create a true symbolic link
/// instead of a junction. The link represents a file or directory and
/// does not change its type after creation. If [target] exists then
/// the type of the link will match the type [target], otherwise a file
/// symlink is created.
/// instead of a junction. Windows treats links to files and links to
/// directories as different and non-interchangable kinds of links.
/// Each link is either a file-link or a directory-link, and the type is
/// chosen when the link is created, and the link then counts as either a
/// file or directory for most purposes. Different Win32 API calls are
/// used to manipulate each. For example, the `DeleteFile` function is
/// used to delete links to files, and `RemoveDirectory` must be used to
/// delete links to directories.
///
/// The created Windows symbolic link will match the type of the [target],
/// if [target] exists, otherwise a file-link is created. The type of the
/// created link will not change if [target] is later replaced by something
/// of a different type and the link will not be resolvable by
/// [resolveSymbolicLinks].
///
/// In order to create a symbolic link on Windows, Dart must be run in
/// Administrator mode or the system must have Developer Mode enabled,
@ -76,10 +86,20 @@ abstract interface class Link implements FileSystemEntity {
/// the path of [target] are not affected, unless they are also in [path].
///
/// On the Windows platform, this call will create a true symbolic link
/// instead of a junction. The link represents a file or directory and
/// does not change its type after creation. If [target] exists then
/// the type of the link will match the type [target], otherwise a file
/// symlink is created.
/// instead of a junction. Windows treats links to files and links to
/// directories as different and non-interchangable kinds of links.
/// Each link is either a file-link or a directory-link, and the type is
/// chosen when the link is created, and the link then counts as either a
/// file or directory for most purposes. Different Win32 API calls are
/// used to manipulate each. For example, the `DeleteFile` function is
/// used to delete links to files, and `RemoveDirectory` must be used to
/// delete links to directories.
///
/// The created Windows symbolic link will match the type of the [target],
/// if [target] exists, otherwise a file-link is created. The type of the
/// created link will not change if [target] is later replaced by something
/// of a different type and the link will not be resolvable by
/// [resolveSymbolicLinks].
///
/// In order to create a symbolic link on Windows, Dart must be run in
/// Administrator mode or the system must have Developer Mode enabled,
@ -91,16 +111,22 @@ abstract interface class Link implements FileSystemEntity {
/// it will be interpreted relative to the directory containing the link.
void createSync(String target, {bool recursive = false});
/// Synchronously updates the link.
/// Synchronously updates an existing link.
///
/// Calling [updateSync] on a non-existing link will throw an exception.
/// Deletes the existing link at [path] and uses [createSync] to create a new
/// link to [target]. Throws [PathNotFoundException] if the original link
/// does not exist or with any [FileSystemException] that [deleteSync] or
/// [createSync] can throw.
void updateSync(String target);
/// Updates the link.
/// Updates an existing link.
///
/// Returns a `Future<Link>` that completes with the
/// link when it has been updated. Calling [update] on a non-existing link
/// will complete its returned future with an exception.
/// Deletes the existing link at [path] and creates a new link to [target],
/// using [create].
///
/// Returns a future which completes with this `Link` if successful,
/// and with a [PathNotFoundException] if there is no existing link at [path],
/// or with any [FileSystemException] that [delete] or [create] can throw.
Future<Link> update(String target);
Future<String> resolveSymbolicLinks();

View file

@ -280,11 +280,24 @@ Future testRelativeLinks(_) {
});
}
Future testBrokenLinkTypeSync(_) async {
String base = (await Directory.systemTemp.createTemp('dart_link')).path;
String link = join(base, 'link');
await Link(link).create('does not exist');
Expect.equals(FileSystemEntityType.link,
await FileSystemEntity.type(link, followLinks: false));
Expect.equals(FileSystemEntityType.notFound,
await FileSystemEntity.type(link, followLinks: true));
}
main() {
asyncStart();
testCreate()
.then(testCreateLoopingLink)
.then(testRename)
.then(testRelativeLinks)
.then(testBrokenLinkTypeSync)
.then((_) => asyncEnd());
}

View file

@ -314,6 +314,18 @@ testIsDir() async {
sandbox.deleteSync(recursive: true);
}
testBrokenLinkTypeSync() {
String base = Directory.systemTemp.createTempSync('dart_link').path;
String link = join(base, 'link');
Link(link).createSync('does not exist');
Expect.equals(FileSystemEntityType.link,
FileSystemEntity.typeSync(link, followLinks: false));
Expect.equals(FileSystemEntityType.notFound,
FileSystemEntity.typeSync(link, followLinks: true));
}
main() {
testCreateSync();
testCreateLoopingLink();
@ -321,4 +333,5 @@ main() {
testLinkErrorSync();
testRelativeLinksSync();
testIsDir();
testBrokenLinkTypeSync();
}

View file

@ -95,6 +95,8 @@ main() {
tempDir.delete(recursive: true);
});
}));
asyncTest(testLinkTargetTypeChangedAfterCreation);
}
Future makeEntities(String temp) {
@ -154,3 +156,32 @@ Future testLink(String name) {
.then((identical) => Expect.isTrue(identical));
});
}
Future testLinkTargetTypeChangedAfterCreation() async {
// Test the following scenario:
// 1. create a file
// 2. create a link to that file
// 3. replace the file with a directory
// 4. attempt to resolve the link
final tmp =
await Directory.systemTemp.createTemp('dart_resolve_symbolic_links');
final tmpPath = tmp.absolute.path;
final filePath = join(tmpPath, "file");
final linkPath = join(tmpPath, "link");
await File(filePath).create();
await Link(linkPath).create(filePath);
Expect.isTrue(FileSystemEntity.identicalSync(
filePath, await Directory(linkPath).resolveSymbolicLinks()));
await File(filePath).delete();
await Directory(filePath).create();
if (Platform.isWindows) {
await asyncExpectThrows<PathAccessException>(
Directory(linkPath).resolveSymbolicLinks());
} else {
Expect.isTrue(await FileSystemEntity.identical(
filePath, await Directory(linkPath).resolveSymbolicLinks()));
}
}

View file

@ -57,7 +57,7 @@ Future testJunctionTypeDelete() {
.then((_) => FutureExpect.isFalse(FileSystemEntity.isDirectory(y)))
.then((_) => FutureExpect.isFalse(FileSystemEntity.isDirectory(x)))
.then((_) => FutureExpect.equals(
FileSystemEntityType.link, FileSystemEntity.type(y)))
FileSystemEntityType.notFound, FileSystemEntity.type(y)))
.then((_) => FutureExpect.equals(
FileSystemEntityType.notFound, FileSystemEntity.type(x)))
.then((_) => FutureExpect.equals(FileSystemEntityType.link,
@ -86,8 +86,8 @@ Future testJunctionTypeDelete() {
FileSystemEntityType.notFound, FileSystemEntity.type(y)))
.then((_) => FutureExpect.equals(FileSystemEntityType.notFound,
FileSystemEntity.type(y, followLinks: false)))
.then((_) =>
FutureExpect.equals(FileSystemEntityType.directory, FileSystemEntity.type(x)))
.then(
(_) => FutureExpect.equals(FileSystemEntityType.directory, FileSystemEntity.type(x)))
.then((_) => FutureExpect.equals(FileSystemEntityType.directory, FileSystemEntity.type(x, followLinks: false)))
.then((_) => FutureExpect.throws(new Link(y).target()))
.then((_) => temp.delete(recursive: true));

View file

@ -36,7 +36,7 @@ testJunctionTypeDelete() {
Expect.isFalse(FileSystemEntity.isLinkSync(x));
Expect.isFalse(FileSystemEntity.isDirectorySync(y));
Expect.isFalse(FileSystemEntity.isDirectorySync(x));
Expect.equals(FileSystemEntityType.link, FileSystemEntity.typeSync(y));
Expect.equals(FileSystemEntityType.notFound, FileSystemEntity.typeSync(y));
Expect.equals(FileSystemEntityType.notFound, FileSystemEntity.typeSync(x));
Expect.equals(FileSystemEntityType.link,
FileSystemEntity.typeSync(y, followLinks: false));