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 package:
129 
130     /// Setup the IOStream for reading a file. The passed `IOHandle` will need to be a `FILE*`.
131     /// For internal Gamut usage.
132     void setupForFileIO() pure @trusted
133     {
134         read  = cast(ReadProc) &fread;
135         write = cast(WriteProc) &fwrite;
136         seek  = cast(SeekProc) &fseek;
137         tell  = cast(TellProc) &ftell;
138         eof   = cast(EofProc) &feof;
139     }
140 
141     /// Setup the IOStream for using a  a file. The passed `IOHandle` will need to be a `MemoryFile*`.
142     /// For internal Gamut usage.
143     void setupForMemoryIO() pure @trusted
144     {
145         read  = cast(ReadProc)  &mread;
146         write = cast(WriteProc) &mwrite;
147         seek  = cast(SeekProc)  &mseek;
148         tell  = cast(TellProc)  &mtell;
149         eof   = cast(EofProc)   &meof;
150     }
151 
152     /// Setup the IOStream for wrapping another IOStream and logging what happens.
153     /// The passed `IOHandle` will need to be a `WrappedIO`.
154     /// For internal Gamut usage.
155     debug void setupSetupForLogging(ref IOStream io) pure @trusted
156     {
157         io.read  = &debug_fread;
158         io.write = &debug_fwrite;
159         io.seek  = &debug_fseek;
160         io.tell  = &debug_ftell;
161         io.eof   = &debug_feof;
162     }
163 }
164 
165 debug package struct WrappedIO
166 {
167     IOStream* wrapped; /// I/O object being wrapped.
168     IOHandle handle;   /// Original handle.
169 }
170 
171 package bool fileIsStartingWithSignature(IOStream *io, IOHandle handle, immutable ubyte[] signature)
172 {
173     assert(signature.length <= 16);
174 
175     // save I/O cursor
176     c_long offset = io.tell(handle);
177 
178     ubyte[16] header;
179     bool enoughBytes = (signature.length == io.read(header.ptr, 1, signature.length, handle));
180     bool match = enoughBytes && (signature == header[0..signature.length]);
181 
182     // restore I/O cursor
183     if (!io.seekAbsolute(handle, offset))
184         return false; // TODO: that rare error should propagate somehow
185 
186     return match;
187 }
188 
189 debug extern(C) @system private
190 {
191     // Note: these functions expect a `WrappedIO` to be passed as handle.
192     import core.stdc.stdio;
193 
194     size_t debug_fwrite (const(void)* buffer, size_t size, size_t count, IOHandle handle)
195     {
196         WrappedIO* wio = cast(WrappedIO*) handle;
197         printf("Write %lld elements of %lld bytes\n", cast(long)count, cast(long)size);
198         size_t r = wio.wrapped.write(buffer, size, count, wio.handle);
199         printf("  => written %lld elements\n", cast(long)r);
200         return r;
201     }
202 
203     size_t debug_fread (void* buffer, size_t size, size_t count, IOHandle handle)
204     {
205         WrappedIO* wio = cast(WrappedIO*) handle;
206         printf("Read %lld elements of %lld bytes\n", cast(long)count, cast(long)size);
207         size_t r = wio.wrapped.read(buffer, size, count, wio.handle);
208         printf("  => read %lld elements\n", cast(long)r);
209         return r;
210     }
211 
212     int debug_fseek(IOHandle handle, c_long offset, int origin)
213     {
214         WrappedIO* wio = cast(WrappedIO*) handle;
215         printf("Seek to offset %lld, mode %d\n", cast(long) offset, origin);
216         int r = wio.wrapped.seek(wio.handle, offset, origin);
217         if (r == 0)
218             printf("  => success\n", r);
219         else
220             printf(" => failure\n");
221         return r;
222     }
223 
224     c_long debug_ftell(IOHandle handle)
225     {
226         WrappedIO* wio = cast(WrappedIO*) handle;
227         printf("Tell offset\n");
228         c_long r = wio.wrapped.tell(wio.handle);
229         printf("  => offset is %lld\n", cast(long)r);
230         return r;
231     }
232 
233     int debug_feof(IOHandle handle)
234     {
235         WrappedIO* wio = cast(WrappedIO*) handle;
236         printf("Is feof?\n");
237         int r = wio.wrapped.eof(wio.handle);
238         printf("  => returned %d\n", r);
239         return r;
240     }
241 }
242 
243 
244 package:
245 
246 /// This is basically an owned buffer, with capacity, optionally borrowed.
247 /// The original things being that it can both be used for reading and writing.
248 struct MemoryFile
249 {
250 public nothrow @nogc @safe:
251 
252     /// If the memory is owned by MemoryFile, or borrowed.
253     bool owned = false;
254 
255     /// If can only read from buffer.
256     bool readOnly = false;
257 
258     /// Pointer to data (owned or borrowed).
259     /// if `owned`, the buffer is guaranteed to be allocated with malloc/free/realloc.
260     ubyte* data = null;
261 
262     /// Length of buffer.
263     size_t bytes = 0;
264 
265     /// Current pointer in the buffer.
266     size_t offset = 0;
267 
268     /// Size of the underlying allocation, meaningful if `owned`.
269     size_t capacity = 0;
270 
271     /// Return internal data pointer (allocated with malloc/free)
272     /// stream doesn't own it anymore, the caller does instead.
273     /// Can only be called if that buffer is owned is the first place.
274     ubyte[] releaseData() @trusted
275     {
276         assert (owned);
277         owned = false;
278         if (data is null)
279             return null;
280         ubyte* v = data;
281         data = null;
282         return v[0..offset];
283     }
284 
285     @disable this(this);
286 
287     ~this() @trusted
288     {
289         if (owned)
290         {
291             free(data);
292             data = null;
293         }
294     }
295 
296     /// Initialize empty buffer for writing.
297     /// Must be a T.init object.
298     void initEmpty()
299     {
300         owned = true;
301     }
302 
303     /// Initialize buffer as reading a slice.
304     /// Must be a T.init object.
305     void initFromExistingSlice(const(ubyte)[] arr) @system
306     {
307         owned = false;
308         readOnly = true;
309         data = cast(ubyte*) arr.ptr; // const_cast here
310         bytes = arr.length;
311     }
312 
313     // Resize internal buffer so that it exceeds numBytes.
314     // Such buffers are grow-only.
315     void ensureCapacity(size_t numBytes) @trusted
316     {
317         assert(owned);
318 
319         if (capacity >= numBytes)
320             return;
321 
322         // Take greater of numBytes and 2 x current capacity, as the new capacity.
323         size_t newCapacity = numBytes;
324         size_t doubleCap = 1 + 2 * capacity;
325         if (doubleCap > newCapacity)
326             newCapacity = doubleCap;
327 
328         capacity = newCapacity;
329         data = cast(ubyte*) realloc(data, newCapacity);
330     }
331 }
332 
333 extern(C) @system
334 {
335     c_long mtell(MemoryFile *stream)
336     {
337         assert (stream !is null);
338 
339         // Files larger than 0x7fffffff bytes not supported, return errors.
340         if (stream.offset > GAMUT_MAX_POSSIBLE_MEMORY_OFFSET)
341             return -1;
342 
343         return cast(c_long) stream.offset;
344     }
345 
346     int mseek(MemoryFile *stream, c_long offset, int origin)
347     {
348         assert (stream !is null);
349 
350         long baseOffset;
351         if (origin == SEEK_CUR)
352         {
353             baseOffset = stream.offset;
354         }
355         else if (origin == SEEK_END)
356         {
357             baseOffset = stream.bytes;
358         }
359         else if (origin == SEEK_SET)
360         {
361             baseOffset = 0;
362         }
363         long newOffset = baseOffset + offset;
364         assert(newOffset < cast(long)GAMUT_MAX_POSSIBLE_MEMORY_OFFSET);
365 
366         // It is valid to seek from 0 to bytes.
367         //  0________________N-1 N      N+1
368         //  ^ ok                 ^ ok   ^ not ok
369         bool success = newOffset >= 0 && newOffset <= stream.bytes;
370 
371         if (!success)
372             return -1;
373 
374         stream.offset = cast(size_t) newOffset;
375         return 0;
376     }
377 
378     size_t mread(void *buffer, size_t size, size_t count, MemoryFile *stream)
379     {
380         assert (stream !is null);
381 
382         size_t available = stream.bytes - stream.offset;
383         assert (available >= 0); // cursor not allowed to be after eof
384 
385         assert(size <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ);
386         assert(count <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ);
387         long needed = cast(long)size * cast(long)count; // won't overflow
388 
389         assert(needed <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_READ);
390 
391         size_t toRead;
392         if (available >= needed)
393             toRead = count;
394         else
395             toRead = available / size;
396 
397         size_t bytes = toRead * cast(size_t)size;
398         memcpy(buffer, &stream.data[stream.offset], bytes);
399         stream.offset += bytes;
400         return toRead;
401     }
402 
403     size_t mwrite(void *buffer, size_t size, size_t count, MemoryFile *stream)
404     {
405         assert (stream !is null);
406         assert(size <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_WRITE);
407         assert(count <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_WRITE);
408         size_t bytes = cast(size_t)size * cast(size_t)count; // won't overflow
409         assert(bytes <= GAMUT_MAX_POSSIBLE_SIMULTANEOUS_WRITE);
410         stream.ensureCapacity(stream.offset + bytes);
411         memcpy(&stream.data[stream.offset], buffer, bytes);
412         stream.offset += bytes;
413         return count;
414     }
415     
416     int meof(MemoryFile *stream)
417     {
418         assert(stream);
419         return (stream.offset >= stream.bytes) ? 1 : 0;
420     }
421 }