1 /**
2 Copyright: Copyright (c) 2016, 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 for easier usage of clang with in-memory files.
11 
12 An user of Clang and especially SourceRanges/SourceLocation shouldn't need to
13 keep track of which files are from the filesystem, which are from the in-memory
14 cache.
15 
16 The VFS provides an agnostic access to files both in-memory and on the
17 filesystem with helper functions for SourceRange/SourceLocation.
18 
19 TODO this is deprecated. Migrate to using `dextool.vfs`.
20 */
21 module cpptooling.utility.virtualfilesystem;
22 
23 import std.traits : isSomeString;
24 import std.typecons : Flag, Yes, No;
25 import logger = std.experimental.logger;
26 
27 import clang.SourceLocation : SourceLocation;
28 import clang.SourceRange : SourceRange;
29 
30 import clang.c.Index : CXUnsavedFile;
31 
32 public import dextool.type : FileName;
33 
34 version (unittest) {
35     import unit_threaded : shouldEqual;
36 }
37 
38 // dfmt off
39 
40 enum Content : string {
41     _init = null
42 }
43 
44 enum Mode {
45     read,
46     readWrite
47 }
48 
49 /** File layer abstracting the handling of in-memory files and concrete
50  * filesystem files.
51  *
52  * This struct abstracts and contains those differences.
53  *
54  * The lookup rule for a filename is:
55  *  - in-memory container.
56  *  - load from the filesystem.
57  *
58  * TODO Is it better to have everything as MMF?
59  * I think it would be possible to have the source code as an anonymous MMF.
60  */
61 struct VirtualFileSystem {
62     import std.mmfile : MmFile;
63 
64     private {
65         struct MmFSize {
66             MmFile file;
67             Mode mode;
68             size_t size;
69         }
70 
71         ubyte[][FileName] in_memory;
72         MmFSize[FileName] filesys;
73     }
74 
75     // The VFS is "heavy", forbid movement.
76     @disable this(this);
77 
78     /** Add a mapping to a concrete file.
79      *
80      * Params:
81      *   fname = file to map into the VFS
82      */
83     void open(FileName fname, Mode mode = Mode.read) @safe {
84         if (fname in in_memory || fname in filesys) {
85             return;
86         }
87 
88         import std.file : getSize, exists;
89 
90         auto mmf_mode = mode == Mode.read ? MmFile.Mode.read : MmFile.Mode.readWriteNew;
91         debug logger.tracef("File memory mapping mode %s (%s) for '%s'", mmf_mode, mode, cast(string) fname);
92 
93         size_t sz;
94         size_t buf_size;
95         if (exists(cast(string) fname)) {
96             sz = getSize(cast(string) fname);
97             buf_size = sz;
98         } else {
99             // a new file must have a buffer size > 0
100             buf_size = 1;
101         }
102 
103         // TODO I'm not sure what checks need to be added to make this safe.
104         // Should be doable so marking it as safe for now with the intention of
105         // revisiting this to ensure it is safe.
106         auto mmf = () @trusted { return new MmFile(cast(string) fname, mmf_mode, buf_size, null); }();
107         filesys[fname] = MmFSize(mmf, mode, sz);
108     }
109 
110     /** Create an in-memory file.
111      *
112      * Params:
113      *   fname = simulated in-memory filename.
114      */
115     void openInMemory(FileName fname) @safe pure {
116         in_memory[fname] = new ubyte[0];
117     }
118 
119     void close(FileName fname) @safe {
120         if (fname in in_memory) {
121             in_memory.remove(fname);
122         } else if (auto f = fname in filesys) {
123             f.destroy;
124             filesys.remove(fname);
125         }
126     }
127 
128     /** Write to a file.
129      *
130      * An in-memory file CAN override the lookup of a filesystem file.
131      *
132      * In-memory is appended to.
133      * A memory mapped is BLOCKED.
134      *
135      * Params:
136      *   fname = filename to write to
137      *   content = stuff to write
138      */
139     void write(FileName fname, Content content) @safe {
140         if (auto f = fname in in_memory) {
141             *f ~= content.dup;
142         } else if (auto f = fname in filesys) {
143             if (f.mode == Mode.readWrite) {
144                 auto cont_s = () @trusted { return cast(ubyte[]) content[]; }();
145                 auto s = () @trusted { return (*f).file[f.size .. f.size + cont_s.length]; }();
146                 () @trusted {
147                     s[] = cont_s[];
148                 }();
149                 f.size += cont_s.length;
150             } else {
151                 assert(false);
152             }
153         }
154     }
155 
156     /**
157      * Trusted on the assumption that byKey is @safe _enough_.
158      *
159      * Returns: range of the filenames in the VFS.
160      */
161     auto files() @trusted pure nothrow const @nogc {
162         import std.range : chain;
163 
164         return chain(in_memory.byKey, filesys.byKey);
165     }
166 }
167 
168 @("shall be an in-memory mapped file")
169 @safe unittest {
170     VirtualFileSystem vfs;
171     string code = "some code";
172     auto filename = cast(FileName) "path/to/code.c";
173 
174     vfs.openInMemory(filename);
175     vfs.write(filename, cast(Content) code);
176 
177     vfs.slice(filename).shouldEqual(code);
178 }
179 
180 @("shall be a file from the filesystem")
181 unittest {
182     import std..string : toStringz;
183     import std.stdio;
184 
185     VirtualFileSystem vfs;
186     string code = "content of fun.txt";
187 
188     auto filename = cast(FileName) "test_vfs.txt";
189     File(filename, "w").write(code);
190     scope(exit) remove((cast(string) filename).toStringz);
191 
192     vfs.open(filename);
193     vfs.slice(filename).shouldEqual(code);
194 }
195 
196 /**
197  * Params:
198  *   T = type of the elements of the slice
199  *   auto_load = if the file is loaded from the filesystem if it isn't found.
200  *
201  * Returns: slice of the whole file */
202 T slice(T = string, Flag!"autoLoad" auto_load = Yes.autoLoad)(ref VirtualFileSystem vfs, FileName fname) @safe {
203     ubyte[] data;
204     bool found;
205 
206     if (auto code = fname in vfs.in_memory) {
207         data = (*code)[];
208         found = true;
209     } else if (auto mmf = fname in vfs.filesys) {
210         data = () @trusted { return cast(ubyte[]) (*mmf).file[0 .. (*mmf).size]; }();
211         found = true;
212     }
213 
214     if (!found) {
215         // error handling.
216         // Either try to automatically load the file or error out.
217         static if (auto_load) {
218             vfs.open(fname);
219             return vfs.slice!(T, No.autoLoad)(fname);
220         } else {
221             import std.exception;
222 
223             throw new Exception("File not found in VirtualFileSystem: " ~ fname);
224         }
225     }
226 
227     import std.utf : validate;
228 
229     static auto trustedCast(ubyte[] buf) @trusted {
230         return cast(T) buf;
231     }
232 
233     auto result = trustedCast(data);
234 
235     static if (isSomeString!T) {
236         validate(result);
237     }
238 
239     return result;
240 }
241 
242 // keep the clang specifics together in one place
243 
244 ///** Returns: a slice starting at the offset of the SourceLocation. */
245 T slice(T = string)(ref VirtualFileSystem vfs, SourceLocation sloc) @safe {
246     auto spell = sloc.spelling;
247     auto s = vfs.slice!(T, Yes.autoLoad)(cast(FileName) spell.file.name);
248     return s[spell.offset .. $];
249 }
250 
251 /** Returns: a slice of the text the SourceRanges refers to. */
252 T slice(T = string)(ref VirtualFileSystem vfs, SourceRange srange) @safe {
253     // sanity check
254     if (!srange.isValid) {
255         throw new Exception("Invalid SourceRange");
256     }
257 
258     auto start = srange.start.spelling;
259     auto end = srange.end.spelling;
260 
261     if (start.file.name != end.file.name) {
262         throw new Exception("Strange SourceRange, begin and end references different files: "
263                             ~ start.file.name ~ " " ~ end.file.name);
264     }
265 
266     auto s = vfs.slice!(T, Yes.autoLoad)(cast(FileName) start.file.name);
267     return s[start.offset .. end.offset];
268 }
269 
270 /**
271  * Trusted because:
272  *  -
273  */
274 CXUnsavedFile[] toClangFiles(ref VirtualFileSystem vfs) @trusted {
275     import std.algorithm : map;
276     import std.array : array;
277     import std..string : toStringz;
278 
279     return vfs.files.map!((a) {
280                           auto s = vfs.slice!(ubyte[], No.autoLoad)(cast(FileName) a);
281                           auto strz = (cast(char[]) a).toStringz;
282                           return CXUnsavedFile(strz, cast(char*) s.ptr, s.length);
283                           }).array();
284 }