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