1 /** 2 Copyright: Copyright (c) 2016-2018, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Abstractions merging in-memory *file like objects" with memory mapped files. 11 */ 12 module dextool.vfs; 13 14 import std.traits : isSomeString; 15 import std.typecons : Flag, Yes, No; 16 import logger = std.experimental.logger; 17 18 public import dextool.type : FileName; 19 20 enum Mode { 21 read, 22 readWrite 23 } 24 25 /** 26 * TODO use DIP-1000 to implement slices that do not escape the scope. 27 */ 28 @safe struct VfsFile { 29 import std.typecons : NullableRef; 30 31 private static struct Impl { 32 VirtualFileSystem.RefCntMem* mem; 33 34 bool isInitialized() @safe pure nothrow const @nogc { 35 return mem !is null; 36 } 37 } 38 39 private Impl* payload; 40 private NullableRef!VirtualFileSystem owner; 41 42 this(NullableRef!VirtualFileSystem owner, VirtualFileSystem.RefCntMem* a) { 43 this.payload = new Impl(a); 44 this.owner = owner; 45 } 46 47 this(this) { 48 if (!payload.isInitialized) 49 return; 50 ++payload.mem.count; 51 } 52 53 ~this() nothrow @nogc { 54 if (!payload.isInitialized) 55 return; 56 57 --payload.mem.count; 58 } 59 60 size_t length() @safe pure nothrow const @nogc { 61 assert(payload.isInitialized); 62 return payload.mem.data.length; 63 } 64 65 void opAssign(typeof(this) rhs) { 66 import std.algorithm : swap; 67 68 swap(payload, rhs.payload); 69 } 70 71 ubyte opIndexAssign(ubyte value, size_t i) { 72 assert(payload.isInitialized); 73 return payload.mem.data[i] = value; 74 } 75 76 ubyte opIndex(size_t i) { 77 assert(payload.isInitialized); 78 return payload.mem.data[i]; 79 } 80 81 ubyte[] opSlice() @safe pure nothrow @nogc { 82 assert(payload.isInitialized); 83 return payload.mem.data; 84 } 85 86 ubyte[] opSlice(size_t begin, size_t end) @safe pure nothrow @nogc { 87 assert(payload.isInitialized); 88 assert(end <= opDollar); 89 90 return payload.mem.data[begin .. end]; 91 } 92 93 size_t opDollar() @safe pure nothrow const @nogc { 94 return this.length; 95 } 96 97 void write(const(ubyte)[] content) { 98 assert(payload.isInitialized); 99 100 if (payload.mem.isMmf) { 101 owner.appendMmf(payload.mem, content); 102 } else { 103 payload.mem.data ~= content; 104 } 105 } 106 107 void write(string content) @safe { 108 this.write(trustedCast!(ubyte[])(content)); 109 } 110 111 // Note: it is NOT safe to return a string because the buffer mutates if 112 // the file on disk is changed. 113 scope const(char)[] toChars() @safe { 114 import std.utf : validate; 115 116 auto data = this.opSlice(); 117 118 auto result = trustedCast!(const(char)[])(data); 119 validate(result); 120 return result; 121 } 122 123 private static auto trustedCast(T0, T1)(T1 buf) @trusted { 124 return cast(T0) buf; 125 } 126 } 127 128 /** File layer abstracting the handling of in-memory files and concrete 129 * filesystem files. 130 * 131 * This struct abstracts and contains those differences. 132 * 133 * The lookup rule for a filename is: 134 * - in-memory container. 135 * - load from the filesystem. 136 * 137 * TODO Is it better to have everything as MMF? 138 * I think it would be possible to have the source code as an anonymous MMF. 139 */ 140 struct VirtualFileSystem { 141 import std.typecons : nullableRef; 142 import std.mmfile : MmFile; 143 144 private { 145 struct RefCntMem { 146 ubyte[] data; 147 size_t count; 148 bool isMmf; 149 150 this(bool is_mmf) @safe pure nothrow { 151 this.count = 1; 152 this.isMmf = is_mmf; 153 } 154 } 155 156 struct MmFSize { 157 MmFile file; 158 Mode mode; 159 size_t size; 160 } 161 162 // enables *fast* reverse mapping of a ptr to its filename. 163 // must be kept in sync with files_ and filesys 164 FileName[RefCntMem* ] rev_files; 165 166 RefCntMem*[FileName] files_; 167 MmFSize[RefCntMem* ] filesys; 168 } 169 170 // The VFS is "heavy", forbid movement. 171 @disable this(this); 172 173 /** Release all resources held by the VFS. 174 * 175 * trusted: the memory mapped files are NOT really trusted until DIP-1000 176 * is used. This is because the slices that leave them can be stored/used 177 * at other places in such a manner that the ptr of the slice reference a 178 * memory mapped file that has been released. 179 * 180 * But this is so far a minor problem that is partially mitigated by 181 * disabling the postblit. This mean that the VFS commonly have a lifetime 182 * that is longer than the users of the slices. 183 */ 184 void release() @trusted nothrow { 185 foreach (f; filesys.byValue()) { 186 f.destroy; 187 } 188 filesys.clear; 189 files_.clear; 190 rev_files.clear; 191 } 192 193 /** Add a mapping to a concrete file. 194 * 195 * Params: 196 * fname = file to map into the VFS 197 */ 198 VfsFile open(FileName fname, Mode mode = Mode.read) @safe { 199 if (auto v = fname in files_) { 200 (*v).count += 1; 201 return VfsFile(nullableRef(&this), *v); 202 } 203 204 import std.file : getSize, exists; 205 206 auto mmf_mode = mode == Mode.read ? MmFile.Mode.read : MmFile.Mode.readWriteNew; 207 208 size_t sz; 209 size_t buf_size; 210 if (exists(cast(string) fname)) { 211 sz = getSize(cast(string) fname); 212 buf_size = sz; 213 } 214 215 // a new file must have a buffer size > 0 or it crashes. 216 if (buf_size == 0) 217 buf_size = 1; 218 219 // TODO I'm not sure what checks need to be added to make this safe. 220 // Should be doable so marking it as safe for now with the intention of 221 // revisiting this to ensure it is safe. 222 auto mmf = () @trusted{ 223 return new MmFile(cast(string) fname, mmf_mode, buf_size, null); 224 }(); 225 226 auto mem = new RefCntMem(true); 227 mem.data = () @trusted{ return cast(ubyte[]) mmf[0 .. mmf.length]; }(); 228 229 filesys[mem] = MmFSize(mmf, mode, sz); 230 files_[fname] = mem; 231 rev_files[mem] = fname; 232 233 return VfsFile(nullableRef(&this), mem); 234 } 235 236 /** Create an in-memory file. 237 * 238 * Params: 239 * fname = simulated in-memory filename. 240 */ 241 VfsFile openInMemory(FileName fname) @safe { 242 if (auto v = fname in files_) { 243 (*v).count += 1; 244 return VfsFile(nullableRef(&this), *v); 245 } 246 247 auto mem = new RefCntMem(false); 248 files_[fname] = mem; 249 rev_files[mem] = fname; 250 return VfsFile(nullableRef(&this), mem); 251 } 252 253 private void close(FileName fname) @safe nothrow @nogc { 254 if (auto v = fname in files_) { 255 if ((*v).isMmf) { 256 if (auto mmf = *v in filesys) { 257 mmf.destroy; 258 filesys.remove(*v); 259 } 260 } 261 rev_files.remove(*v); 262 files_.remove(fname); 263 } 264 } 265 266 /** Append data to a memory mapped file. */ 267 private void appendMmf(VirtualFileSystem.RefCntMem* mem, const(ubyte)[] data) @safe { 268 assert(mem.isMmf); 269 270 if (auto mmf = mem in filesys) { 271 const orig_sz = mmf.size; 272 273 mem.data = () @trusted{ 274 return cast(ubyte[])(*mmf).file[0 .. mmf.size + data.length]; 275 }(); 276 mmf.size += data.length; 277 mem.data[orig_sz .. $] = data[]; 278 } else { 279 assert(0); 280 } 281 } 282 283 /** 284 * Trusted on the assumption that byKey is @safe _enough_. 285 * 286 * Returns: range of the filenames in the VFS. 287 */ 288 auto files() @trusted pure nothrow const @nogc { 289 return rev_files.byKey; 290 } 291 } 292 293 version (unittest) { 294 import unit_threaded : shouldEqual; 295 } 296 297 @("shall be an in-memory mapped file") 298 @safe unittest { 299 VirtualFileSystem vfs; 300 string code = "some code"; 301 auto filename = FileName("path/to/code.c"); 302 303 auto f = vfs.openInMemory(filename); 304 f.write(code); 305 306 f.toChars.shouldEqual(code); 307 308 () @trusted{ 309 f[].shouldEqual(cast(ubyte[]) code); 310 f[0].shouldEqual(cast(ubyte) 's'); 311 f[1 .. 3].shouldEqual(cast(ubyte[]) "om"); 312 f[5 .. $].shouldEqual(cast(ubyte[]) "code"); 313 314 string write_val = "smurf"; 315 auto buf = f[4 .. $]; 316 buf[] = cast(ubyte[]) write_val; 317 f[4 .. $].shouldEqual(cast(ubyte[]) "smurf"); 318 }(); 319 } 320 321 @("shall be a file from the filesystem") 322 unittest { 323 import std..string : toStringz; 324 import std.stdio; 325 import std.random : uniform; 326 import std.conv : to; 327 328 VirtualFileSystem vfs; 329 string code = "content of fun.txt"; 330 331 auto filename = FileName(uniform!size_t.to!string ~ "_test_vfs.txt"); 332 File(filename, "w").write(code); 333 scope (exit) 334 remove(filename.toStringz); 335 336 auto f = vfs.open(filename); 337 f.toChars.shouldEqual(code); 338 }