Skip to content

Commit 0859cc4

Browse files
author
Yogthos
committed
Report the JVM's errors across the rest of the Files surface
#899 translated the output-open path. Everywhere else in java.nio.file.Files a Chez condition still escaped to host-faults' generic fallback -- which names failures with java.io classes and renders the Chez primitive's own message -- or the operation answered without failing at all: createFile on an existing path TRUNCATED it and returned the path Files/size on a missing path 0 newInputStream on a directory a stream that threw on first read createDirectories blocked by a file reported success, created nothing readAllBytes/readAllLines missing java.io.FileNotFoundException createDirectory / move / dirstream bare java.io.IOException delete / readSymbolicLink clojure.lang.ExceptionInfo createFile is the serious one: the same data loss #895 reported for newOutputStream, reached through a different entry point. It now takes the same exclusive-create open and throws FileAlreadyExistsException. The translator #899 grew for opens is now shared as nio-fs-call, and every Files entry point that touches the filesystem routes through it. Two facts about Chez shaped the design: - Only open-file-input-port/open-file-output-port attach the R6RS subconditions. mkdir, rename-file and directory-list raise a bare &i/o-filename whose only clue to the errno is the strerror text in the irritants. - Reading a class off that text would tie it to libc's wording and to the process locale. So the entry points needing a class Chez does not type stat first and name the error themselves. That is a race for the error's NAME only, never for correctness -- no pre-check added here gates a mutation. createFile, the one place where losing the race would cost data, takes the O_EXCL open instead and stats nothing. Also fixed while in the area: nio-fs-error-reason takes the LAST string irritant rather than the first non-path one, because rename-file raises (src dst reason) and the old scan would have promoted the destination path into the reason slot. NotDirectoryException and NotLinkException join the hierarchy under FileSystemException. The four new rows catch only FileSystemException (or IOException), so a regression to a raw condition or an ex-info shows up as :wrong rather than escaping the row. unit gate: 1692/1692. corpus: 5174/5184, 0 new divergences, 0 crashes. cli smoke: 200 passed, 0 failed.
1 parent e033db1 commit 0859cc4

4 files changed

Lines changed: 154 additions & 51 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
120120

121121
### Fixed
122122

123+
- **`Files.createFile` no longer truncates an existing file.** It opened with
124+
create-or-truncate and returned the path, so calling it on a path that already
125+
held data silently emptied it — the same failure #895 reported for
126+
`Files.newOutputStream`, at a different entry point. It now takes the same
127+
exclusive-create open and throws `FileAlreadyExistsException`.
128+
129+
- **The rest of the `java.nio.file.Files` surface reports NIO exceptions.**
130+
Only the output-open path had been translated. Elsewhere a Chez condition
131+
escaped to jolt's generic fallback, which names failures with `java.io`
132+
classes, or the operation answered without failing at all:
133+
`Files.size` returned `0` for a missing path, `newInputStream` handed back a
134+
stream for a directory that only threw on first read, and `createDirectories`
135+
reported success when a plain file blocked the path. Reads, `size`,
136+
`createDirectory`/`createDirectories`, `move`, `copy`, `delete`,
137+
`newDirectoryStream`, `readSymbolicLink` and `getLastModifiedTime` now answer
138+
`NoSuchFileException`, `FileAlreadyExistsException`, `NotDirectoryException`,
139+
`NotLinkException`, `AccessDeniedException` or a `FileSystemException` reading
140+
`<path>: <reason>`, matching `UnixException.translateToIOException`.
141+
123142
- **A failed `Files.newOutputStream`/`Files.write` open reports the NIO class the
124143
JVM reports.** Only ENOENT and EEXIST were translated; every other errno let
125144
the underlying Chez condition escape, so opening a directory for output — or

