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 }