1 module squiz_box.box;
2 
3 import squiz_box.priv;
4 import squiz_box.squiz;
5 import squiz_box.util;
6 
7 import std.datetime.systime;
8 import std.exception;
9 import std.range;
10 
11 public import squiz_box.box.tar;
12 public import squiz_box.box.zip;
13 
14 /// Static check that a type is an InputRange of ArchiveCreateEntry
15 template isCreateEntryRange(I)
16 {
17     import std.range : ElementType, isInputRange;
18 
19     enum isCreateEntryRange = isInputRange!I && is(ElementType!I : ArchiveCreateEntry);
20 }
21 
22 /// Static check that a type is an InputRange of ArchiveCreateEntry
23 template isCreateEntryForwardRange(I)
24 {
25     import std.range : ElementType, isForwardRange;
26 
27     enum isCreateEntryForwardRange = isForwardRange!I && is(ElementType!I : ArchiveCreateEntry);
28 }
29 
30 static assert(isCreateEntryRange!(ArchiveCreateEntry[]));
31 static assert(isCreateEntryForwardRange!(ArchiveCreateEntry[]));
32 
33 /// Static check that a type is an InputRange of ArchiveExtractEntry
34 template isExtractEntryRange(I)
35 {
36     import std.range : ElementType, isInputRange;
37 
38     enum isExtractEntryRange = isInputRange!I && is(ElementType!I : ArchiveExtractEntry);
39 }
40 
41 static assert(isExtractEntryRange!(ArchiveExtractEntry[]));
42 
43 /// Type of an archive entry
44 enum EntryType
45 {
46     /// Regular file
47     regular,
48     /// Directory
49     directory,
50     /// Symlink
51     symlink,
52 }
53 
54 /// Describe in what archive mode an entry is for.
55 enum EntryMode
56 {
57     /// Entry is used for archive creation
58     creation,
59     /// Entry is used for archive extraction
60     extraction,
61 }
62 
63 /// Common interface to archive entry.
64 /// Each type implementing ArchiveEntry is either for creation or for extraction, but not both.
65 /// Entries for archive creation implement ArchiveCreateEntry.
66 /// Entries for archive extraction implement ArchiveExtractionEntry.
67 ///
68 /// Instances of ArchiveCreateEntry are typically instanciated directly by the user or by thin helpers (e.g. FileArchiveEntry)
69 /// Instances of ArchiveExtractEntry are instantiated by the extraction algorithm and their final type is hidden.
70 interface ArchiveEntry
71 {
72     /// Tell whether the entry is used for creation (ArchiveCreateEntry)
73     /// or extraction (ArchiveExtractEntry)
74     @property EntryMode mode();
75 
76     /// The archive mode this entry is for.
77     /// The path of the entry within the archive.
78     /// Should always be a relative path, and never go backward (..)
79     @property string path();
80 
81     /// The type of entry (directory, file, symlink)
82     @property EntryType type();
83 
84     /// If symlink, this is the path pointed to by the link (relative to the symlink).
85     /// For directories and regular file, returns null.
86     @property string linkname();
87 
88     /// The size of the entry in bytes (returns zero for directories and symlink)
89     /// This is the size of uncompressed, extracted data.
90     @property ulong size();
91 
92     /// The timeLastModified of the entry
93     @property SysTime timeLastModified();
94 
95     /// The file attributes (as returned std.file.getLinkAttributes)
96     @property uint attributes();
97 
98     version (Posix)
99     {
100         /// The owner id of the entry
101         @property int ownerId();
102         /// The group id of the entry
103         @property int groupId();
104     }
105 
106     /// Check if the entry is a potential bomb.
107     /// A bomb is typically an entry that may overwrite other files
108     /// outside of the extraction directory.
109     /// isBomb will return true if the path is an absolute path
110     /// or a relative path going backwards (containing '..' after normalization).
111     /// In addition, a criteria of maximum allowed size can be provided (by default all sizes are accepted).
112     final bool isBomb(ulong allowedSz = ulong.max)
113     {
114         import std.path : buildNormalizedPath, isAbsolute;
115         import std.string : startsWith;
116 
117         if (allowedSz != ulong.max && size > allowedSz)
118             return true;
119 
120         const p = path;
121         return isAbsolute(p) || buildNormalizedPath(p).startsWith("..");
122     }
123 }
124 
125 /// Interface of ArchiveEntry used to create archives
126 interface ArchiveCreateEntry : ArchiveEntry
127 {
128     /// A byte range to the content of the entry.
129     /// Only relevant for regular files.
130     /// Other types of entry will return an empty range.
131     ByteRange byChunk(size_t chunkSize = defaultChunkSize);
132 
133     /// Helper function that read the complete data of the entry (using byChunk).
134     final ubyte[] readContent()
135     {
136         ubyte[] result = new ubyte[size];
137         size_t offset;
138 
139         foreach (chunk; byChunk())
140         {
141             assert(offset + chunk.length <= result.length);
142             result[offset .. offset + chunk.length] = chunk;
143             offset += chunk.length;
144         }
145 
146         return result;
147     }
148 }
149 
150 /// Interface of ArchiveEntry used for archive extraction
151 interface ArchiveExtractEntry : ArchiveEntry
152 {
153     /// The size occupied by the entry in the archive.
154     @property ulong entrySize();
155 
156     /// A byte range to the content of the entry.
157     /// Only relevant for regular files.
158     /// Other types of entry will return an empty range.
159     ByteRange byChunk(size_t chunkSize = defaultChunkSize);
160 
161     /// Helper function that read the complete data of the entry (using byChunk).
162     final ubyte[] readContent()
163     {
164         ubyte[] result = new ubyte[size];
165         size_t offset;
166 
167         foreach (chunk; byChunk())
168         {
169             assert(offset + chunk.length <= result.length);
170             result[offset .. offset + chunk.length] = chunk;
171             offset += chunk.length;
172         }
173 
174         return result;
175     }
176 
177     /// Extract the entry to a file under the given base directory
178     final void extractTo(string baseDirectory)
179     {
180         import std.file : exists, isDir, mkdirRecurse, setAttributes, setTimes;
181         import std.path : buildNormalizedPath, dirName;
182         import std.stdio : File;
183 
184         assert(exists(baseDirectory) && isDir(baseDirectory));
185 
186         enforce(
187             !this.isBomb,
188             "archive bomb detected! Extraction aborted (entry will extract to " ~
189                 this.path ~ " - outside of extraction directory).",
190         );
191 
192         const extractPath = buildNormalizedPath(baseDirectory, this.path);
193 
194         final switch (this.type)
195         {
196         case EntryType.directory:
197             mkdirRecurse(extractPath);
198             break;
199         case EntryType.symlink:
200             mkdirRecurse(dirName(extractPath));
201             version (Posix)
202             {
203                 import core.sys.posix.unistd : lchown;
204                 import std.file : symlink;
205                 import std.string : toStringz;
206 
207                 symlink(this.linkname, extractPath);
208                 lchown(toStringz(extractPath), this.ownerId, this.groupId);
209             }
210             else version (Windows)
211             {
212                 import core.sys.windows.winbase : CreateSymbolicLinkW, SYMBOLIC_LINK_FLAG_DIRECTORY;
213                 import core.sys.windows.windows : DWORD;
214                 import std.utf : toUTF16z;
215 
216                 DWORD flags;
217                 // if not exists (yet - we don't control order of extraction)
218                 // regular file is assumed
219                 if (exists(extractPath) && isDir(extractPath))
220                 {
221                     flags = SYMBOLIC_LINK_FLAG_DIRECTORY;
222                 }
223                 CreateSymbolicLinkW(extractPath.toUTF16z, this.linkname.toUTF16z, flags);
224             }
225             break;
226         case EntryType.regular:
227             mkdirRecurse(dirName(extractPath));
228 
229             writeBinaryFile(this.byChunk(), extractPath);
230 
231             setTimes(extractPath, Clock.currTime, this.timeLastModified);
232 
233             const attrs = this.attributes;
234             if (attrs != 0)
235             {
236                 setAttributes(extractPath, attrs);
237             }
238 
239             version (Posix)
240             {
241                 import core.sys.posix.unistd : chown;
242                 import std.string : toStringz;
243 
244                 chown(toStringz(extractPath), this.ownerId, this.groupId);
245             }
246             break;
247         }
248     }
249 }
250 
251 /// Create a file entry from a file path, relative to a base.
252 /// archiveBase must be a parent path from filename,
253 /// such as the the path of the entry is filename, relative to archiveBase.
254 /// prefix is prepended to the name of the file in the archive.
255 ArchiveCreateEntry fileEntry(string filename, string archiveBase, string prefix = null)
256 {
257     import std.path : absolutePath, buildNormalizedPath, relativePath;
258     import std.string : startsWith;
259 
260     const fn = buildNormalizedPath(absolutePath(filename));
261     const ab = buildNormalizedPath(absolutePath(archiveBase));
262 
263     enforce(fn.startsWith(ab), "archiveBase is not a parent of filename");
264 
265     auto pathInArchive = relativePath(fn, ab);
266     if (prefix)
267         pathInArchive = prefix ~ pathInArchive;
268 
269     return new FileArchiveEntry(filename, pathInArchive);
270 }
271 
272 /// File based implementation of ArchiveCreateEntry.
273 /// Used to create archives from files in the file system.
274 class FileArchiveEntry : ArchiveCreateEntry
275 {
276     string filePath;
277     string pathInArchive;
278 
279     this(string filePath, string pathInArchive)
280     {
281         import std.algorithm : canFind;
282         import std.file : exists;
283         import std.path : isAbsolute;
284 
285         enforce(exists(filePath), filePath ~ ": No such file or directory");
286         enforce(!isAbsolute(pathInArchive) && !pathInArchive.canFind(".."), "Potential archive bomb");
287 
288         if (!pathInArchive)
289         {
290             pathInArchive = filePath;
291         }
292         this.filePath = filePath;
293         this.pathInArchive = pathInArchive;
294     }
295 
296     @property EntryMode mode()
297     {
298         return EntryMode.creation;
299     }
300 
301     @property string path()
302     {
303         return pathInArchive;
304     }
305 
306     @property EntryType type()
307     {
308         import std.file : isDir, isSymlink;
309 
310         if (isDir(filePath))
311             return EntryType.directory;
312         if (isSymlink(filePath))
313             return EntryType.symlink;
314         return EntryType.regular;
315     }
316 
317     @property string linkname()
318     {
319         version (Posix)
320         {
321             import std.file : isSymlink, readLink;
322 
323             if (isSymlink(filePath))
324                 return readLink(filePath);
325         }
326         return null;
327     }
328 
329     @property ulong size()
330     {
331         import std.file : getSize;
332 
333         return getSize(filePath);
334     }
335 
336     @property SysTime timeLastModified()
337     {
338         import std.file : stdmtime = timeLastModified;
339 
340         return stdmtime(filePath);
341     }
342 
343     @property uint attributes()
344     {
345         import std.file : getAttributes;
346 
347         return getAttributes(filePath);
348     }
349 
350     version (Posix)
351     {
352         import core.sys.posix.sys.stat : stat_t, stat;
353 
354         stat_t statStruct;
355         bool statFetched;
356 
357         private void ensureStat()
358         {
359             import std.string : toStringz;
360 
361             if (!statFetched)
362             {
363                 errnoEnforce(
364                     stat(toStringz(filePath), &statStruct) == 0,
365                     "Could not retrieve file stat of " ~ filePath
366                 );
367                 statFetched = true;
368             }
369         }
370 
371         @property int ownerId()
372         {
373             ensureStat();
374 
375             return statStruct.st_uid;
376         }
377 
378         @property int groupId()
379         {
380             ensureStat();
381 
382             return statStruct.st_gid;
383         }
384     }
385 
386     ByteRange byChunk(size_t chunkSize)
387     {
388         import std.stdio : File;
389 
390         return inputRangeObject(ByChunkImpl(File(filePath, "rb"), chunkSize));
391     }
392 }