‎host/chez/java/class-hierarchy.ss‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,8 @@
656656
(jch-register-supers! "java.nio.file.FileAlreadyExistsException" '("java.nio.file.FileSystemException"))
657657
(jch-register-supers! "java.nio.file.NoSuchFileException" '("java.nio.file.FileSystemException"))
658658
(jch-register-supers! "java.nio.file.AccessDeniedException" '("java.nio.file.FileSystemException"))
659+
(jch-register-supers! "java.nio.file.NotDirectoryException" '("java.nio.file.FileSystemException"))
660+
(jch-register-supers! "java.nio.file.NotLinkException" '("java.nio.file.FileSystemException"))
659661
(jch-register-supers! "java.nio.file.DirectoryNotEmptyException" '("java.nio.file.FileSystemException"))
660662
(jch-register-supers! "java.net.UnknownHostException" '("java.io.IOException"))
661663
(jch-register-supers! "java.net.SocketException" '("java.io.IOException"))

‎host/chez/java/nio-file.ss‎

Lines changed: 117 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -266,14 +266,74 @@
266266
(let ((s (npath-string-of x))) (project-relative (if (string=? s "") "." s)))))
267267
(define (->path x) (if (nio-path? x) x (make-nio-path (npath-string-of x))))
268268

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+
269328
(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)))))
273333

274334
(define (nio-read-bv fp)
275335
(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)))
277337
(let ((bv (get-bytevector-all port)))
278338
(close-port port)
279339
(if (eof-object? bv) (make-bytevector 0) bv))))
@@ -312,7 +372,7 @@
312372
(define (nio-delete1 fp missing-ok?)
313373
(cond ((nio-is-symlink? fp) (delete-file fp) #t) ; the link itself, even if dangling
314374
((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)))
316376
((file-directory? fp) (if (delete-directory fp) #t
317377
(jolt-throw (jolt-host-throwable "java.nio.file.DirectoryNotEmptyException"
318378
(npath-string-of fp)))))
@@ -357,7 +417,7 @@
357417
(cons "readAllLines" (lambda (p . _) (nio-read-lines (nfp p))))
358418
(cons "newInputStream"(lambda (p . _) (let ((fp (nfp p)))
359419
(io-note-file-read! fp)
360-
(make-in-stream (open-file-input-port fp)))))
420+
(make-in-stream (nio-open-input-port fp)))))
361421
(cons "createTempFile" (lambda args (nio-files-create-temp args #f)))
362422
(cons "createTempDirectory" (lambda args (nio-files-create-temp args #t))))))
363423
(set! files-accum (append files-accum files-statics)))
@@ -457,7 +517,10 @@
457517
(define (nio-new-directory-stream dir . rest)
458518
(let* ((base (npath-string-of dir))
459519
(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)))))
461524
(arg (and (pair? rest) (car rest)))
462525
(paths (map (lambda (nm) (make-nio-path (nio-path-join base nm))) names)))
463526
(make-dir-stream
@@ -637,9 +700,10 @@
637700
(->path link)))
638701
(cons "createLink" (lambda (link existing . _)
639702
(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))))))
643707
(cons "setPosixFilePermissions" (lambda (p perms . _)
644708
(when c-chmod (c-chmod (nfp p) (posix-set->mode perms))) (->path p))))))
645709
(set! files-accum (append files-accum files-attr)))
@@ -714,38 +778,8 @@
714778
(truncate? (file-options no-create no-fail))
715779
(else (file-options no-create no-fail no-truncate))))))
716780

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.
734781
(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))))
749783

750784
(let ((files-opt
751785
(list (cons "write" (lambda (p data . opts)
@@ -956,6 +990,11 @@
956990
(define (nio-parent-of fp)
957991
(let loop ((i (- (string-length fp) 1)))
958992
(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))))))
959998
(define (nio-missing-ancestors fp) ; the not-yet-existing path chain, shallowest first
960999
(let loop ((p fp) (acc '()))
9611000
(cond ((or (string=? p "") (string=? p "/") (file-exists? p)) acc)
@@ -964,23 +1003,46 @@
9641003
(define (nio-dest-present? d) (or (file-exists? d) (nio-is-symlink? d)))
9651004
(let ((files-create+move
9661005
(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.
9681020
(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))))
9711024
(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)))
9751036
(->path p)))
9761037
(cons "move" (lambda (src dst . opts)
9771038
(let ((s (nfp src)) (d (nfp dst)))
9781039
(cond
9791040
((string=? s d) (->path dst))
1041+
((not (nio-dest-present? s)) (nio-no-such-file s))
9801042
((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))
9821044
(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)))))))))
9841046
(set! files-accum (append files-accum files-create+move)))
9851047

9861048
;; ---- nofollow timestamps (the link's own mtime, via lstat/lutimes) ----------
@@ -1005,7 +1067,10 @@
10051067
(file-mtime-millis fp)))
10061068
(let ((files-nofollow-time
10071069
(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)))))
10091074
(cons "getAttribute" (lambda (path attr . opts)
10101075
(let ((fp (nfp path)) (nm (nio-attr-name (npath-string-of attr))))
10111076
(if (member nm '("lastModifiedTime" "creationTime" "lastAccessTime"))
@@ -1062,8 +1127,9 @@
10621127
(let ((s (nfp src)) (d (nfp dst)))
10631128
(cond
10641129
((string=? s d) (->path dst))
1130+
((not (nio-dest-present? s)) (nio-no-such-file s))
10651131
((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))
10671133
(else
10681134
(when (nio-dest-present? d) (nio-delete1 d #t))
10691135
(cond

0 commit comments

Comments
 (0)