The OpenD Programming Language

1 /**
2 I/O streams.
3 
4 Copyright: Copyright Guillaume Piolat 2022
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module gamut.io;
8 
9 import core.stdc.stdio;
10 import core.stdc.string: memcpy;
11 import core.stdc.stdlib: malloc, free, realloc;
12 public import core.stdc.config: c_long;
13 public import core.stdc.stdio: SEEK_SET, SEEK_CUR, SEEK_END;
14 
15 nothrow @nogc:
16 
17 
18 // Limits of I/O in gamut.
19 // Callbacks are modelled upon C stdlib functions, some of those use c_long or int. So, 32-bit is a possibility.
20 enum size_t GAMUT_MAX_POSSIBLE_MEMORY_OFFSET = 0x7fff_fffe;       /// Can't open file larger than this much bytes
21 enum size_t GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ = 0x7fff_ffff;   /// Can't read more bytes than this at once
22 enum size_t GAMUT_MAX_POSSIBLE_SIMULTANEOUS_WRITE = 0x7fff_ffff;  /// Can't write more bytes than this at once
23 
24 static assert(GAMUT_MAX_POSSIBLE_MEMORY_OFFSET + 1 <= cast(long) int.max);
25 
26 // Note: those function pointers made to be binary compatible with ftell/fseek/fwrite/fread/feof.
27 extern(C) @system
28 {
29     /// A function with same signature and semantics than `fread`.
30     ///
31     /// Some details from the Linux man pages:
32     ///
33     /// "On success, fread() and fwrite() return the number of items read
34     ///  or written.  This number equals the number of bytes transferred
35     ///  only when size is 1.  If an error occurs, or the end of the file
36     ///  is reached, the return value is a short item count (or zero).
37     ///
38     ///  The file position indicator for the stream is advanced by the
39     ///  number of bytes successfully read or written.
40     ///
41     ///  fread() does not distinguish between end-of-file and error, and
42     ///  callers must use feof() and ferror() to determine which
43     ///  occurred. (well, ferror not available in gamut).
44     ///
45     /// Params:
46     ///    buffer Where to read. Must be able to hold `size` * `count` bytes.
47     ///    size Size of elements to read in stream.
48     ///    count Number of elements to read in stream.
49     ///
50     /// Returns: 
51     ///    Number of item successfully read. If return value != `count`, there was an error.
52     ///
53     /// Limitations: it is forbidden to ask more than 0x7fffffff bytes at once.
54     alias ReadProc = size_t function(void* buffer, size_t size, size_t count, IOHandle handle);
55 
56     /// A function with same signature and semantics than `fwrite`.
57     alias WriteProc = size_t function(const(void)* buffer, size_t size, size_t count, IOHandle handle);
58 
59     /// A function with same signature and semantics than `fseek`.
60     /// Origin: position from which offset is added
61     ///   SEEK_SET = beginning of file.
62     ///   SEEK_CUR = Current position of file pointer.
63     ///   SEEK_END = end of file.
64     /// This function returns zero if successful, or else it returns a non-zero value.
65     /// Note: c_long offsets in gamut can always be cast to int without loss.
66     alias SeekProc = int function(IOHandle handle, c_long offset, int origin);
67 
68     /// A function with same signature and semantics than `ftell`.
69     /// Tells where we are in the file. -1 if error.
70     /// Note: c_long offsets in gamut can always be cast to int without loss.
71     alias TellProc = c_long function(IOHandle handle);
72 
73     /// A function with same signature and semantics than `feof`.
74     /// From Linux man:
75     /// "The function `feof()` tests the end-of-file indicator for the stream pointed to by stream, 
76     ///  returning nonzero if it is set."
77     alias EofProc = int function(IOHandle handle);
78 }
79 
80 
81 /// Can be a `FILE*` handle, a `FIMEMORY`, a `WrappedIO`...
82 /// identifies the I/O stream.
83 alias IOHandle = void*;
84 
85 /// I/O abstraction, to support load/write from a file, from memory, or from user-provided callbacks.
86 struct IOStream
87 {
88 nothrow @nogc @safe:
89 
90     /// A function with semantics and signature similar to `fread`.
91     ReadProc  read;
92 
93     /// A function with semantics and signature similar to `fwrite`.
94     WriteProc write;
95 
96     /// A function with semantics and signature similar to `fseek`.
97     SeekProc  seek;
98 
99     /// A function with semantics and signature similar to `ftell`.
100     TellProc  tell;
101 
102     /// A function with semantics and signature similar to `feof`.
103     EofProc   eof;
104 
105     /// Skip bytes.
106     /// Returns: true if it was possible to skip those bytes. false if there was an I/O error, or end of file.
107     /// Limitations: `nbytes` must be from 0 to 0x7fffffff
108     bool skipBytes(IOHandle handle, int nbytes) nothrow @nogc @trusted
109     {
110         assert(nbytes >= 0 && nbytes <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ);
111         return seek(handle, nbytes, SEEK_CUR) == 0;
112     }
113 
114     /// Seek to beginning of the I/O stream.
115     /// Returns: true if successful.
116     bool rewind(IOHandle handle) nothrow @nogc @trusted
117     {
118         return seek(handle, 0, SEEK_SET) == 0;
119     }
120 
121     /// Seek to asolute position in the I/O stream.
122     /// Useful because some function need to preserve it.
123     bool seekAbsolute(IOHandle handle, c_long offset) nothrow @nogc @trusted
124     {
125         return seek(handle, offset, SEEK_SET) == 0;
126     }
127 
128     /// Helper to read one ubyte in stream.
129     /// On error, sets `err` to `true` and return 0.
130     ubyte read_ubyte(IOHandle handle, bool* err) nothrow @nogc @trusted
131     {
132         ubyte v;
133         if (1 == read(&v, 1, 1, handle))
134         {
135             *err = false;
136             return v;
137         }
138         else
139         {
140             *err = true;
141             return 0;
142         }
143     }
144 
145     /// Helper to read one little-endian ushort in stream.
146     /// On error, sets `err` to `true` and return 0.
147     ushort read_ushort_LE(IOHandle handle, bool* err) nothrow @nogc @trusted
148     {
149         ushort v; // Note: no support for BigEndian here
150         if (2 == read(&v, 1, 2, handle))
151         {
152             *err = false;
153             return v;
154         }
155         else
156         {
157             *err = true;
158             return 0;
159         }
160     }
161 
162     /// Helper to read one little-endian uint in stream.
163     /// On error, sets `err` to `true` and return 0.
164     uint read_uint_LE(IOHandle handle, bool* err) nothrow @nogc @trusted
165     {
166         uint v; // Note: no support for BigEndian here
167         if (4 == read(&v, 1, 4, handle))
168         {
169             *err = false;
170             return v;
171         }
172         else
173         {
174             *err = true;
175             return 0;
176         }
177     }
178 
179 package:
180 
181     /// Setup the IOStream for reading a file. The passed `IOHandle` will need to be a `FILE*`.
182     /// For internal Gamut usage.
183     void setupForFileIO() pure @trusted
184     {
185         read  = cast(ReadProc) &fread;
186         write = cast(WriteProc) &fwrite;
187         seek  = cast(SeekProc) &fseek;
188         tell  = cast(TellProc) &ftell;
189         eof   = cast(EofProc) &feof;
190     }
191 
192     /// Setup the IOStream for using a  a file. The passed `IOHandle` will need to be a `MemoryFile*`.
193     /// For internal Gamut usage.
194     void setupForMemoryIO() pure @trusted
195     {
196         read  = cast(ReadProc)  &mread;
197         write = cast(WriteProc) &mwrite;
198         seek  = cast(SeekProc)  &mseek;
199         tell  = cast(TellProc)  &mtell;
200         eof   = cast(EofProc)   &meof;
201     }
202 
203     /// Setup the IOStream for wrapping another IOStream and logging what happens.
204     /// The passed `IOHandle` will need to be a `WrappedIO`.
205     /// For internal Gamut usage.
206     debug void setupSetupForLogging(ref IOStream io) pure @trusted
207     {
208         io.read  = &debug_fread;
209         io.write = &debug_fwrite;
210         io.seek  = &debug_fseek;
211         io.tell  = &debug_ftell;
212         io.eof   = &debug_feof;
213     }
214 }
215 
216 debug package struct WrappedIO
217 {
218     IOStream* wrapped; /// I/O object being wrapped.
219     IOHandle handle;   /// Original handle.
220 }
221 
222 package bool fileIsStartingWithSignature(IOStream *io, IOHandle handle, immutable ubyte[] signature)
223 {
224     assert(signature.length <= 16);
225 
226     // save I/O cursor
227     c_long offset = io.tell(handle);
228 
229     ubyte[16] header;
230     bool enoughBytes = (signature.length == io.read(header.ptr, 1, signature.length, handle));
231     bool match = enoughBytes && (signature == header[0..signature.length]);
232 
233     // restore I/O cursor
234     if (!io.seekAbsolute(handle, offset))
235         return false; // TODO: that rare error should propagate somehow
236 
237     return match;
238 }
239 
240 debug extern(C) @system private
241 {
242     // Note: these functions expect a `WrappedIO` to be passed as handle.
243     import core.stdc.stdio;
244 
245     size_t debug_fwrite (const(void)* buffer, size_t size, size_t count, IOHandle handle)
246     {
247         WrappedIO* wio = cast(WrappedIO*) handle;
248         printf("Write %lld elements of %lld bytes\n", cast(long)count, cast(long)size);
249         size_t r = wio.wrapped.write(buffer, size, count, wio.handle);
250         printf("  => written %lld elements\n", cast(long)r);
251         return r;
252     }
253 
254     size_t debug_fread (void* buffer, size_t size, size_t count, IOHandle handle)
255     {
256         WrappedIO* wio = cast(WrappedIO*) handle;
257         printf("Read %lld elements of %lld bytes\n", cast(long)count, cast(long)size);
258         size_t r = wio.wrapped.read(buffer, size, count, wio.handle);
259         printf("  => read %lld elements\n", cast(long)r);
260         return r;
261     }
262 
263     int debug_fseek(IOHandle handle, c_long offset, int origin)
264     {
265         WrappedIO* wio = cast(WrappedIO*) handle;
266         printf("Seek to offset %lld, mode %d\n", cast(long) offset, origin);
267         int r = wio.wrapped.seek(wio.handle, offset, origin);
268         if (r == 0)
269             printf("  => success\n", r);
270         else
271             printf(" => failure\n");
272         return r;
273     }
274 
275     c_long debug_ftell(IOHandle handle)
276     {
277         WrappedIO* wio = cast(WrappedIO*) handle;
278         printf("Tell offset\n");
279         c_long r = wio.wrapped.tell(wio.handle);
280         printf("  => offset is %lld\n", cast(long)r);
281         return r;
282     }
283 
284     int debug_feof(IOHandle handle)
285     {
286         WrappedIO* wio = cast(WrappedIO*) handle;
287         printf("Is feof?\n");
288         int r = wio.wrapped.eof(wio.handle);
289         printf("  => returned %d\n", r);
290         return r;
291     }
292 }
293 
294 
295 package:
296 
297 /// This is basically an owned buffer, with capacity, optionally borrowed.
298 /// The original things being that it can both be used for reading and writing.
299 struct MemoryFile
300 {
301 public nothrow @nogc @safe:
302 
303     /// If the memory is owned by MemoryFile, or borrowed.
304     bool owned = false;
305 
306     /// If can only read from buffer.
307     bool readOnly = false;
308 
309     /// Pointer to data (owned or borrowed).
310     /// if `owned`, the buffer is guaranteed to be allocated with malloc/free/realloc.
311     ubyte* data = null;
312 
313     /// Length of buffer.
314     size_t bytes = 0;
315 
316     /// Current pointer in the buffer.
317     size_t offset = 0;
318 
319     /// Size of the underlying allocation, meaningful if `owned`.
320     size_t capacity = 0;
321 
322     /// Return internal data pointer (allocated with malloc/free)
323     /// stream doesn't own it anymore, the caller does instead.
324     /// Can only be called if that buffer is owned is the first place.
325     ubyte[] releaseData() @trusted
326     {
327         assert (owned);
328         owned = false;
329         if (data is null)
330             return null;
331         ubyte* v = data;
332         data = null;
333         return v[0..offset];
334     }
335 
336     @disable this(this);
337 
338     ~this() @trusted
339     {
340         if (owned)
341         {
342             free(data);
343             data = null;
344         }
345     }
346 
347     /// Initialize empty buffer for writing.
348     /// Must be a T.init object.
349     void initEmpty()
350     {
351         owned = true;
352     }
353 
354     /// Initialize buffer as reading a slice.
355     /// Must be a T.init object.
356     void initFromExistingSlice(const(ubyte)[] arr) @system
357     {
358         owned = false;
359         readOnly = true;
360         data = cast(ubyte*) arr.ptr; // const_cast here
361         bytes = arr.length;
362     }
363 
364     // Resize internal buffer so that it exceeds numBytes.
365     // Such buffers are grow-only.
366     void ensureCapacity(size_t numBytes) @trusted
367     {
368         assert(owned);
369 
370         if (capacity >= numBytes)
371             return;
372 
373         // Take greater of numBytes and 2 x current capacity, as the new capacity.
374         size_t newCapacity = numBytes;
375         size_t doubleCap = 1 + 2 * capacity;
376         if (doubleCap > newCapacity)
377             newCapacity = doubleCap;
378 
379         capacity = newCapacity;
380         data = cast(ubyte*) realloc(data, newCapacity);
381     }
382 }
383 
384 extern(C) @system
385 {
386     c_long mtell(MemoryFile *stream)
387     {
388         assert (stream !is null);
389 
390         // Files larger than 0x7fffffff bytes not supported, return errors.
391         if (stream.offset > GAMUT_MAX_POSSIBLE_MEMORY_OFFSET)
392             return -1;
393 
394         return cast(c_long) stream.offset;
395     }
396 
397     int mseek(MemoryFile *stream, c_long offset, int origin)
398     {
399         assert (stream !is null);
400 
401         long baseOffset;
402         if (origin == SEEK_CUR)
403         {
404             baseOffset = stream.offset;
405         }
406         else if (origin == SEEK_END)
407         {
408             baseOffset = stream.bytes;
409         }
410         else if (origin == SEEK_SET)
411         {
412             baseOffset = 0;
413         }
414         long newOffset = baseOffset + offset;
415         assert(newOffset < cast(long)GAMUT_MAX_POSSIBLE_MEMORY_OFFSET);
416 
417         // It is valid to seek from 0 to bytes.
418         //  0________________N-1 N      N+1
419         //  ^ ok                 ^ ok   ^ not ok
420         bool success = newOffset >= 0 && newOffset <= stream.bytes;
421 
422         if (!success)
423             return -1;
424 
425         stream.offset = cast(size_t) newOffset;
426         return 0;
427     }
428 
429     size_t mread(void *buffer, size_t size, size_t count, MemoryFile *stream)
430     {
431         assert (stream !is null);
432 
433         size_t available = stream.bytes - stream.offset;
434         assert (available >= 0); // cursor not allowed to be after eof
435 
436         assert(size <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ);
437         assert(count <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ);
438         long needed = cast(long)size * cast(long)count; // won't overflow
439 
440         assert(needed <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ);
441 
442         size_t toRead;
443         if (available >= needed)
444             toRead = count;
445         else
446             toRead = available / size;
447 
448         size_t bytes = toRead * cast(size_t)size;
449         memcpy(buffer, &stream.data[stream.offset], bytes);
450         stream.offset += bytes;
451         return toRead;
452     }
453 
454     size_t mwrite(void *buffer, size_t size, size_t count, MemoryFile *stream)
455     {
456         assert (stream !is null);
457         assert(size <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_WRITE);
458         assert(count <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_WRITE);
459         size_t bytes = cast(size_t)size * cast(size_t)count; // won't overflow
460         assert(bytes <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_WRITE);
461         stream.ensureCapacity(stream.offset + bytes);
462         memcpy(&stream.data[stream.offset], buffer, bytes);
463         stream.offset += bytes;
464         return count;
465     }
466     
467     int meof(MemoryFile *stream)
468     {
469         assert(stream);
470         return (stream.offset >= stream.bytes) ? 1 : 0;
471     }
472 }