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 }