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 }