1 /** 2 * Defines input and output streams for reading and writing files using C's 3 * stdio functions like `fopen`, `fread`, and `fwrite` as a basis. 4 */ 5 module streams.types.file; 6 7 import streams.primitives; 8 9 import core.stdc.stdio : fopen, fclose, fread, fwrite, fflush, feof, ferror, FILE; 10 11 /** 12 * A byte input stream that reads from a file. Makes use of the underlying 13 * `fopen` and related C functions. 14 */ 15 struct FileInputStream { 16 private FILE* filePtr; 17 18 /** 19 * Constructs an input stream to read from the given file pointer. 20 * Params: 21 * filePtr = The file pointer. It should not be null, and should refer to 22 * a file opened with `fopen` in `rb` mode. 23 */ 24 this(FILE* filePtr) { 25 assert(filePtr !is null); 26 this.filePtr = filePtr; 27 } 28 29 /** 30 * Constructs an input stream to read from the given file. 31 * Params: 32 * filename = The filename to open. 33 */ 34 this(const(char*) filename) { 35 this(fopen(filename, "rb")); 36 } 37 38 /** 39 * Reads up to `buffer.length` bytes from the file. 40 * Params: 41 * buffer = The buffer to read into. 42 * Returns: The number of bytes that were read, or an error if `fread` fails. 43 */ 44 StreamResult readFromStream(ubyte[] buffer) { 45 if (this.filePtr is null) { 46 return StreamResult(StreamError("File is not open.", -1)); 47 } 48 size_t bytesRead = fread(buffer.ptr, ubyte.sizeof, buffer.length, this.filePtr); 49 if (bytesRead != buffer.length && ferror(this.filePtr) != 0) { 50 return StreamResult(StreamError("An error occurred while reading.", cast(int) bytesRead)); // cov-ignore 51 } 52 return StreamResult(cast(uint) bytesRead); 53 } 54 55 /** 56 * Closes the file stream by calling `fclose` on the underlying file 57 * pointer. 58 * Returns: An optional stream error if `fclose` fails. 59 */ 60 OptionalStreamError closeStream() { 61 if (this.filePtr !is null) { 62 int result = fclose(this.filePtr); 63 if (result != 0) { 64 return OptionalStreamError(StreamError("Could not close the file.", result)); // cov-ignore 65 } 66 this.filePtr = null; 67 } 68 return OptionalStreamError.init; 69 } 70 } 71 72 unittest { 73 import streams.primitives : isInputStream, isClosableStream; 74 import streams.types.array : ArrayOutputStream, byteArrayOutputStream; 75 import streams.functions : transferTo; 76 import core.stdc.stdio; 77 78 assert(isInputStream!(FileInputStream, ubyte)); 79 assert(isClosableStream!FileInputStream); 80 81 // Test reading from a file. 82 ArrayOutputStream!ubyte sOut = byteArrayOutputStream(); 83 FILE* fp1 = fopen("LICENSE", "rb"); 84 fseek(fp1, 0L, SEEK_END); 85 ulong expectedFilesize = ftell(fp1); 86 fclose(fp1); 87 FileInputStream fIn = FileInputStream("LICENSE"); 88 transferTo(fIn, sOut); 89 fIn.closeStream(); 90 // Check that after closing the stream, the file pointer is nullified. 91 assert(fIn.filePtr is null); 92 ubyte[3] tempBuffer; 93 assert(fIn.readFromStream(tempBuffer).hasError); // Reading after closed should error. 94 95 // Check that the number of bytes read matches. 96 assert(sOut.toArrayRaw().length == expectedFilesize); 97 98 // Check that the read was correct manually. We need a no-gc way to read 99 // the file contents without using the FileInputStream impl. 100 import core.stdc.stdlib; 101 fp1 = fopen("LICENSE", "rb"); 102 assert(fp1 !is null); 103 ubyte* buffer = cast(ubyte*) malloc(expectedFilesize * ubyte.sizeof); 104 size_t bytesRead = fread(buffer, ubyte.sizeof, expectedFilesize, fp1); 105 assert(bytesRead == expectedFilesize); 106 fclose(fp1); 107 108 assert(sOut.toArrayRaw() == buffer[0 .. expectedFilesize]); 109 free(buffer); 110 } 111 112 /** 113 * A byte output stream that writes to a file. 114 */ 115 struct FileOutputStream { 116 private FILE* filePtr; 117 118 /** 119 * Constructs an output stream to write to the given file pointer. 120 * Params: 121 * filePtr = The file pointer. It should not be null, and it should refer 122 * to a file opened by `fopen` in `wb` mode. 123 */ 124 this(FILE* filePtr) { 125 assert(filePtr !is null); 126 this.filePtr = filePtr; 127 } 128 129 /** 130 * Constructs an output stream to write to the given file. 131 * Params: 132 * filename = The name of the file to write to. 133 */ 134 this(const(char*) filename) { 135 this(fopen(filename, "wb")); 136 } 137 138 /** 139 * Writes up to `buffer.length` bytes to the file. 140 * Params: 141 * buffer = The bytes to write. 142 * Returns: A result that's either `buffer.length` or an error. 143 */ 144 StreamResult writeToStream(ubyte[] buffer) { 145 if (this.filePtr is null) { 146 return StreamResult(StreamError("File is not open.", -1)); 147 } 148 size_t bytesWritten = fwrite(buffer.ptr, ubyte.sizeof, buffer.length, this.filePtr); 149 if (bytesWritten < buffer.length && ferror(this.filePtr) != 0) { 150 return StreamResult(StreamError("An error occurred while writing.", cast(int) bytesWritten)); // cov-ignore 151 } 152 return StreamResult(cast(uint) bytesWritten); 153 } 154 155 /** 156 * Flushes the file to the disk. 157 * Returns: An optional stream error if `fflush` fails. 158 */ 159 OptionalStreamError flushStream() { 160 if (this.filePtr !is null) { 161 int result = fflush(this.filePtr); 162 if (result != 0) return OptionalStreamError(StreamError("Could not flush the file.", result)); 163 } 164 return OptionalStreamError.init; 165 } 166 167 /** 168 * Closes the file. 169 * Returns: An optional stream error if `fclose` fails. 170 */ 171 OptionalStreamError closeStream() { 172 if (this.filePtr !is null) { 173 int result = fclose(this.filePtr); 174 if (result != 0) return OptionalStreamError(StreamError("Could not close the file.", result)); 175 this.filePtr = null; 176 } 177 return OptionalStreamError.init; 178 } 179 } 180 181 unittest { 182 import streams.primitives : isOutputStream, isClosableStream, isFlushableStream; 183 import core.stdc.stdio; 184 import core.stdc.stdlib; 185 186 187 assert(isOutputStream!(FileOutputStream, ubyte)); 188 assert(isClosableStream!FileOutputStream); 189 assert(isFlushableStream!FileOutputStream); 190 191 // Test flushing of file. 192 const(char*) FILENAME = "test-file-flush"; 193 scope(exit) { 194 int result = remove(FILENAME); 195 assert(result == 0); 196 } 197 198 FileOutputStream fOut = FileOutputStream(FILENAME); 199 char[5] content = ['H', 'e', 'l', 'l', 'o']; 200 fOut.writeToStream(cast(ubyte[5]) content); 201 202 // Check that the file doesn't exist yet, when we haven't flushed. 203 FILE* fp1 = fopen(FILENAME, "rb"); 204 ubyte* buffer = cast(ubyte*) malloc(1000 * ubyte.sizeof); 205 size_t bytesRead = fread(buffer, ubyte.sizeof, 1000, fp1); 206 assert(bytesRead == 0); 207 assert(feof(fp1) != 0); 208 assert(ferror(fp1) == 0); 209 fclose(fp1); 210 211 // Flush and check that the contents have updated. 212 fOut.flushStream(); 213 fp1 = fopen(FILENAME, "rb"); 214 assert(fp1 !is null); 215 bytesRead = fread(buffer, ubyte.sizeof, 1000, fp1); 216 assert(bytesRead == 5); 217 fclose(fp1); 218 assert(buffer[0 .. 5] == content); 219 220 // Check that the file pointer is closed upon closing the stream. 221 fOut.closeStream(); 222 assert(fOut.filePtr is null); 223 assert(fOut.writeToStream(cast(ubyte[5]) content).hasError); 224 }