1 /**
2  * Read a file from disk and store it in memory.
3  *
4  * Copyright: Copyright (C) 1999-2023 by The D Language Foundation, All Rights Reserved
5  * License:   $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6  * Source:    $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/file_manager.d, _file_manager.d)
7  * Documentation:  https://dlang.org/phobos/dmd_file_manager.html
8  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/file_manager.d
9  */
10 
11 module dmd.file_manager;
12 
13 import dmd.root.stringtable : StringTable;
14 import dmd.root.file : File, Buffer;
15 import dmd.root.filename : FileName, isDirSeparator;
16 import dmd.root.string : toDString;
17 import dmd.globals;
18 import dmd.identifier;
19 import dmd.location;
20 
21 enum package_d  = "package." ~ mars_ext;
22 enum package_di = "package." ~ hdr_ext;
23 
24 /// Returns: whether a file with `name` is a special "package.d" module
25 bool isPackageFileName(scope FileName fileName) nothrow
26 {
27     return FileName.equals(fileName.name, package_d) || FileName.equals(fileName.name, package_di);
28 }
29 
30 // A path stack that allows one to go up and down the path using directory
31 // separators. `cur` is the current path, `up` goes up one path, `down` goes
32 // down one path. if `up` or `down` return false, there are no further paths.
33 private struct PathStack
34 {
35     private const(char)[] path;
36     private size_t pos;
37 
38     @safe @nogc nothrow pure:
39 
40     this(const(char)[] p)
41     {
42         path = p;
43         pos = p.length;
44     }
45 
46     const(char)[] cur()
47     {
48         return path[0 .. pos];
49     }
50 
51     bool up()
52     {
53         if (pos == 0)
54             return false;
55         while (--pos != 0)
56             if (isDirSeparator(path[pos]))
57                 return true;
58         return false;
59     }
60 
61     bool down()
62     {
63         if (pos == path.length)
64             return false;
65         while (++pos != path.length)
66             if (isDirSeparator(path[pos]))
67                 return true;
68         return false;
69     }
70 }
71 
72 final class FileManager
73 {
74     private StringTable!(const(ubyte)[]) files;
75     private StringTable!(bool) packageStatus;
76 
77     // check if the package path of the given path exists. The input path is
78     // expected to contain the full path to the module, so the parent
79     // directories of that path are checked.
80     private bool packageExists(const(char)[] p) nothrow
81     {
82         // step 1, look for the closest parent path that is cached
83         bool exists = true;
84         auto st = PathStack(p);
85         while (st.up) {
86             if (auto cached = packageStatus.lookup(st.cur)) {
87                 exists = cached.value;
88                 break;
89             }
90         }
91         // found a parent that is cached (or reached the end of the stack).
92         // step 2, traverse back up the stack storing either false if the
93         // parent doesn't exist, or the result of the `exists` call if it does.
94         while (st.down) {
95             if (!exists)
96                 packageStatus.insert(st.cur, false);
97             else
98                 exists = packageStatus.insert(st.cur, FileName.exists(st.cur) == 2).value;
99         }
100 
101         // at this point, exists should be the answer.
102         return exists;
103     }
104 
105     ///
106     public this () nothrow
107     {
108         this.files._init();
109         this.packageStatus._init();
110     }
111 
112 nothrow:
113     /********************************************
114     * Look for the source file if it's different from filename.
115     * Look for .di, .d, directory, and along global.path.
116     * Does not open the file.
117     * Params:
118     *      filename = as supplied by the user
119     *      path = path to look for filename
120     * Returns:
121     *      the found file name or
122     *      `null` if it is not different from filename.
123     */
124     const(char)[] lookForSourceFile(const char[] filename, const char*[] path)
125     {
126         //printf("lookForSourceFile(`%.*s`)\n", cast(int)filename.length, filename.ptr);
127         /* Search along path[] for .di file, then .d file.
128         */
129         // see if we should check for the module locally.
130         bool checkLocal = packageExists(filename);
131         const sdi = FileName.forceExt(filename, hdr_ext);
132         if (checkLocal && FileName.exists(sdi) == 1)
133             return sdi;
134         scope(exit) FileName.free(sdi.ptr);
135 
136         const sd = FileName.forceExt(filename, mars_ext);
137         // Special file name representing `stdin`, always assume its presence
138         if (sd == "__stdin.d")
139             return sd;
140         if (checkLocal && FileName.exists(sd) == 1)
141             return sd;
142         scope(exit) FileName.free(sd.ptr);
143 
144         if (checkLocal)
145         {
146             auto cached = packageStatus.lookup(filename);
147             if (!cached)
148                 cached = packageStatus.insert(filename, FileName.exists(filename) == 2);
149             if (cached.value)
150             {
151                 /* The filename exists and it's a directory.
152                  * Therefore, the result should be: filename/package.d
153                  * iff filename/package.d is a file
154                  */
155                 const ni = FileName.combine(filename, package_di);
156                 if (FileName.exists(ni) == 1)
157                     return ni;
158                 FileName.free(ni.ptr);
159 
160                 const n = FileName.combine(filename, package_d);
161                 if (FileName.exists(n) == 1)
162                     return n;
163                 FileName.free(n.ptr);
164             }
165         }
166 
167         if (FileName.absolute(filename))
168             return null;
169         if (!path.length)
170             return null;
171         foreach (entry; path)
172         {
173             const p = entry.toDString();
174 
175             const(char)[] n = FileName.combine(p, sdi);
176 
177             if (!packageExists(n)) {
178                 FileName.free(n.ptr);
179                 continue; // no need to check for anything else.
180             }
181             if (FileName.exists(n) == 1) {
182                 return n;
183             }
184             FileName.free(n.ptr);
185 
186             n = FileName.combine(p, sd);
187             if (FileName.exists(n) == 1) {
188                 return n;
189             }
190             FileName.free(n.ptr);
191 
192             const b = FileName.removeExt(filename);
193             n = FileName.combine(p, b);
194             FileName.free(b.ptr);
195 
196             scope(exit) FileName.free(n.ptr);
197 
198             // also cache this if we are looking for package.d[i]
199             auto cached = packageStatus.lookup(n);
200             if (!cached) {
201                 cached = packageStatus.insert(n, FileName.exists(n) == 2);
202             }
203 
204             if (cached.value)
205             {
206                 const n2i = FileName.combine(n, package_di);
207                 if (FileName.exists(n2i) == 1)
208                     return n2i;
209                 FileName.free(n2i.ptr);
210                 const n2 = FileName.combine(n, package_d);
211                 if (FileName.exists(n2) == 1) {
212                     return n2;
213                 }
214                 FileName.free(n2.ptr);
215             }
216         }
217 
218         /* ImportC: No D modules found, now search along path[] for .i file, then .c file.
219          */
220         const si = FileName.forceExt(filename, i_ext);
221         if (FileName.exists(si) == 1)
222             return si;
223         scope(exit) FileName.free(si.ptr);
224 
225         const sc = FileName.forceExt(filename, c_ext);
226         if (FileName.exists(sc) == 1)
227             return sc;
228         scope(exit) FileName.free(sc.ptr);
229         foreach (entry; path)
230         {
231             const p = entry.toDString();
232 
233             const(char)[] n = FileName.combine(p, si);
234             if (FileName.exists(n) == 1) {
235                 return n;
236             }
237             FileName.free(n.ptr);
238 
239             n = FileName.combine(p, sc);
240             if (FileName.exists(n) == 1) {
241                 return n;
242             }
243             FileName.free(n.ptr);
244         }
245         return null;
246     }
247 
248     /**
249      * Looks up the given filename from the internal file buffer table.
250      * If the file does not already exist within the table, it will be read from the filesystem.
251      * If it has been read before,
252      *
253      * Returns: the loaded source file if it was found in memory,
254      *      otherwise `null`
255      */
256     const(ubyte)[] lookup(FileName filename)
257     {
258         const name = filename.toString;
259         if (auto val = files.lookup(name))
260             return val.value;
261 
262         if (name == "__stdin.d")
263         {
264             auto buffer = readFromStdin().extractSlice();
265             if (this.files.insert(name, buffer) is null)
266                 assert(0, "stdin: Insert after lookup failure should never return `null`");
267             return buffer;
268         }
269 
270         if (FileName.exists(name) != 1)
271             return null;
272 
273         auto readResult = File.read(name);
274         if (!readResult.success)
275             return null;
276 
277         auto fb = readResult.extractSlice();
278         if (files.insert(name, fb) is null)
279             assert(0, "Insert after lookup failure should never return `null`");
280 
281         return fb;
282     }
283 
284     /**
285      * Looks up the given filename from the internal file buffer table, and returns the lines within the file.
286      * If the file does not already exist within the table, it will be read from the filesystem.
287      * If it has been read before,
288      *
289      * Returns: the loaded source file if it was found in memory,
290      *      otherwise `null`
291      */
292     const(char)[][] getLines(FileName file)
293     {
294         const(char)[][] lines;
295         if (const buffer = lookup(file))
296         {
297             const slice = buffer;
298             size_t start, end;
299             for (auto i = 0; i < slice.length; i++)
300             {
301                 const c = slice[i];
302                 if (c == '\n' || c == '\r')
303                 {
304                     if (i != 0)
305                     {
306                         end = i;
307                         // Appending lines one at a time will certainly be slow
308                         lines ~= cast(const(char)[])slice[start .. end];
309                     }
310                     // Check for Windows-style CRLF newlines
311                     if (c == '\r')
312                     {
313                         if (slice.length > i + 1 && slice[i + 1] == '\n')
314                         {
315                             // This is a CRLF sequence, skip over two characters
316                             start = i + 2;
317                             i++;
318                         }
319                         else
320                         {
321                             // Just a CR sequence
322                             start = i + 1;
323                         }
324                     }
325                     else
326                     {
327                         // The next line should start after the LF sequence
328                         start = i + 1;
329                     }
330                 }
331             }
332 
333             if (slice[$ - 1] != '\r' && slice[$ - 1] != '\n')
334             {
335                 end = slice.length;
336                 lines ~= cast(const(char)[])slice[start .. end];
337             }
338         }
339 
340         return lines;
341     }
342 
343     /**
344      * Adds the contents of a file to the table.
345      * Params:
346      *  filename = name of the file
347      *  buffer = contents of the file
348      * Returns:
349      *  the buffer added, or null
350      */
351     const(ubyte)[] add(FileName filename, const(ubyte)[] buffer)
352     {
353         auto val = files.insert(filename.toString, buffer);
354         return val == null ? null : val.value;
355     }
356 }
357 
358 private Buffer readFromStdin() nothrow
359 {
360     import core.stdc.stdio;
361     import dmd.errors;
362     import dmd.root.rmem;
363 
364     enum bufIncrement = 128 * 1024;
365     size_t pos = 0;
366     size_t sz = bufIncrement;
367 
368     ubyte* buffer = null;
369     for (;;)
370     {
371         buffer = cast(ubyte*)mem.xrealloc(buffer, sz + 4); // +2 for sentinel and +2 for lexer
372 
373         // Fill up buffer
374         do
375         {
376             assert(sz > pos);
377             size_t rlen = fread(buffer + pos, 1, sz - pos, stdin);
378             pos += rlen;
379             if (ferror(stdin))
380             {
381                 import core.stdc.errno;
382                 error(Loc.initial, "cannot read from stdin, errno = %d", errno);
383                 fatal();
384             }
385             if (feof(stdin))
386             {
387                 // We're done
388                 assert(pos < sz + 2);
389                 buffer[pos .. pos + 4] = '\0';
390                 return Buffer(buffer[0 .. pos]);
391             }
392         } while (pos < sz);
393 
394         // Buffer full, expand
395         sz += bufIncrement;
396     }
397 
398     assert(0);
399 }