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 }