1// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2// for details. All rights reserved. Use of this source code is governed by a
3// BSD-style license that can be found in the LICENSE file.
4
5#include "platform/globals.h"
6#if defined(HOST_OS_FUCHSIA)
7
8#include "bin/file.h"
9
10#include <errno.h> // NOLINT
11#include <fcntl.h> // NOLINT
12#include <lib/fdio/fdio.h> // NOLINT
13#include <lib/fdio/namespace.h> // NOLINT
14#include <libgen.h> // NOLINT
15#include <sys/mman.h> // NOLINT
16#include <sys/stat.h> // NOLINT
17#include <sys/types.h> // NOLINT
18#include <unistd.h> // NOLINT
19#include <utime.h> // NOLINT
20
21#include "bin/builtin.h"
22#include "bin/fdutils.h"
23#include "bin/namespace.h"
24#include "platform/signal_blocker.h"
25#include "platform/syslog.h"
26#include "platform/utils.h"
27
28namespace dart {
29namespace bin {
30
31class FileHandle {
32 public:
33 explicit FileHandle(int fd) : fd_(fd) {}
34 ~FileHandle() {}
35 int fd() const { return fd_; }
36 void set_fd(int fd) { fd_ = fd; }
37
38 private:
39 int fd_;
40
41 DISALLOW_COPY_AND_ASSIGN(FileHandle);
42};
43
44File::~File() {
45 if (!IsClosed() && (handle_->fd() != STDOUT_FILENO) &&
46 (handle_->fd() != STDERR_FILENO)) {
47 Close();
48 }
49 delete handle_;
50}
51
52void File::Close() {
53 ASSERT(handle_->fd() >= 0);
54 if (handle_->fd() == STDOUT_FILENO) {
55 // If stdout, redirect fd to Fuchsia's equivalent of /dev/null.
56 auto* null_fdio = fdio_null_create();
57 ASSERT(null_fdio != nullptr);
58 int null_fd = NO_RETRY_EXPECTED(fdio_bind_to_fd(null_fdio, -1, 0));
59 ASSERT(null_fd >= 0);
60 VOID_NO_RETRY_EXPECTED(dup2(null_fd, handle_->fd()));
61 VOID_NO_RETRY_EXPECTED(close(null_fd));
62 } else {
63 int err = NO_RETRY_EXPECTED(close(handle_->fd()));
64 if (err != 0) {
65 const int kBufferSize = 1024;
66 char error_buf[kBufferSize];
67 Syslog::PrintErr("%s\n", Utils::StrError(errno, error_buf, kBufferSize));
68 }
69 }
70 handle_->set_fd(kClosedFd);
71}
72
73intptr_t File::GetFD() {
74 return handle_->fd();
75}
76
77bool File::IsClosed() {
78 return handle_->fd() == kClosedFd;
79}
80
81MappedMemory* File::Map(MapType type,
82 int64_t position,
83 int64_t length,
84 void* start) {
85 ASSERT(handle_->fd() >= 0);
86 ASSERT(length > 0);
87 int prot = PROT_NONE;
88 switch (type) {
89 case kReadOnly:
90 prot = PROT_READ;
91 break;
92 case kReadExecute:
93 prot = PROT_READ | PROT_EXEC;
94 break;
95 case kReadWrite:
96 prot = PROT_READ | PROT_WRITE;
97 break;
98 }
99 const int flags = MAP_PRIVATE | (start != nullptr ? MAP_FIXED : 0);
100 void* addr = mmap(start, length, prot, flags, handle_->fd(), position);
101 if (addr == MAP_FAILED) {
102 return NULL;
103 }
104 return new MappedMemory(addr, length, /*should_unmap=*/start == nullptr);
105}
106
107void MappedMemory::Unmap() {
108 int result = munmap(address_, size_);
109 ASSERT(result == 0);
110 address_ = 0;
111 size_ = 0;
112}
113
114int64_t File::Read(void* buffer, int64_t num_bytes) {
115 ASSERT(handle_->fd() >= 0);
116 return NO_RETRY_EXPECTED(read(handle_->fd(), buffer, num_bytes));
117}
118
119int64_t File::Write(const void* buffer, int64_t num_bytes) {
120 ASSERT(handle_->fd() >= 0);
121 return NO_RETRY_EXPECTED(write(handle_->fd(), buffer, num_bytes));
122}
123
124bool File::VPrint(const char* format, va_list args) {
125 // Measure.
126 va_list measure_args;
127 va_copy(measure_args, args);
128 intptr_t len = vsnprintf(NULL, 0, format, measure_args);
129 va_end(measure_args);
130
131 char* buffer = reinterpret_cast<char*>(malloc(len + 1));
132
133 // Print.
134 va_list print_args;
135 va_copy(print_args, args);
136 vsnprintf(buffer, len + 1, format, print_args);
137 va_end(print_args);
138
139 bool result = WriteFully(buffer, len);
140 free(buffer);
141 return result;
142}
143
144int64_t File::Position() {
145 ASSERT(handle_->fd() >= 0);
146 return NO_RETRY_EXPECTED(lseek(handle_->fd(), 0, SEEK_CUR));
147}
148
149bool File::SetPosition(int64_t position) {
150 ASSERT(handle_->fd() >= 0);
151 return NO_RETRY_EXPECTED(lseek(handle_->fd(), position, SEEK_SET)) >= 0;
152}
153
154bool File::Truncate(int64_t length) {
155 ASSERT(handle_->fd() >= 0);
156 return NO_RETRY_EXPECTED(ftruncate(handle_->fd(), length) != -1);
157}
158
159bool File::Flush() {
160 ASSERT(handle_->fd() >= 0);
161 return NO_RETRY_EXPECTED(fsync(handle_->fd())) != -1;
162}
163
164bool File::Lock(File::LockType lock, int64_t start, int64_t end) {
165 ASSERT(handle_->fd() >= 0);
166 ASSERT((end == -1) || (end > start));
167 struct flock fl;
168 switch (lock) {
169 case File::kLockUnlock:
170 fl.l_type = F_UNLCK;
171 break;
172 case File::kLockShared:
173 case File::kLockBlockingShared:
174 fl.l_type = F_RDLCK;
175 break;
176 case File::kLockExclusive:
177 case File::kLockBlockingExclusive:
178 fl.l_type = F_WRLCK;
179 break;
180 default:
181 return false;
182 }
183 fl.l_whence = SEEK_SET;
184 fl.l_start = start;
185 fl.l_len = end == -1 ? 0 : end - start;
186 int cmd = F_SETLK;
187 if ((lock == File::kLockBlockingShared) ||
188 (lock == File::kLockBlockingExclusive)) {
189 cmd = F_SETLKW;
190 }
191 return NO_RETRY_EXPECTED(fcntl(handle_->fd(), cmd, &fl)) != -1;
192}
193
194int64_t File::Length() {
195 ASSERT(handle_->fd() >= 0);
196 struct stat st;
197 if (NO_RETRY_EXPECTED(fstat(handle_->fd(), &st)) == 0) {
198 return st.st_size;
199 }
200 return -1;
201}
202
203File* File::FileOpenW(const wchar_t* system_name, FileOpenMode mode) {
204 UNREACHABLE();
205 return NULL;
206}
207
208File* File::OpenFD(int fd) {
209 return new File(new FileHandle(fd));
210}
211
212File* File::Open(Namespace* namespc, const char* name, FileOpenMode mode) {
213 NamespaceScope ns(namespc, name);
214 // Report errors for non-regular files.
215 struct stat st;
216 if (NO_RETRY_EXPECTED(fstatat(ns.fd(), ns.path(), &st, 0)) == 0) {
217 if (S_ISDIR(st.st_mode)) {
218 errno = EISDIR;
219 return NULL;
220 }
221 }
222 int flags = O_RDONLY;
223 if ((mode & kWrite) != 0) {
224 ASSERT((mode & kWriteOnly) == 0);
225 flags = (O_RDWR | O_CREAT);
226 }
227 if ((mode & kWriteOnly) != 0) {
228 ASSERT((mode & kWrite) == 0);
229 flags = (O_WRONLY | O_CREAT);
230 }
231 if ((mode & kTruncate) != 0) {
232 flags = flags | O_TRUNC;
233 }
234 flags |= O_CLOEXEC;
235 int fd = NO_RETRY_EXPECTED(openat(ns.fd(), ns.path(), flags, 0666));
236 if (fd < 0) {
237 return NULL;
238 }
239 if ((((mode & kWrite) != 0) && ((mode & kTruncate) == 0)) ||
240 (((mode & kWriteOnly) != 0) && ((mode & kTruncate) == 0))) {
241 int64_t position = lseek(fd, 0, SEEK_END);
242 if (position < 0) {
243 return NULL;
244 }
245 }
246 return OpenFD(fd);
247}
248
249Utils::CStringUniquePtr File::UriToPath(const char* uri) {
250 const char* path = (strlen(uri) >= 8 && strncmp(uri, "file:///", 8) == 0)
251 ? uri + 7 : uri;
252 UriDecoder uri_decoder(path);
253 if (uri_decoder.decoded() == nullptr) {
254 errno = EINVAL;
255 return Utils::CreateCStringUniquePtr(nullptr);
256 }
257 return Utils::CreateCStringUniquePtr(strdup(uri_decoder.decoded()));
258}
259
260File* File::OpenUri(Namespace* namespc, const char* uri, FileOpenMode mode) {
261 auto path = UriToPath(uri);
262 if (path == nullptr) {
263 return nullptr;
264 }
265 return File::Open(namespc, path.get(), mode);
266}
267
268File* File::OpenStdio(int fd) {
269 return new File(new FileHandle(fd));
270}
271
272bool File::Exists(Namespace* namespc, const char* name) {
273 NamespaceScope ns(namespc, name);
274 struct stat st;
275 if (NO_RETRY_EXPECTED(fstatat(ns.fd(), ns.path(), &st, 0)) == 0) {
276 // Everything but a directory and a link is a file to Dart.
277 return !S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode);
278 }
279 return false;
280}
281
282bool File::ExistsUri(Namespace* namespc, const char* uri) {
283 auto path = UriToPath(uri);
284 if (path == nullptr) {
285 return false;
286 }
287 return File::Exists(namespc, path.get());
288}
289
290bool File::Create(Namespace* namespc, const char* name) {
291 NamespaceScope ns(namespc, name);
292 const int fd = NO_RETRY_EXPECTED(
293 openat(ns.fd(), ns.path(), O_RDONLY | O_CREAT | O_CLOEXEC, 0666));
294 if (fd < 0) {
295 Syslog::PrintErr("File::Create() openat(%ld, %s) failed: %s\n", ns.fd(),
296 ns.path(), strerror(errno));
297 return false;
298 }
299 // File.create returns a File, so we shouldn't be giving the illusion that the
300 // call has created a file or that a file already exists if there is already
301 // an entity at the same path that is a directory or a link.
302 bool is_file = false;
303 struct stat st;
304 if (NO_RETRY_EXPECTED(fstat(fd, &st)) == 0) {
305 is_file = true;
306 if (S_ISDIR(st.st_mode)) {
307 errno = EISDIR;
308 is_file = false;
309 } else if (S_ISLNK(st.st_mode)) {
310 errno = ENOENT;
311 is_file = false;
312 }
313 }
314 FDUtils::SaveErrorAndClose(fd);
315 return is_file;
316}
317
318bool File::CreateLink(Namespace* namespc,
319 const char* name,
320 const char* target) {
321 NamespaceScope ns(namespc, name);
322 return NO_RETRY_EXPECTED(symlinkat(target, ns.fd(), ns.path())) == 0;
323}
324
325File::Type File::GetType(Namespace* namespc,
326 const char* name,
327 bool follow_links) {
328 NamespaceScope ns(namespc, name);
329 struct stat entry_info;
330 int stat_success;
331 if (follow_links) {
332 stat_success =
333 TEMP_FAILURE_RETRY(fstatat(ns.fd(), ns.path(), &entry_info, 0));
334 } else {
335 stat_success = TEMP_FAILURE_RETRY(
336 fstatat(ns.fd(), ns.path(), &entry_info, AT_SYMLINK_NOFOLLOW));
337 }
338 if (stat_success == -1) {
339 return File::kDoesNotExist;
340 }
341 if (S_ISDIR(entry_info.st_mode)) {
342 return File::kIsDirectory;
343 }
344 if (S_ISREG(entry_info.st_mode)) {
345 return File::kIsFile;
346 }
347 if (S_ISLNK(entry_info.st_mode)) {
348 return File::kIsLink;
349 }
350 return File::kDoesNotExist;
351}
352
353static bool CheckTypeAndSetErrno(Namespace* namespc,
354 const char* name,
355 File::Type expected,
356 bool follow_links) {
357 File::Type actual = File::GetType(namespc, name, follow_links);
358 if (actual == expected) {
359 return true;
360 }
361 switch (actual) {
362 case File::kIsDirectory:
363 errno = EISDIR;
364 break;
365 case File::kDoesNotExist:
366 errno = ENOENT;
367 break;
368 default:
369 errno = EINVAL;
370 break;
371 }
372 return false;
373}
374
375bool File::Delete(Namespace* namespc, const char* name) {
376 NamespaceScope ns(namespc, name);
377 return CheckTypeAndSetErrno(namespc, name, kIsFile, true) &&
378 (NO_RETRY_EXPECTED(unlinkat(ns.fd(), ns.path(), 0)) == 0);
379}
380
381bool File::DeleteLink(Namespace* namespc, const char* name) {
382 NamespaceScope ns(namespc, name);
383 return CheckTypeAndSetErrno(namespc, name, kIsLink, false) &&
384 (NO_RETRY_EXPECTED(unlinkat(ns.fd(), ns.path(), 0)) == 0);
385}
386
387bool File::Rename(Namespace* namespc,
388 const char* old_path,
389 const char* new_path) {
390 NamespaceScope oldns(namespc, old_path);
391 NamespaceScope newns(namespc, new_path);
392 return CheckTypeAndSetErrno(namespc, old_path, kIsFile, true) &&
393 (NO_RETRY_EXPECTED(renameat(oldns.fd(), oldns.path(), newns.fd(),
394 newns.path())) == 0);
395}
396
397bool File::RenameLink(Namespace* namespc,
398 const char* old_path,
399 const char* new_path) {
400 NamespaceScope oldns(namespc, old_path);
401 NamespaceScope newns(namespc, new_path);
402 return CheckTypeAndSetErrno(namespc, old_path, kIsLink, false) &&
403 (NO_RETRY_EXPECTED(renameat(oldns.fd(), oldns.path(), newns.fd(),
404 newns.path())) == 0);
405}
406
407bool File::Copy(Namespace* namespc,
408 const char* old_path,
409 const char* new_path) {
410 if (!CheckTypeAndSetErrno(namespc, old_path, kIsFile, true)) {
411 return false;
412 }
413 NamespaceScope oldns(namespc, old_path);
414 struct stat st;
415 if (NO_RETRY_EXPECTED(fstatat(oldns.fd(), oldns.path(), &st, 0)) != 0) {
416 return false;
417 }
418 const int old_fd = NO_RETRY_EXPECTED(
419 openat(oldns.fd(), oldns.path(), O_RDONLY | O_CLOEXEC));
420 if (old_fd < 0) {
421 return false;
422 }
423 NamespaceScope newns(namespc, new_path);
424 const int new_fd = NO_RETRY_EXPECTED(
425 openat(newns.fd(), newns.path(),
426 O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, st.st_mode));
427 if (new_fd < 0) {
428 close(old_fd);
429 return false;
430 }
431 // TODO(ZX-429): Use sendfile/copyfile or equivalent when there is one.
432 intptr_t result;
433 const intptr_t kBufferSize = 8 * KB;
434 uint8_t* buffer = reinterpret_cast<uint8_t*>(malloc(kBufferSize));
435 while ((result = NO_RETRY_EXPECTED(read(old_fd, buffer, kBufferSize))) > 0) {
436 int wrote = NO_RETRY_EXPECTED(write(new_fd, buffer, result));
437 if (wrote != result) {
438 result = -1;
439 break;
440 }
441 }
442 free(buffer);
443 FDUtils::SaveErrorAndClose(old_fd);
444 FDUtils::SaveErrorAndClose(new_fd);
445 if (result < 0) {
446 int e = errno;
447 VOID_NO_RETRY_EXPECTED(unlinkat(newns.fd(), newns.path(), 0));
448 errno = e;
449 return false;
450 }
451 return true;
452}
453
454static bool StatHelper(Namespace* namespc,
455 const char* name,
456 struct stat* st) {
457 NamespaceScope ns(namespc, name);
458 if (NO_RETRY_EXPECTED(fstatat(ns.fd(), ns.path(), st, 0)) != 0) {
459 return false;
460 }
461 // Signal an error if it's a directory.
462 if (S_ISDIR(st->st_mode)) {
463 errno = EISDIR;
464 return false;
465 }
466 // Otherwise assume the caller knows what it's doing.
467 return true;
468}
469
470int64_t File::LengthFromPath(Namespace* namespc, const char* name) {
471 struct stat st;
472 if (!StatHelper(namespc, name, &st)) {
473 return -1;
474 }
475 return st.st_size;
476}
477
478static int64_t TimespecToMilliseconds(const struct timespec& t) {
479 return static_cast<int64_t>(t.tv_sec) * 1000L +
480 static_cast<int64_t>(t.tv_nsec) / 1000000L;
481}
482
483static void MillisecondsToTimespec(int64_t millis, struct timespec* t) {
484 ASSERT(t != NULL);
485 t->tv_sec = millis / kMillisecondsPerSecond;
486 t->tv_nsec = (millis % kMillisecondsPerSecond) * 1000L;
487}
488
489void File::Stat(Namespace* namespc, const char* name, int64_t* data) {
490 NamespaceScope ns(namespc, name);
491 struct stat st;
492 if (TEMP_FAILURE_RETRY(fstatat(ns.fd(), ns.path(), &st, 0)) == 0) {
493 if (S_ISREG(st.st_mode)) {
494 data[kType] = kIsFile;
495 } else if (S_ISDIR(st.st_mode)) {
496 data[kType] = kIsDirectory;
497 } else if (S_ISLNK(st.st_mode)) {
498 data[kType] = kIsLink;
499 } else {
500 data[kType] = kDoesNotExist;
501 }
502 data[kCreatedTime] = TimespecToMilliseconds(st.st_ctim);
503 data[kModifiedTime] = TimespecToMilliseconds(st.st_mtim);
504 data[kAccessedTime] = TimespecToMilliseconds(st.st_atim);
505 data[kMode] = st.st_mode;
506 data[kSize] = st.st_size;
507 } else {
508 data[kType] = kDoesNotExist;
509 }
510}
511
512time_t File::LastModified(Namespace* namespc, const char* name) {
513 struct stat st;
514 if (!StatHelper(namespc, name, &st)) {
515 return -1;
516 }
517 return st.st_mtime;
518}
519
520time_t File::LastAccessed(Namespace* namespc, const char* name) {
521 struct stat st;
522 if (!StatHelper(namespc, name, &st)) {
523 return -1;
524 }
525 return st.st_atime;
526}
527
528bool File::SetLastAccessed(Namespace* namespc,
529 const char* name,
530 int64_t millis) {
531 // First get the current times.
532 struct stat st;
533 if (!StatHelper(namespc, name, &st)) {
534 return false;
535 }
536
537 // Set the new time:
538 NamespaceScope ns(namespc, name);
539 struct timespec times[2];
540 MillisecondsToTimespec(millis, &times[0]);
541 times[1] = st.st_mtim;
542 return utimensat(ns.fd(), ns.path(), times, 0) == 0;
543}
544
545bool File::SetLastModified(Namespace* namespc,
546 const char* name,
547 int64_t millis) {
548 // First get the current times.
549 struct stat st;
550 if (!StatHelper(namespc, name, &st)) {
551 return false;
552 }
553
554 // Set the new time:
555 NamespaceScope ns(namespc, name);
556 struct timespec times[2];
557 times[0] = st.st_atim;
558 MillisecondsToTimespec(millis, &times[1]);
559 return utimensat(ns.fd(), ns.path(), times, 0) == 0;
560}
561
562const char* File::LinkTarget(Namespace* namespc,
563 const char* name,
564 char* dest,
565 int dest_size) {
566 NamespaceScope ns(namespc, name);
567 struct stat link_stats;
568 const int status = TEMP_FAILURE_RETRY(
569 fstatat(ns.fd(), ns.path(), &link_stats, AT_SYMLINK_NOFOLLOW));
570 if (status != 0) {
571 return NULL;
572 }
573 if (!S_ISLNK(link_stats.st_mode)) {
574 errno = ENOENT;
575 return NULL;
576 }
577 // Don't rely on the link_stats.st_size for the size of the link
578 // target. For some filesystems, e.g. procfs, this value is always
579 // 0. Also the link might have changed before the readlink call.
580 const int kBufferSize = PATH_MAX + 1;
581 char target[kBufferSize];
582 const int target_size =
583 TEMP_FAILURE_RETRY(readlinkat(ns.fd(), ns.path(), target, kBufferSize));
584 if (target_size <= 0) {
585 return NULL;
586 }
587 if (dest == NULL) {
588 dest = DartUtils::ScopedCString(target_size + 1);
589 } else {
590 ASSERT(dest_size > 0);
591 if (dest_size <= target_size) {
592 return NULL;
593 }
594 }
595 memmove(dest, target, target_size);
596 dest[target_size] = '\0';
597 return dest;
598}
599
600bool File::IsAbsolutePath(const char* pathname) {
601 return ((pathname != NULL) && (pathname[0] == '/'));
602}
603
604const char* File::GetCanonicalPath(Namespace* namespc,
605 const char* name,
606 char* dest,
607 int dest_size) {
608 if (name == NULL) {
609 return NULL;
610 }
611 if (!Namespace::IsDefault(namespc)) {
612 // TODO(zra): There is no realpathat(). Also chasing a symlink might result
613 // in a path to something outside of the namespace, so canonicalizing paths
614 // would have to be done carefully. For now, don't do anything.
615 return name;
616 }
617 char* abs_path;
618 if (dest == NULL) {
619 dest = DartUtils::ScopedCString(PATH_MAX + 1);
620 } else {
621 ASSERT(dest_size >= PATH_MAX);
622 }
623 ASSERT(dest != NULL);
624 do {
625 abs_path = realpath(name, dest);
626 } while ((abs_path == NULL) && (errno == EINTR));
627 ASSERT(abs_path == NULL || IsAbsolutePath(abs_path));
628 ASSERT(abs_path == NULL || (abs_path == dest));
629 return abs_path;
630}
631
632const char* File::PathSeparator() {
633 return "/";
634}
635
636const char* File::StringEscapedPathSeparator() {
637 return "/";
638}
639
640static int fd_is_valid(int fd) {
641 return NO_RETRY_EXPECTED(fcntl(fd, F_GETFD)) != -1 || errno != EBADF;
642}
643
644File::StdioHandleType File::GetStdioHandleType(int fd) {
645 struct stat buf;
646 int result = TEMP_FAILURE_RETRY(fstat(fd, &buf));
647 if (result == -1) {
648 // fstat() on fds 0, 1, 2 on Fuchsia return -1 with errno ENOTSUP,
649 // but if they are opened, then we can read/write them, so pretend they
650 // are kPipe.
651 return ((errno == ENOTSUP) && fd_is_valid(fd)) ? kPipe : kTypeError;
652 }
653 if (S_ISCHR(buf.st_mode)) {
654 return kTerminal;
655 }
656 if (S_ISFIFO(buf.st_mode)) {
657 return kPipe;
658 }
659 if (S_ISSOCK(buf.st_mode)) {
660 return kSocket;
661 }
662 if (S_ISREG(buf.st_mode)) {
663 return kFile;
664 }
665 return kOther;
666}
667
668File::Identical File::AreIdentical(Namespace* namespc_1,
669 const char* file_1,
670 Namespace* namespc_2,
671 const char* file_2) {
672 struct stat file_1_info;
673 struct stat file_2_info;
674 int status;
675 {
676 NamespaceScope ns1(namespc_1, file_1);
677 status = TEMP_FAILURE_RETRY(
678 fstatat(ns1.fd(), ns1.path(), &file_1_info, AT_SYMLINK_NOFOLLOW));
679 if (status == -1) {
680 return File::kError;
681 }
682 }
683 {
684 NamespaceScope ns2(namespc_2, file_2);
685 status = TEMP_FAILURE_RETRY(
686 fstatat(ns2.fd(), ns2.path(), &file_2_info, AT_SYMLINK_NOFOLLOW));
687 if (status == -1) {
688 return File::kError;
689 }
690 }
691 return ((file_1_info.st_ino == file_2_info.st_ino) &&
692 (file_1_info.st_dev == file_2_info.st_dev))
693 ? File::kIdentical
694 : File::kDifferent;
695}
696
697} // namespace bin
698} // namespace dart
699
700#endif // defined(HOST_OS_FUCHSIA)
701