1111package org .eclipse .rdf4j .common .io ;
1212
1313import java .io .Closeable ;
14+ import java .io .EOFException ;
1415import java .io .File ;
1516import java .io .IOException ;
1617import java .nio .ByteBuffer ;
2223import java .nio .file .StandardOpenOption ;
2324import java .util .EnumSet ;
2425import java .util .Set ;
26+ import java .util .concurrent .TimeUnit ;
2527
2628/**
2729 * File wrapper that protects against concurrent file closing events due to e.g. {@link Thread#interrupt() thread
@@ -45,6 +47,13 @@ public final class NioFile implements Closeable {
4547
4648 private volatile FileChannel fc ;
4749
50+ /**
51+ * Disable strict guards via system property to maintain legacy behavior without additional exceptions. Property:
52+ * org.eclipse.rdf4j.common.io.niofile.disableStrictGuards (default: false)
53+ */
54+ private static final boolean STRICT_GUARDS = !Boolean
55+ .getBoolean ("org.eclipse.rdf4j.common.io.niofile.disableStrictGuards" );
56+
4857 private volatile boolean explictlyClosed ;
4958
5059 /**
@@ -238,12 +247,24 @@ public long transferTo(long position, long count, WritableByteChannel target) th
238247 * @throws IOException
239248 */
240249 public int write (ByteBuffer buf , long offset ) throws IOException {
250+ final int startPosition = buf .position ();
241251 while (true ) {
242252 try {
243- return fc .write (buf , offset );
253+ // Ensure the entire buffer is written, even if the underlying channel performs a partial write
254+ while (buf .hasRemaining ()) {
255+ long position = offset + (buf .position () - startPosition );
256+ int n = fc .write (buf , position );
257+ if (n == 0 ) {
258+ // Avoid tight spin in pathological cases: reattempt write
259+ // FileChannel positional writes may occasionally return 0 without progress
260+ continue ;
261+ }
262+ }
263+ return buf .position () - startPosition ;
244264 } catch (ClosedByInterruptException e ) {
245265 throw e ;
246266 } catch (ClosedChannelException e ) {
267+ // Preserve already-consumed bytes on retry
247268 reopen (e );
248269 }
249270 }
@@ -258,12 +279,29 @@ public int write(ByteBuffer buf, long offset) throws IOException {
258279 * @throws IOException
259280 */
260281 public int read (ByteBuffer buf , long offset ) throws IOException {
282+ final int startPosition = buf .position ();
261283 while (true ) {
262284 try {
263- return fc .read (buf , offset );
285+ while (buf .hasRemaining ()) {
286+ long position = offset + (buf .position () - startPosition );
287+ int n = fc .read (buf , position );
288+ if (n < 0 ) {
289+ // Preserve FileChannel contract: if no bytes were read and EOF is reached, return -1
290+ if (buf .position () == startPosition ) {
291+ return -1 ;
292+ }
293+ break ; // EOF after having read some bytes
294+ }
295+ if (n == 0 ) {
296+ // Avoid tight spin; allow retry in case of transient 0-byte read
297+ continue ;
298+ }
299+ }
300+ return buf .position () - startPosition ;
264301 } catch (ClosedByInterruptException e ) {
265302 throw e ;
266303 } catch (ClosedChannelException e ) {
304+ // Preserve already-consumed bytes on retry
267305 reopen (e );
268306 }
269307 }
@@ -277,7 +315,10 @@ public int read(ByteBuffer buf, long offset) throws IOException {
277315 * @throws IOException
278316 */
279317 public void writeBytes (byte [] value , long offset ) throws IOException {
280- write (ByteBuffer .wrap (value ), offset );
318+ int write = write (ByteBuffer .wrap (value ), offset );
319+ if (STRICT_GUARDS && write != value .length ) {
320+ throw new IOException ("Incomplete writeBytes: expected " + value .length + ", wrote " + write );
321+ }
281322 }
282323
283324 /**
@@ -290,7 +331,10 @@ public void writeBytes(byte[] value, long offset) throws IOException {
290331 */
291332 public byte [] readBytes (long offset , int length ) throws IOException {
292333 ByteBuffer buf = ByteBuffer .allocate (length );
293- read (buf , offset );
334+ int read = read (buf , offset );
335+ if (STRICT_GUARDS && read < length ) {
336+ throw new EOFException ("Unexpected EOF in readBytes: expected " + length + ", read " + read );
337+ }
294338 return buf .array ();
295339 }
296340
@@ -326,7 +370,10 @@ public byte readByte(long offset) throws IOException {
326370 public void writeLong (long value , long offset ) throws IOException {
327371 ByteBuffer buf = ByteBuffer .allocate (8 );
328372 buf .putLong (0 , value );
329- write (buf , offset );
373+ int write = write (buf , offset );
374+ if (STRICT_GUARDS && write != 8 ) {
375+ throw new IOException ("Incomplete writeLong: wrote " + write );
376+ }
330377 }
331378
332379 /**
@@ -338,7 +385,10 @@ public void writeLong(long value, long offset) throws IOException {
338385 */
339386 public long readLong (long offset ) throws IOException {
340387 ByteBuffer buf = ByteBuffer .allocate (8 );
341- read (buf , offset );
388+ int read = read (buf , offset );
389+ if (STRICT_GUARDS && read < 8 ) {
390+ throw new EOFException ("Unexpected EOF in readLong: read " + read );
391+ }
342392 return buf .getLong (0 );
343393 }
344394
@@ -352,7 +402,10 @@ public long readLong(long offset) throws IOException {
352402 public void writeInt (int value , long offset ) throws IOException {
353403 ByteBuffer buf = ByteBuffer .allocate (4 );
354404 buf .putInt (0 , value );
355- write (buf , offset );
405+ int write = write (buf , offset );
406+ if (STRICT_GUARDS && write != 4 ) {
407+ throw new IOException ("Incomplete writeInt: wrote " + write );
408+ }
356409 }
357410
358411 /**
@@ -364,7 +417,10 @@ public void writeInt(int value, long offset) throws IOException {
364417 */
365418 public int readInt (long offset ) throws IOException {
366419 ByteBuffer buf = ByteBuffer .allocate (4 );
367- read (buf , offset );
420+ int read = read (buf , offset );
421+ if (STRICT_GUARDS && read < 4 ) {
422+ throw new EOFException ("Unexpected EOF in readInt: read " + read );
423+ }
368424 return buf .getInt (0 );
369425 }
370426}
0 commit comments