|
266 | 266 | (let ((s (npath-string-of x))) (project-relative (if (string=? s "") "." s))))) |
267 | 267 | (define (->path x) (if (nio-path? x) x (make-nio-path (npath-string-of x)))) |
268 | 268 |
|
| 269 | +;; ---- error reporting: java.nio.file's exception family, not java.io's ------- |
| 270 | +;; Chez's filesystem primitives raise &i/o-filename conditions. Left to escape, |
| 271 | +;; they reach jolt through host-faults' generic fallback, which names them with |
| 272 | +;; java.io classes -- FileNotFoundException for a missing path -- and renders the |
| 273 | +;; Chez primitive's own message. java.nio.file.Files answers a different family, |
| 274 | +;; per UnixException.translateToIOException, so every Files entry point that |
| 275 | +;; touches the filesystem translates its own failure here. |
| 276 | +;; |
| 277 | +;; Two facts about the raise shape this: |
| 278 | +;; |
| 279 | +;; - Only open-file-input-port / open-file-output-port attach the R6RS |
| 280 | +;; subconditions (&i/o-file-already-exists, &i/o-file-does-not-exist, |
| 281 | +;; &i/o-file-protection). mkdir, rename-file and directory-list raise a bare |
| 282 | +;; &i/o-filename whose only clue to the errno is the strerror text sitting in |
| 283 | +;; the irritants. |
| 284 | +;; - Reading a class off that text would tie it to libc's wording, and to the |
| 285 | +;; locale the process happens to run under. |
| 286 | +;; |
| 287 | +;; So the entry points needing a class Chez does not type stat first and name the |
| 288 | +;; error themselves. That is a race for the error's NAME only, never for |
| 289 | +;; correctness: no pre-check here gates a mutation. createFile -- the one place |
| 290 | +;; where losing the race would cost data -- takes the O_EXCL open instead, which |
| 291 | +;; does raise a typed condition, and stats nothing. |
| 292 | +(define (nio-fs-throw cls fp) (jolt-throw (jolt-host-throwable cls fp))) |
| 293 | +(define (nio-no-such-file fp) (nio-fs-throw "java.nio.file.NoSuchFileException" fp)) |
| 294 | +(define (nio-already-exists fp) (nio-fs-throw "java.nio.file.FileAlreadyExistsException" fp)) |
| 295 | +;; "<path>: <reason>" is the JDK's rendering for every errno without a class. |
| 296 | +(define (nio-fs-detail fp reason) |
| 297 | + (nio-fs-throw "java.nio.file.FileSystemException" |
| 298 | + (if reason (string-append fp ": " reason) fp))) |
| 299 | + |
| 300 | +;; The strerror text is the LAST string irritant: an open raises (path reason), |
| 301 | +;; rename-file raises (src dst reason). Any other shape degrades to a bare path |
| 302 | +;; rather than promoting some other irritant into the reason slot. |
| 303 | +(define (nio-fs-error-reason e fp) |
| 304 | + (and (irritants-condition? e) |
| 305 | + (let loop ((xs (condition-irritants e)) (last #f)) |
| 306 | + (cond ((null? xs) (and (string? last) (not (string=? last fp)) last)) |
| 307 | + ((string? (car xs)) (loop (cdr xs) (car xs))) |
| 308 | + (else (loop (cdr xs) last)))))) |
| 309 | + |
| 310 | +;; Run a Chez filesystem primitive, translating whatever it raises. |
| 311 | +(define (nio-fs-call fp thunk) |
| 312 | + (guard (e |
| 313 | + ((i/o-file-already-exists-error? e) (nio-already-exists fp)) |
| 314 | + ((i/o-file-does-not-exist-error? e) (nio-no-such-file fp)) |
| 315 | + ((i/o-file-protection-error? e) |
| 316 | + (nio-fs-throw "java.nio.file.AccessDeniedException" fp)) |
| 317 | + ((i/o-filename-error? e) (nio-fs-detail fp (nio-fs-error-reason e fp))) |
| 318 | + (else (raise e))) |
| 319 | + (thunk))) |
| 320 | + |
| 321 | +;; A directory opens for reading on Linux and only fails at the first read, so |
| 322 | +;; newInputStream handed back a stream that threw later. The JVM checks at open |
| 323 | +;; and reports exactly this message. |
| 324 | +(define (nio-open-input-port fp) |
| 325 | + (when (file-directory? fp) (nio-fs-detail fp "Is a directory")) |
| 326 | + (nio-fs-call fp (lambda () (open-file-input-port fp)))) |
| 327 | + |
269 | 328 | (define (nio-size fp) |
270 | | - (if (or (not (file-exists? fp)) (file-directory? fp)) 0 |
271 | | - (let ((port (open-file-input-port fp))) |
272 | | - (let ((n (file-length port))) (close-port port) n)))) |
| 329 | + (cond ((not (or (file-exists? fp) (nio-is-symlink? fp))) (nio-no-such-file fp)) |
| 330 | + ((file-directory? fp) 0) |
| 331 | + (else (let ((port (nio-open-input-port fp))) |
| 332 | + (let ((n (file-length port))) (close-port port) n))))) |
273 | 333 |
|
274 | 334 | (define (nio-read-bv fp) |
275 | 335 | (io-note-file-read! fp) ; a compile-time read belongs in the AOT key (io.ss) |
276 | | - (let ((port (open-file-input-port fp))) |
| 336 | + (let ((port (nio-open-input-port fp))) |
277 | 337 | (let ((bv (get-bytevector-all port))) |
278 | 338 | (close-port port) |
279 | 339 | (if (eof-object? bv) (make-bytevector 0) bv)))) |
|
312 | 372 | (define (nio-delete1 fp missing-ok?) |
313 | 373 | (cond ((nio-is-symlink? fp) (delete-file fp) #t) ; the link itself, even if dangling |
314 | 374 | ((not (file-exists? fp)) |
315 | | - (if missing-ok? #f (jolt-throw (jolt-ex-info fp empty-pmap)))) |
| 375 | + (if missing-ok? #f (nio-no-such-file fp))) |
316 | 376 | ((file-directory? fp) (if (delete-directory fp) #t |
317 | 377 | (jolt-throw (jolt-host-throwable "java.nio.file.DirectoryNotEmptyException" |
318 | 378 | (npath-string-of fp))))) |
|
357 | 417 | (cons "readAllLines" (lambda (p . _) (nio-read-lines (nfp p)))) |
358 | 418 | (cons "newInputStream"(lambda (p . _) (let ((fp (nfp p))) |
359 | 419 | (io-note-file-read! fp) |
360 | | - (make-in-stream (open-file-input-port fp))))) |
| 420 | + (make-in-stream (nio-open-input-port fp))))) |
361 | 421 | (cons "createTempFile" (lambda args (nio-files-create-temp args #f))) |
362 | 422 | (cons "createTempDirectory" (lambda args (nio-files-create-temp args #t)))))) |
363 | 423 | (set! files-accum (append files-accum files-statics))) |
|
457 | 517 | (define (nio-new-directory-stream dir . rest) |
458 | 518 | (let* ((base (npath-string-of dir)) |
459 | 519 | (fp (project-relative base)) |
460 | | - (names (sort string<? (directory-list fp))) |
| 520 | + (_ (cond ((not (file-exists? fp)) (nio-no-such-file fp)) |
| 521 | + ((not (file-directory? fp)) |
| 522 | + (nio-fs-throw "java.nio.file.NotDirectoryException" fp)))) |
| 523 | + (names (sort string<? (nio-fs-call fp (lambda () (directory-list fp))))) |
461 | 524 | (arg (and (pair? rest) (car rest))) |
462 | 525 | (paths (map (lambda (nm) (make-nio-path (nio-path-join base nm))) names))) |
463 | 526 | (make-dir-stream |
|
637 | 700 | (->path link))) |
638 | 701 | (cons "createLink" (lambda (link existing . _) |
639 | 702 | (when c-link (c-link (nfp existing) (nfp link))) (->path link))) |
640 | | - (cons "readSymbolicLink" (lambda (p) (let ((t (nio-readlink (nfp p)))) |
641 | | - (if t (make-nio-path t) |
642 | | - (jolt-throw (jolt-ex-info (npath-string-of p) empty-pmap)))))) |
| 703 | + (cons "readSymbolicLink" (lambda (p) (let* ((fp (nfp p)) (t (nio-readlink fp))) |
| 704 | + (cond (t (make-nio-path t)) |
| 705 | + ((not (file-exists? fp)) (nio-no-such-file fp)) |
| 706 | + (else (nio-fs-throw "java.nio.file.NotLinkException" fp)))))) |
643 | 707 | (cons "setPosixFilePermissions" (lambda (p perms . _) |
644 | 708 | (when c-chmod (c-chmod (nfp p) (posix-set->mode perms))) (->path p)))))) |
645 | 709 | (set! files-accum (append files-accum files-attr))) |
|
714 | 778 | (truncate? (file-options no-create no-fail)) |
715 | 779 | (else (file-options no-create no-fail no-truncate)))))) |
716 | 780 |
|
717 | | -;; A failed open raises a Chez &i/o-filename condition whose second irritant is |
718 | | -;; the errno's strerror text; the JDK renders that text after the path for the |
719 | | -;; errnos it has no dedicated class for. Match on the string rather than the |
720 | | -;; position so an unexpected irritant list degrades to the bare path. |
721 | | -(define (nio-open-error-reason e fp) |
722 | | - (and (irritants-condition? e) |
723 | | - (let loop ((xs (condition-irritants e))) |
724 | | - (cond ((null? xs) #f) |
725 | | - ((and (string? (car xs)) (not (string=? (car xs) fp))) (car xs)) |
726 | | - (else (loop (cdr xs))))))) |
727 | | - |
728 | | -;; UnixException.translateToIOException is the contract here: ENOENT, EEXIST and |
729 | | -;; EACCES each get a class and carry only the path as their message, and every |
730 | | -;; other errno -- EISDIR, ELOOP, ENOTDIR, ENOSPC -- arrives as a plain |
731 | | -;; FileSystemException reading "<path>: <reason>". The last arm used to re-raise, |
732 | | -;; which let the Chez condition escape to be rendered as a bare java.io. |
733 | | -;; IOException whose message named open-file-output-port. |
734 | 781 | (define (nio-open-output-port fp options) |
735 | | - (define (throw-nio cls msg) (jolt-throw (jolt-host-throwable cls msg))) |
736 | | - (guard (e |
737 | | - ((i/o-file-already-exists-error? e) |
738 | | - (throw-nio "java.nio.file.FileAlreadyExistsException" fp)) |
739 | | - ((i/o-file-does-not-exist-error? e) |
740 | | - (throw-nio "java.nio.file.NoSuchFileException" fp)) |
741 | | - ((i/o-file-protection-error? e) |
742 | | - (throw-nio "java.nio.file.AccessDeniedException" fp)) |
743 | | - ((i/o-filename-error? e) |
744 | | - (let ((reason (nio-open-error-reason e fp))) |
745 | | - (throw-nio "java.nio.file.FileSystemException" |
746 | | - (if reason (string-append fp ": " reason) fp)))) |
747 | | - (else (raise e))) |
748 | | - (open-file-output-port fp options))) |
| 782 | + (nio-fs-call fp (lambda () (open-file-output-port fp options)))) |
749 | 783 |
|
750 | 784 | (let ((files-opt |
751 | 785 | (list (cons "write" (lambda (p data . opts) |
|
956 | 990 | (define (nio-parent-of fp) |
957 | 991 | (let loop ((i (- (string-length fp) 1))) |
958 | 992 | (cond ((< i 0) "") ((char=? (string-ref fp i) #\/) (substring fp 0 i)) (else (loop (- i 1)))))) |
| 993 | +(define (nio-blocking-ancestor fp) ; nearest existing ancestor that is not a directory |
| 994 | + (let loop ((p (nio-parent-of fp))) |
| 995 | + (cond ((or (string=? p "") (string=? p "/")) #f) |
| 996 | + ((file-exists? p) (and (not (file-directory? p)) p)) |
| 997 | + (else (loop (nio-parent-of p)))))) |
959 | 998 | (define (nio-missing-ancestors fp) ; the not-yet-existing path chain, shallowest first |
960 | 999 | (let loop ((p fp) (acc '())) |
961 | 1000 | (cond ((or (string=? p "") (string=? p "/") (file-exists? p)) acc) |
|
964 | 1003 | (define (nio-dest-present? d) (or (file-exists? d) (nio-is-symlink? d))) |
965 | 1004 | (let ((files-create+move |
966 | 1005 | (list |
967 | | - (cons "createDirectory" (lambda (p . attrs) (mkdir (nfp p)) (nio-apply-attrs-umask! (nfp p) attrs) (->path p))) |
| 1006 | + (cons "createDirectory" (lambda (p . attrs) |
| 1007 | + (let ((fp (nfp p))) |
| 1008 | + ;; mkdir's EEXIST and ENOENT come back untyped, so name them |
| 1009 | + ;; here. A non-directory in the way is neither: that is |
| 1010 | + ;; ENOTDIR, which nio-fs-call renders as a FileSystemException |
| 1011 | + ;; exactly as the JVM does. |
| 1012 | + (when (nio-dest-present? fp) (nio-already-exists fp)) |
| 1013 | + (let ((parent (nio-parent-of fp))) |
| 1014 | + (when (and (not (string=? parent "")) (not (file-exists? parent))) |
| 1015 | + (nio-no-such-file fp))) |
| 1016 | + (nio-fs-call fp (lambda () (mkdir fp))) |
| 1017 | + (nio-apply-attrs-umask! fp attrs) (->path p)))) |
| 1018 | + ;; CREATE_NEW's open, for the same reason Files/newOutputStream takes it: |
| 1019 | + ;; `no-fail` here made createFile TRUNCATE an existing file and return it. |
968 | 1020 | (cons "createFile" (lambda (p . attrs) |
969 | | - (close-port (open-file-output-port (nfp p) (file-options no-fail))) |
970 | | - (nio-apply-attrs-umask! (nfp p) attrs) (->path p))) |
| 1021 | + (let ((fp (nfp p))) |
| 1022 | + (close-port (nio-fs-call fp (lambda () (open-file-output-port fp (file-options))))) |
| 1023 | + (nio-apply-attrs-umask! fp attrs) (->path p)))) |
971 | 1024 | (cons "createDirectories" (lambda (p . attrs) |
972 | | - (let ((missing (nio-missing-ancestors (nfp p)))) |
973 | | - (mkdirs! (nfp p)) |
974 | | - (for-each (lambda (d) (nio-apply-attrs-umask! d attrs)) missing)) |
| 1025 | + (let ((fp (nfp p))) |
| 1026 | + ;; an existing directory is a no-op; anything else in the |
| 1027 | + ;; way -- at the target or above it -- is the JVM's |
| 1028 | + ;; FileAlreadyExistsException, named for what blocks |
| 1029 | + (when (and (file-exists? fp) (not (file-directory? fp))) |
| 1030 | + (nio-already-exists fp)) |
| 1031 | + (let ((blocked (nio-blocking-ancestor fp))) |
| 1032 | + (when blocked (nio-already-exists blocked))) |
| 1033 | + (let ((missing (nio-missing-ancestors fp))) |
| 1034 | + (nio-fs-call fp (lambda () (mkdirs! fp))) |
| 1035 | + (for-each (lambda (d) (nio-apply-attrs-umask! d attrs)) missing))) |
975 | 1036 | (->path p))) |
976 | 1037 | (cons "move" (lambda (src dst . opts) |
977 | 1038 | (let ((s (nfp src)) (d (nfp dst))) |
978 | 1039 | (cond |
979 | 1040 | ((string=? s d) (->path dst)) |
| 1041 | + ((not (nio-dest-present? s)) (nio-no-such-file s)) |
980 | 1042 | ((and (nio-dest-present? d) (not (nio-opts-have? opts copt-sym 'replace-existing))) |
981 | | - (jolt-throw (jolt-ex-info (string-append d " already exists") empty-pmap))) |
| 1043 | + (nio-already-exists d)) |
982 | 1044 | (else (when (nio-dest-present? d) (nio-delete1 d #t)) |
983 | | - (rename-file s d) (->path dst))))))))) |
| 1045 | + (nio-fs-call s (lambda () (rename-file s d))) (->path dst))))))))) |
984 | 1046 | (set! files-accum (append files-accum files-create+move))) |
985 | 1047 |
|
986 | 1048 | ;; ---- nofollow timestamps (the link's own mtime, via lstat/lutimes) ---------- |
|
1005 | 1067 | (file-mtime-millis fp))) |
1006 | 1068 | (let ((files-nofollow-time |
1007 | 1069 | (list |
1008 | | - (cons "getLastModifiedTime" (lambda (p . opts) (make-file-time (nio-lmtime-millis (nfp p) opts)))) |
| 1070 | + (cons "getLastModifiedTime" (lambda (p . opts) |
| 1071 | + (let ((fp (nfp p))) |
| 1072 | + (unless (or (file-exists? fp) (nio-is-symlink? fp)) (nio-no-such-file fp)) |
| 1073 | + (make-file-time (nio-lmtime-millis fp opts))))) |
1009 | 1074 | (cons "getAttribute" (lambda (path attr . opts) |
1010 | 1075 | (let ((fp (nfp path)) (nm (nio-attr-name (npath-string-of attr)))) |
1011 | 1076 | (if (member nm '("lastModifiedTime" "creationTime" "lastAccessTime")) |
|
1062 | 1127 | (let ((s (nfp src)) (d (nfp dst))) |
1063 | 1128 | (cond |
1064 | 1129 | ((string=? s d) (->path dst)) |
| 1130 | + ((not (nio-dest-present? s)) (nio-no-such-file s)) |
1065 | 1131 | ((and (nio-dest-present? d) (not (nio-opts-have? opts copt-sym 'replace-existing))) |
1066 | | - (jolt-throw (jolt-ex-info (string-append d " already exists") empty-pmap))) |
| 1132 | + (nio-already-exists d)) |
1067 | 1133 | (else |
1068 | 1134 | (when (nio-dest-present? d) (nio-delete1 d #t)) |
1069 | 1135 | (cond |
|
0 commit comments