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 }