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 }