-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathCustomFileChannel.java
More file actions
384 lines (352 loc) · 13.4 KB
/
CustomFileChannel.java
File metadata and controls
384 lines (352 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
package org.perlonjava.runtime.io;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Set;
import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable;
import static org.perlonjava.runtime.runtimetypes.RuntimeIO.handleIOException;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.getScalarInt;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue;
/**
* A custom file channel implementation that provides Perl-compatible I/O operations.
*
* <p>This class wraps Java's {@link FileChannel} to provide an implementation of
* {@link IOHandle} that supports file-based I/O operations. It handles character
* encoding/decoding, EOF detection, and provides Perl-style return values for
* all operations.
*
* <p>Key features:
* <ul>
* <li>Supports both file path and file descriptor based construction</li>
* <li>Handles multi-byte character sequences correctly across read boundaries</li>
* <li>Tracks EOF state for Perl-compatible EOF detection</li>
* <li>Provides atomic position-based operations (tell, seek)</li>
* <li>Supports file truncation</li>
* </ul>
*
* <p>Example usage:
* <pre>
* // Open a file for reading
* Set<StandardOpenOption> options = Set.of(StandardOpenOption.READ);
* CustomFileChannel channel = new CustomFileChannel(Paths.get("file.txt"), options);
*
* // Read data
* RuntimeScalar data = channel.read(1024, StandardCharsets.UTF_8);
*
* // Check EOF
* if (channel.eof().getBoolean()) {
* // End of file reached
* }
* </pre>
*
* @see IOHandle
* @see FileChannel
*/
public class CustomFileChannel implements IOHandle {
/**
* The underlying Java NIO FileChannel for actual I/O operations
*/
private final FileChannel fileChannel;
/**
* Tracks whether end-of-file has been reached during reading
*/
private boolean isEOF;
// When true, writes should always occur at end-of-file (Perl's append semantics).
private boolean appendMode;
/**
* Helper for handling multi-byte character decoding across read boundaries
*/
private CharsetDecoderHelper decoderHelper;
/**
* Creates a new CustomFileChannel for the specified file path.
*
* @param path the path to the file to open
* @param options the options specifying how the file is opened (READ, WRITE, etc.)
* @throws IOException if an I/O error occurs opening the file
*/
public CustomFileChannel(Path path, Set<StandardOpenOption> options) throws IOException {
this.fileChannel = FileChannel.open(path, options);
this.isEOF = false;
this.appendMode = false;
}
/**
* Creates a new CustomFileChannel from an existing file descriptor.
*
* <p>This constructor is useful for wrapping standard I/O streams (stdin, stdout, stderr)
* or file descriptors obtained from native code.
*
* @param fd the file descriptor to wrap
* @param options the options specifying the mode (must contain either READ or WRITE)
* @throws IOException if an I/O error occurs
* @throws IllegalArgumentException if options don't contain READ or WRITE
*/
public CustomFileChannel(FileDescriptor fd, Set<StandardOpenOption> options) throws IOException {
if (options.contains(StandardOpenOption.READ)) {
// Create a read channel from the file descriptor
this.fileChannel = new FileInputStream(fd).getChannel();
} else if (options.contains(StandardOpenOption.WRITE)) {
// Create a write channel from the file descriptor
this.fileChannel = new FileOutputStream(fd).getChannel();
} else {
throw new IllegalArgumentException("Invalid options for FileDescriptor");
}
this.isEOF = false;
this.appendMode = false;
}
public void setAppendMode(boolean appendMode) {
this.appendMode = appendMode;
}
/**
* Reads data from the file with proper character encoding support.
*
* <p>This method handles multi-byte character sequences correctly, buffering
* incomplete sequences until enough data is available to decode them properly.
* This is crucial for UTF-8 and other variable-length encodings.
*
* @param maxBytes the maximum number of bytes to read
* @param charset the character encoding to use for decoding
* @return RuntimeScalar containing the decoded string data
*/
@Override
public RuntimeScalar doRead(int maxBytes, Charset charset) {
try {
byte[] buffer = new byte[maxBytes];
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
int bytesRead = fileChannel.read(byteBuffer);
if (bytesRead == -1) {
isEOF = true;
return new RuntimeScalar("");
}
// Check if we've reached EOF (read less than requested)
if (bytesRead < maxBytes) {
isEOF = true;
}
// Also treat "at end of file" as EOF for Perl semantics (eof true after last successful read)
try {
if (fileChannel.position() >= fileChannel.size()) {
isEOF = true;
}
} catch (IOException e) {
// ignore
}
// Convert bytes to string where each char represents a byte
StringBuilder result = new StringBuilder(bytesRead);
for (int i = 0; i < bytesRead; i++) {
result.append((char) (buffer[i] & 0xFF));
}
return new RuntimeScalar(result.toString());
} catch (IOException e) {
return handleIOException(e, "Read operation failed");
}
}
/**
* Writes a string to the file.
*
* <p>The string is converted to bytes using ISO-8859-1 encoding, which
* preserves byte values for binary data. This allows the method to handle
* both text and binary data correctly.
*
* @param string the string data to write
* @return RuntimeScalar containing the number of bytes written
*/
@Override
public RuntimeScalar write(String string) {
try {
if (appendMode) {
fileChannel.position(fileChannel.size());
}
byte[] data = new byte[string.length()];
for (int i = 0; i < string.length(); i++) {
data[i] = (byte) string.charAt(i);
}
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
int bytesWritten = fileChannel.write(byteBuffer);
return new RuntimeScalar(bytesWritten);
} catch (IOException e) {
return handleIOException(e, "write failed");
}
}
/**
* Closes the file channel and releases any system resources.
*
* @return RuntimeScalar with true value on success
*/
@Override
public RuntimeScalar close() {
try {
// Ensure all data is flushed before closing
fileChannel.force(true); // Force both content and metadata
fileChannel.close();
return scalarTrue;
} catch (IOException e) {
return handleIOException(e, "close failed");
}
}
/**
* Checks if end-of-file has been reached.
*
* <p>The EOF flag is set when a read operation returns -1 (no more data).
*
* @return RuntimeScalar with true if EOF reached, false otherwise
*/
@Override
public RuntimeScalar eof() {
return new RuntimeScalar(isEOF);
}
/**
* Gets the current position in the file.
*
* @return RuntimeScalar containing the current byte position, or -1 on error
*/
@Override
public RuntimeScalar tell() {
try {
return getScalarInt(fileChannel.position());
} catch (IOException e) {
handleIOException(e, "tell failed");
return getScalarInt(-1);
}
}
/**
* Seeks to a new position in the file based on the whence parameter.
*
* <p>The whence parameter determines how the position is calculated:
* <ul>
* <li>SEEK_SET (0): Set position to pos bytes from the beginning of the file</li>
* <li>SEEK_CUR (1): Set position to current position + pos bytes</li>
* <li>SEEK_END (2): Set position to end of file + pos bytes</li>
* </ul>
*
* <p>Seeking clears the EOF flag since we may no longer be at the end of file.
*
* @param pos the offset in bytes
* @param whence the reference point for the offset (SEEK_SET, SEEK_CUR, or SEEK_END)
* @return RuntimeScalar with true on success, false on failure
*/
@Override
public RuntimeScalar seek(long pos, int whence) {
try {
long newPosition;
switch (whence) {
case SEEK_SET: // from beginning
newPosition = pos;
break;
case SEEK_CUR: // from current position
newPosition = fileChannel.position() + pos;
break;
case SEEK_END: // from end of file
newPosition = fileChannel.size() + pos;
break;
default:
return handleIOException(new IOException("Invalid whence value: " + whence), "seek failed");
}
// Ensure the new position is not negative
if (newPosition < 0) {
return handleIOException(new IOException("Negative seek position"), "seek failed");
}
fileChannel.position(newPosition);
// Perl semantics: seeking to EOF sets eof flag, seeking elsewhere clears it.
try {
isEOF = (fileChannel.position() >= fileChannel.size());
} catch (IOException e) {
isEOF = false;
}
return scalarTrue;
} catch (IOException e) {
return handleIOException(e, "seek failed");
}
}
/**
* Flushes any buffered data to the underlying storage device.
*
* <p>This method forces any buffered data to be written to the storage device,
* including file metadata for reliability.
*
* @return RuntimeScalar with true on success
*/
@Override
public RuntimeScalar flush() {
try {
// Force both content and metadata to be written for reliability
fileChannel.force(true);
return scalarTrue;
} catch (IOException e) {
return handleIOException(e, "flush failed");
}
}
/**
* Gets the file descriptor number for this channel.
*
* <p>Note: FileChannel does not expose the underlying file descriptor in Java,
* so this method returns undef. This is a limitation of the Java API.
*
* @return RuntimeScalar with undef value
*/
@Override
public RuntimeScalar fileno() {
return RuntimeScalarCache.scalarUndef; // FileChannel does not expose a file descriptor
}
/**
* Truncates the file to the specified length.
*
* <p>If the file is currently larger than the specified length, the extra data
* is discarded. If the file is smaller, it is extended with null bytes.
*
* @param length the desired length of the file in bytes
* @return RuntimeScalar with true on success
* @throws IllegalArgumentException if length is negative
*/
public RuntimeScalar truncate(long length) {
try {
if (length < 0) {
throw new IllegalArgumentException("Invalid arguments for truncate operation.");
}
fileChannel.truncate(length);
return scalarTrue;
} catch (IOException e) {
return handleIOException(e, "truncate failed");
}
}
@Override
public RuntimeScalar sysread(int length) {
try {
ByteBuffer buffer = ByteBuffer.allocate(length);
int bytesRead = fileChannel.read(buffer); // Changed from 'channel' to 'fileChannel'
if (bytesRead == -1) {
// EOF - return empty string
return new RuntimeScalar("");
}
buffer.flip();
byte[] readBytes = new byte[buffer.remaining()];
buffer.get(readBytes);
return new RuntimeScalar(readBytes);
} catch (IOException e) {
getGlobalVariable("main::!").set(e.getMessage());
return new RuntimeScalar(); // undef
}
}
@Override
public RuntimeScalar syswrite(String data) {
try {
// Convert string to bytes (each char is a byte 0-255)
ByteBuffer buffer = ByteBuffer.allocate(data.length());
for (int i = 0; i < data.length(); i++) {
buffer.put((byte) (data.charAt(i) & 0xFF));
}
buffer.flip();
int bytesWritten = fileChannel.write(buffer); // Changed from 'channel' to 'fileChannel'
return new RuntimeScalar(bytesWritten);
} catch (IOException e) {
getGlobalVariable("main::!").set(e.getMessage());
return new RuntimeScalar(); // undef
}
}
}