1 /**
2  * Copyright: Copyright Jason White, 2016
3  * License:   MIT
4  * Authors:   Jason White
5  */
6 module button.resource;
7 import std.digest.digest : DigestType, isDigest;
8 
9 import std.array : Appender;
10 
11 /**
12  * A resource identifier.
13  */
14 alias ResourceId = string;
15 
16 /**
17  * A resource key must be unique.
18  */
19 struct ResourceKey
20 {
21     /**
22      * File path to the resource. To ensure uniqueness, this should never be
23      * changed after construction.
24      */
25     string path;
26 
27     /**
28      * Compares this key with another.
29      */
30     int opCmp()(const auto ref typeof(this) that) const pure nothrow
31     {
32         import std.algorithm.comparison : cmp;
33         return cmp(this.path, that.path);
34     }
35 }
36 
37 unittest
38 {
39     static assert(ResourceKey("abc") == ResourceKey("abc"));
40     static assert(ResourceKey("abc") < ResourceKey("abcd"));
41 }
42 
43 /**
44  * Compute the checksum of a file.
45  */
46 private DigestType!Hash digestFile(Hash)(string path)
47     if (isDigest!Hash)
48 {
49     import std.digest.digest : digest;
50     import io.file : SysException, File, FileFlags;
51     import io.range : byChunk;
52 
53     ubyte[4096] buf;
54 
55     try
56     {
57         return digest!Hash(File(path, FileFlags.readExisting).byChunk(buf));
58     }
59     catch (SysException e)
60     {
61         // This may fail if the given path is a directory. The path could have
62         // also been deleted.
63         return typeof(return).init;
64     }
65 }
66 
67 /**
68  * A representation of a file on the disk.
69  *
70  * TODO: Support directories as well as files.
71  */
72 struct Resource
73 {
74     import std.datetime : SysTime;
75     import std.digest.digest : DigestType;
76     import std.digest.sha : SHA256;
77 
78     /**
79      * Digest to use to determine changes.
80      */
81     alias Digest = SHA256;
82 
83     enum Status
84     {
85         unknown  = SysTime.max,
86         notFound = SysTime.min,
87     }
88 
89     /**
90      * File path to the resource. To ensure uniqueness, this should never be
91      * changed after construction.
92      */
93     ResourceId path;
94 
95     /**
96      * Last time the file was modified.
97      */
98     SysTime lastModified = Status.unknown;
99 
100     /**
101      * Checksum of the file.
102      *
103      * TODO: If this is a directory, checksum the sorted list of its contents.
104      */
105     DigestType!Digest checksum;
106 
107     this(ResourceId path, SysTime lastModified = Status.unknown,
108             const(ubyte[]) checksum = []) pure
109     {
110         import std.algorithm.comparison : min;
111 
112         this.path = path;
113         this.lastModified = lastModified;
114 
115         // The only times the length will be different are:
116         //  - The database is corrupt
117         //  - The digest length changed
118         // In either case, it doesn't matter. If the checksum changes it will
119         // simply be recomputed and order will once again be restored in the
120         // realm.
121         immutable bytes = min(this.checksum.length, checksum.length);
122         this.checksum[0 .. bytes] = checksum[0 .. bytes];
123     }
124 
125     /**
126      * Returns a string representation of this resource. This is just the path
127      * to the resource.
128      */
129     string toString() const pure nothrow
130     {
131         return path;
132     }
133 
134     /**
135      * Returns a short string representation of the path.
136      */
137     @property string toShortString() const pure nothrow
138     {
139         import std.path : baseName;
140         return path.baseName;
141     }
142 
143     /**
144      * Returns the unique identifier for this vertex.
145      */
146     @property inout(ResourceId) identifier() inout pure nothrow
147     {
148         return path;
149     }
150 
151     /**
152      * Compares the file path of this resource with another.
153      */
154     int opCmp()(const auto ref Resource rhs) const pure
155     {
156         import std.path : filenameCmp;
157         return filenameCmp(this.path, rhs.path);
158     }
159 
160     /// Ditto
161     bool opEquals()(const auto ref Resource rhs) const pure
162     {
163         return opCmp(rhs) == 0;
164     }
165 
166     unittest
167     {
168         assert(Resource("a") < Resource("b"));
169         assert(Resource("b") > Resource("a"));
170 
171         assert(Resource("test", SysTime(1)) == Resource("test", SysTime(1)));
172         assert(Resource("test", SysTime(1)) == Resource("test", SysTime(2)));
173     }
174 
175     /**
176      * Updates the last modified time and checksum of this resource. Returns
177      * true if anything changed.
178      *
179      * Note that the checksum is not recomputed if the modification time is the
180      * same.
181      */
182     bool update()
183     {
184         import std.file : timeLastModified, FileException;
185 
186         immutable lastModified = timeLastModified(path, Status.notFound);
187 
188         if (lastModified != this.lastModified)
189         {
190             import std.digest.md;
191             this.lastModified = lastModified;
192 
193             if (lastModified != Status.notFound)
194             {
195                 auto checksum = digestFile!Digest(path);
196                 if (checksum != this.checksum)
197                 {
198                     this.checksum = checksum;
199                     return true;
200                 }
201 
202                 // Checksum didn't change.
203                 return false;
204             }
205 
206             return true;
207         }
208 
209         return false;
210     }
211 
212     /**
213      * Returns true if the status of this resource is known.
214      */
215     @property bool statusKnown() const pure nothrow
216     {
217         return lastModified != Status.unknown;
218     }
219 
220     /**
221      * Deletes the resource from disk.
222      */
223     void remove(bool dryRun) nothrow
224     {
225         import std.file : unlink = remove, isFile;
226         import io;
227 
228         // Only delete this file if we know about it. This helps prevent the
229         // build system from haphazardly deleting files that were added to the
230         // build description but never output by a task.
231         if (!statusKnown)
232             return;
233 
234         // TODO: Use rmdir instead if this is a directory.
235 
236         if (!dryRun)
237         {
238             try
239             {
240                 unlink(path);
241             }
242             catch (Exception e)
243             {
244             }
245         }
246 
247         lastModified = Status.notFound;
248     }
249 }
250 
251 /**
252  * Normalizes a resource path while trying to make it relative to the buildRoot.
253  * If it cannot be done, the path is made absolute.
254  *
255  * Params:
256  *     buildRoot = The root directory of the build. Probably always the current
257  *                 working directory.
258  *     taskDir   = The working directory of the task this is for. The path is
259  *                 normalized relative to this directory.
260  *     path      = The path to be normalized.
261  */
262 string normPath(const(char)[] buildRoot, const(char)[] taskDir,
263         const(char)[] path) pure
264 {
265     import std.path : isAbsolute, buildNormalizedPath, pathSplitter,
266            filenameCmp, dirSeparator;
267     import std.algorithm.searching : skipOver;
268     import std.algorithm.iteration : joiner;
269     import std.array : array;
270     import std.utf : byChar;
271 
272     auto normalized = buildNormalizedPath(taskDir, path);
273 
274     // If the normalized path is absolute, get a relative path if the absolute
275     // path is inside the working directory. This is done instead of always
276     // getting a relative path because we don't want to get relative paths to
277     // directories like "/usr/include". If the build directory moves, absolute
278     // paths outside will become invalid.
279     if (isAbsolute(normalized) && buildRoot.length)
280     {
281         auto normPS  = pathSplitter(normalized);
282         auto buildPS = pathSplitter(buildRoot);
283 
284         alias pred = (a, b) => filenameCmp(a, b) == 0;
285 
286         if (skipOver!pred(normPS, &buildPS) && buildPS.empty)
287             return normPS.joiner(dirSeparator).byChar.array;
288     }
289 
290     return normalized;
291 }
292 
293 pure unittest
294 {
295     version (Posix)
296     {
297         assert(normPath("", "", "foo") == "foo");
298         assert(normPath("", "foo", "bar") == "foo/bar");
299 
300         assert(normPath("", "foo/../foo/.", "bar/../baz") == "foo/baz");
301 
302         assert(normPath("", "foo", "/usr/include/bar") == "/usr/include/bar");
303         assert(normPath("/usr", "foo", "/usr/bar") == "bar");
304         assert(normPath("/usr/include", "foo", "/usr/bar") == "/usr/bar");
305     }
306 }
307 
308 /**
309  * Output range of implicit resources.
310  *
311  * This is used to easily accumulate implicit resources while also normalizing
312  * their paths at the same time.
313  */
314 struct Resources
315 {
316     import std.array : Appender;
317     import std.range : isInputRange, ElementType;
318 
319     Appender!(Resource[]) resources;
320 
321     alias resources this;
322 
323     string buildDir;
324     string taskDir;
325 
326     this(string buildDir, string taskDir)
327     {
328         this.buildDir = buildDir;
329         this.taskDir = taskDir;
330     }
331 
332     void put(R)(R items)
333         if (isInputRange!R && is(ElementType!R : const(char)[]))
334     {
335         import std.range : empty, popFront, front;
336 
337         for (; !items.empty; items.popFront())
338             put(items.front);
339     }
340 
341     void put(const(char)[] item)
342     {
343         resources.put(Resource(normPath(buildDir, taskDir, item)));
344     }
345 
346     void put(R)(R items)
347         if (isInputRange!R && is(ElementType!R : Resource))
348     {
349         resources.put(items);
350     }
351 
352     void put(Resource item)
353     {
354         item.path = normPath(buildDir, taskDir, item.path);
355         resources.put(item);
356     }
357 }