1 module jaster.stream.file; 2 3 private 4 { 5 import std.stdio : File, SEEK_CUR, SEEK_END, SEEK_SET; 6 import std.file : exists, fwrite = write; 7 import std.exception : enforce; 8 import std.typecons : Flag; 9 import jaster.stream; 10 } 11 12 /++ 13 + This is a `Stream` that writes/reads directly from, and to a file. 14 + 15 + It uses `std.stdio.File` internally. 16 + 17 + Other than exceptions, this class does not allocate any GC memory (though the same cannot be said for `File`, 18 + I haven't checked yet). 19 + ++/ 20 class FileStream : Stream 21 { 22 alias DisposeOnDtor = Flag!"doDtor"; 23 24 /// Describes how the file should be opened. 25 /// 26 /// This will also determine the capabilities (`Stream.Can`) of the stream. 27 enum Mode : int 28 { 29 /// Opens the $(B only) if it exists. 30 Open = 1 << 0, 31 32 /// Creates the file if it doesn't exist, and then acts the same as `Mode.Open`. 33 Create = 1 << 1, 34 35 /// Supports reading. 36 Read = 1 << 2, 37 38 /// Supports writing. 39 Write = 1 << 3, 40 41 /// Opens the file in binary mode. 42 Binary = 1 << 4, 43 44 /// Sets the file's size to 0 upon opening. $(B Does not imply `Mode.Create`) 45 /// 46 /// Combine with Mode.Create to always make sure to either create, or truncate a file, meaning it's always there 47 /// and always empty. 48 Truncate = 1 << 5, 49 50 ReadWrite = Read | Write, 51 OpenOrCreate = Open | Create, 52 ReadOpen = Open | Read, 53 ReadCreate = Create | Read, 54 ReadOpenOrCreate = OpenOrCreate | Read, 55 WriteOpen = Open | Write, 56 WriteCreate = Create | Write, 57 WriteOpenOrCreate = OpenOrCreate | Write, 58 ReadWriteOpen = Open | Read | Write, 59 ReadWriteCreate = Create | Read | Write, 60 ReadWriteOpenOrCreate = OpenOrCreate | Read | Write 61 } 62 63 private 64 { 65 File _file; 66 Mode _mode; 67 DisposeOnDtor _doDtor; 68 69 static const ubyte[] EMPTY_DATA = []; 70 } 71 72 /++ 73 + Opens the specified file. 74 + 75 + Notes: 76 + During destruction of a stream made from this constructor, the `dispose` function will 77 + be called if the object is not already disposed. 78 + 79 + Throws: 80 + `StreamException` if `file` could not be found. 81 + 82 + `StreamException` if `mode` doesn't specify either `Open`, `Create` in some form. 83 + 84 + Params: 85 + mode = The `FileStream.Mode` to open the `file` in. 86 + file = The file to open. 87 + ++/ 88 this(Mode mode, const char[] file) 89 { 90 if((mode & Mode.Create) && !file.exists) 91 fwrite(file, EMPTY_DATA); 92 93 if(mode & Mode.Open) 94 enforce!StreamException(file.exists, "File not found: "~file); 95 else if(!(mode & Mode.Create)) 96 throw new StreamException("The given mode does not specify Create, Open, or OpenOrCreate."); 97 98 char[3] buffer; 99 this._file = File(file, mode.modeToString(buffer)); 100 this._mode = mode; 101 this._doDtor = DisposeOnDtor.yes; 102 103 super(mode.canFromMode()); 104 } 105 106 /++ 107 + Wraps around an existing `File`. 108 + 109 + Assertions: 110 + `file` must have a file already opened. 111 + 112 + Notes: 113 + Unlike the other constructor, objects made from this constructor will only call `dispose` 114 + during the dtor if `doDtor` is set to `DisposeOnDtor.yes`. 115 + 116 + This means, that if either this stream is disposed manually, or this object is collected/destroyed, 117 + then external copies of the `file` will also end up being closed. Which could possibly lead to confusion. 118 + 119 + Also note that due to how this stream's `isDispose` works, if `file` is closed outside of this stream, then 120 + this stream will begin to throw `StreamDisposedException`s. 121 + 122 + Params: 123 + mode = The `FileStream.Mode` to use. For this constructor, `Open`, `Create`, and `Truncate` have no effect. 124 + file = The `File` to wrap around. 125 + doDtor = See the `Notes` section. 126 + ++/ 127 this(Mode mode, File file, DisposeOnDtor doDtor = DisposeOnDtor.yes) 128 { 129 assert(file.isOpen, "Make sure the file is actually open first."); 130 131 this._file = file; 132 this._mode = mode; 133 this._doDtor = doDtor; 134 135 super(mode.canFromMode()); 136 } 137 138 ~this() 139 { 140 if(this._doDtor) 141 this.dispose(); 142 } 143 144 public override 145 { 146 const(ubyte[]) write(scope const ubyte[] data) 147 { 148 enforceNotDisposed(this); 149 enforceCanWrite(this); 150 this._file.rawWrite(data); 151 152 return data; // rawWrite throws an exception if the entire thing isn't written... Maybe I should wrap it around a StreamException 153 } 154 155 ubyte[] readToBuffer(scope ref ubyte[] buffer) 156 { 157 enforceNotDisposed(this); 158 enforceCanRead(this); 159 160 return this._file.rawRead(buffer); 161 } 162 163 void dispose() 164 { 165 if(!this.isDisposed) 166 this.flush(); 167 168 this._file.close(); 169 } 170 171 void flush() 172 { 173 enforceNotDisposed(this); 174 this._file.flush(); 175 this._file.sync(); 176 } 177 178 void seek(SeekFrom from, ptrdiff_t amount) 179 { 180 enforceNotDisposed(this); 181 auto start = (from == SeekFrom.Start) 182 ? SEEK_SET 183 : (from == SeekFrom.Current) 184 ? SEEK_CUR 185 : SEEK_END; 186 187 auto oldPos = this.position; 188 this._file.seek(cast(long)amount, start); 189 190 if(this.position > this.length) 191 { 192 import std.format : format; 193 auto debugPos = this.position; 194 this.position = oldPos; 195 throw new StreamException( 196 format("Attempted to seek past the stream. Destination = %s. Length = %s", 197 debugPos, 198 this.length 199 ) 200 ); 201 } 202 } 203 204 /// For FileStreams, this function will return `true` if the underlying `File` has been closed. 205 @property 206 bool isDisposed() const 207 { 208 return !this._file.isOpen; 209 } 210 211 @property 212 void writeTimeout(Duration dur) 213 { 214 enforceNotDisposed(this); 215 throw new StreamCannotTimeoutException(); 216 } 217 218 @property 219 Duration writeTimeout() 220 { 221 enforceNotDisposed(this); 222 throw new StreamCannotTimeoutException(); 223 } 224 225 @property 226 void readTimeout(Duration dur) 227 { 228 enforceNotDisposed(this); 229 throw new StreamCannotTimeoutException(); 230 } 231 232 @property 233 Duration readTimeout() 234 { 235 enforceNotDisposed(this); 236 throw new StreamCannotTimeoutException(); 237 } 238 239 @property 240 void length(size_t size) 241 { 242 if(size > this.length) 243 { 244 auto amount = (size - this.length); 245 auto pos = this.position; 246 scope(exit) this.position = pos; 247 248 this._file.seek(size - 1, SEEK_SET); 249 250 byte b = 0; 251 this._file.rawWrite((&b)[0..1]); 252 } 253 } 254 255 @property 256 size_t length() 257 { 258 enforceNotDisposed(this); 259 260 auto size = this._file.size(); 261 enforce!StreamException(size <= size_t.max, "The size is out of bounds of size_t.max"); 262 return cast(size_t)size; 263 } 264 265 @property 266 void position(size_t pos) 267 { 268 this.seek(SeekFrom.Start, pos); 269 } 270 271 @property 272 size_t position() 273 { 274 enforceNotDisposed(this); 275 276 auto pos = this._file.tell(); 277 enforce!StreamException(pos <= size_t.max, "The position is out of bounds of size_t.max"); 278 return cast(size_t)pos; 279 } 280 } 281 } 282 /// 283 unittest 284 { 285 import std.exception : assertThrown; 286 287 auto mode = FileStream.Mode.ReadWriteCreate | FileStream.Mode.Truncate | FileStream.Mode.Binary; 288 auto stream = new FileStream(mode, "tests/fs2.bin"); 289 290 stream.length = 200; 291 assert(stream.length == 200); 292 293 assertThrown!StreamException(stream.position = 201); 294 } 295 296 // Stream.Can checks 297 unittest 298 { 299 import std.exception : assertThrown; 300 301 assert( new FileStream(FileStream.Mode.WriteCreate, "tests/fs1.bin").canWrite); 302 assert( new FileStream(FileStream.Mode.ReadCreate, "tests/fs1.bin").canRead); 303 assert(!new FileStream(FileStream.Mode.WriteCreate, "tests/fs1.bin").canRead); 304 305 assertThrown!StreamCannotWriteException(new FileStream(FileStream.Mode.ReadOpen, "tests/fs1.bin").write([1])); 306 assertThrown!StreamCannotReadException(new FileStream(FileStream.Mode.WriteOpen, "tests/fs1.bin").read(1)); 307 } 308 309 // Dispose checks 310 unittest 311 { 312 import std.exception : assertThrown; 313 314 auto stream = new FileStream(FileStream.Mode.WriteCreate, "tests/fs1.bin"); 315 assert(!stream.isDisposed); 316 stream.dispose(); 317 assert(stream.isDisposed); 318 319 assertThrown!StreamDisposedException(stream.write([1])); 320 } 321 322 /// Returns: A `Stream.Can` from a `FileStream.Mode`. 323 Stream.Can canFromMode(FileStream.Mode mode) 324 { 325 Stream.Can can = Stream.Can.None; 326 327 if(mode & FileStream.Mode.Write) can |= Stream.Can.Write; 328 else if(mode & FileStream.Mode.Read) can |= Stream.Can.Read; 329 330 return can; 331 } 332 333 private char[] modeToString(FileStream.Mode mode, return out char[3] str) 334 { 335 size_t plusIndex = 1; 336 337 if(mode & FileStream.Mode.Binary) 338 { 339 str[1] = 'b'; 340 plusIndex = 2; 341 } 342 343 if(mode & FileStream.Mode.ReadWrite) 344 str[plusIndex] = '+'; 345 346 // Because C is awkward, we'll just make the file ourself if it doesn't exist. 347 // Being able to open a file, with write-only access, *without* truncating the file, seems impossible. 348 str[0] = (mode & FileStream.Mode.Truncate) ? 'w' : 'r'; 349 350 return str[0..plusIndex+1]; 351 }