Line data Source code
1 : /* Copyright (C) 2021 Wildfire Games.
2 : *
3 : * Permission is hereby granted, free of charge, to any person obtaining
4 : * a copy of this software and associated documentation files (the
5 : * "Software"), to deal in the Software without restriction, including
6 : * without limitation the rights to use, copy, modify, merge, publish,
7 : * distribute, sublicense, and/or sell copies of the Software, and to
8 : * permit persons to whom the Software is furnished to do so, subject to
9 : * the following conditions:
10 : *
11 : * The above copyright notice and this permission notice shall be included
12 : * in all copies or substantial portions of the Software.
13 : *
14 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 : * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 : * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 : * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 : * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 : * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 : * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 : */
22 :
23 : /*
24 : * archive backend for Zip files.
25 : */
26 :
27 : #include "precompiled.h"
28 : #include "lib/file/archive/archive_zip.h"
29 :
30 : #include <time.h>
31 : #include <limits>
32 :
33 : #include "lib/utf8.h"
34 : #include "lib/bits.h"
35 : #include "lib/byte_order.h"
36 : #include "lib/allocators/pool.h"
37 : #include "lib/sysdep/filesystem.h"
38 : #include "lib/file/archive/archive.h"
39 : #include "lib/file/archive/codec_zlib.h"
40 : #include "lib/file/archive/stream.h"
41 : #include "lib/file/file.h"
42 : #include "lib/file/io/io.h"
43 :
44 : //-----------------------------------------------------------------------------
45 : // timestamp conversion: DOS FAT <-> Unix time_t
46 : //-----------------------------------------------------------------------------
47 :
48 1 : static time_t time_t_from_FAT(u32 fat_timedate)
49 : {
50 1 : const u32 fat_time = bits(fat_timedate, 0, 15);
51 1 : const u32 fat_date = bits(fat_timedate, 16, 31);
52 :
53 : struct tm t; // struct tm format:
54 1 : t.tm_sec = bits(fat_time, 0,4) * 2; // [0,59]
55 1 : t.tm_min = bits(fat_time, 5,10); // [0,59]
56 1 : t.tm_hour = bits(fat_time, 11,15); // [0,23]
57 1 : t.tm_mday = bits(fat_date, 0,4); // [1,31]
58 1 : t.tm_mon = bits(fat_date, 5,8) - 1; // [0,11]
59 1 : t.tm_year = bits(fat_date, 9,15) + 80; // since 1900
60 1 : t.tm_isdst = -1; // unknown - let libc determine
61 :
62 : // otherwise: totally bogus, and at the limit of 32-bit time_t
63 1 : ENSURE(t.tm_year < 138);
64 :
65 1 : time_t ret = mktime(&t);
66 1 : ENSURE(ret != (time_t)-1); // mktime shouldn't fail
67 1 : return ret;
68 : }
69 :
70 :
71 0 : static u32 FAT_from_time_t(time_t time)
72 : {
73 : // (values are adjusted for DST)
74 0 : struct tm* t = localtime(&time);
75 :
76 0 : const u16 fat_time = u16(
77 0 : (t->tm_sec/2) | // 5
78 0 : (u16(t->tm_min) << 5) | // 6
79 0 : (u16(t->tm_hour) << 11) // 5
80 : );
81 :
82 0 : const u16 fat_date = u16(
83 0 : (t->tm_mday) | // 5
84 0 : (u16(t->tm_mon+1) << 5) | // 4
85 0 : (u16(t->tm_year-80) << 9) // 7
86 : );
87 :
88 0 : u32 fat_timedate = u32_from_u16(fat_date, fat_time);
89 0 : return fat_timedate;
90 : }
91 :
92 :
93 : //-----------------------------------------------------------------------------
94 : // Zip archive definitions
95 : //-----------------------------------------------------------------------------
96 :
97 : static const u32 cdfh_magic = FOURCC_LE('P','K','\1','\2');
98 : static const u32 lfh_magic = FOURCC_LE('P','K','\3','\4');
99 : static const u32 ecdr_magic = FOURCC_LE('P','K','\5','\6');
100 :
101 : enum ZipMethod
102 : {
103 : ZIP_METHOD_NONE = 0,
104 : ZIP_METHOD_DEFLATE = 8
105 : };
106 :
107 : #pragma pack(push, 1)
108 :
109 : class LFH
110 : {
111 : public:
112 0 : void Init(const CFileInfo& fileInfo, off_t csize, ZipMethod method, u32 checksum, const Path& pathname)
113 : {
114 0 : const std::string pathnameUTF8 = utf8_from_wstring(pathname.string());
115 0 : const size_t pathnameSize = pathnameUTF8.length();
116 :
117 0 : m_magic = lfh_magic;
118 0 : m_x1 = to_le16(0);
119 0 : m_flags = to_le16(0);
120 0 : m_method = to_le16(u16_from_larger(method));
121 0 : m_fat_mtime = to_le32(FAT_from_time_t(fileInfo.MTime()));
122 0 : m_crc = to_le32(checksum);
123 0 : m_csize = to_le32(u32_from_larger(csize));
124 0 : m_usize = to_le32(u32_from_larger(fileInfo.Size()));
125 0 : m_fn_len = to_le16(u16_from_larger(pathnameSize));
126 0 : m_e_len = to_le16(0);
127 :
128 0 : memcpy((char*)this + sizeof(LFH), pathnameUTF8.c_str(), pathnameSize);
129 0 : }
130 :
131 0 : size_t Size() const
132 : {
133 0 : ENSURE(m_magic == lfh_magic);
134 0 : size_t size = sizeof(LFH);
135 0 : size += read_le16(&m_fn_len);
136 0 : size += read_le16(&m_e_len);
137 : // note: LFH doesn't have a comment field!
138 0 : return size;
139 : }
140 :
141 : private:
142 : u32 m_magic;
143 : u16 m_x1; // version needed
144 : u16 m_flags;
145 : u16 m_method;
146 : u32 m_fat_mtime; // last modified time (DOS FAT format)
147 : u32 m_crc;
148 : u32 m_csize;
149 : u32 m_usize;
150 : u16 m_fn_len;
151 : u16 m_e_len;
152 : };
153 :
154 : cassert(sizeof(LFH) == 30);
155 :
156 :
157 : class CDFH
158 : {
159 : public:
160 0 : void Init(const CFileInfo& fileInfo, off_t ofs, off_t csize, ZipMethod method, u32 checksum, const Path& pathname, size_t slack)
161 : {
162 0 : const std::string pathnameUTF8 = utf8_from_wstring(pathname.string());
163 0 : const size_t pathnameLength = pathnameUTF8.length();
164 :
165 0 : m_magic = cdfh_magic;
166 0 : m_x1 = to_le32(0);
167 0 : m_flags = to_le16(0);
168 0 : m_method = to_le16(u16_from_larger(method));
169 0 : m_fat_mtime = to_le32(FAT_from_time_t(fileInfo.MTime()));
170 0 : m_crc = to_le32(checksum);
171 0 : m_csize = to_le32(u32_from_larger(csize));
172 0 : m_usize = to_le32(u32_from_larger(fileInfo.Size()));
173 0 : m_fn_len = to_le16(u16_from_larger(pathnameLength));
174 0 : m_e_len = to_le16(0);
175 0 : m_c_len = to_le16(u16_from_larger((size_t)slack));
176 0 : m_x2 = to_le32(0);
177 0 : m_x3 = to_le32(0);
178 0 : m_lfh_ofs = to_le32(u32_from_larger(ofs));
179 :
180 0 : memcpy((char*)this + sizeof(CDFH), pathnameUTF8.c_str(), pathnameLength);
181 0 : }
182 :
183 1 : Path Pathname() const
184 : {
185 1 : const size_t length = (size_t)read_le16(&m_fn_len);
186 1 : const char* pathname = (const char*)this + sizeof(CDFH); // not 0-terminated!
187 1 : return Path(std::string(pathname, length));
188 : }
189 :
190 1 : off_t HeaderOffset() const
191 : {
192 1 : return read_le32(&m_lfh_ofs);
193 : }
194 :
195 1 : off_t USize() const
196 : {
197 1 : return (off_t)read_le32(&m_usize);
198 : }
199 :
200 1 : off_t CSize() const
201 : {
202 1 : return (off_t)read_le32(&m_csize);
203 : }
204 :
205 1 : ZipMethod Method() const
206 : {
207 1 : return (ZipMethod)read_le16(&m_method);
208 : }
209 :
210 1 : u32 Checksum() const
211 : {
212 1 : return read_le32(&m_crc);
213 : }
214 :
215 1 : time_t MTime() const
216 : {
217 1 : const u32 fat_mtime = read_le32(&m_fat_mtime);
218 1 : return time_t_from_FAT(fat_mtime);
219 : }
220 :
221 1 : size_t Size() const
222 : {
223 1 : size_t size = sizeof(CDFH);
224 1 : size += read_le16(&m_fn_len);
225 1 : size += read_le16(&m_e_len);
226 1 : size += read_le16(&m_c_len);
227 1 : return size;
228 : }
229 :
230 : private:
231 : u32 m_magic;
232 : u32 m_x1; // versions
233 : u16 m_flags;
234 : u16 m_method;
235 : u32 m_fat_mtime; // last modified time (DOS FAT format)
236 : u32 m_crc;
237 : u32 m_csize;
238 : u32 m_usize;
239 : u16 m_fn_len;
240 : u16 m_e_len;
241 : u16 m_c_len;
242 : u32 m_x2; // spanning
243 : u32 m_x3; // attributes
244 : u32 m_lfh_ofs;
245 : };
246 :
247 : cassert(sizeof(CDFH) == 46);
248 :
249 :
250 : class ECDR
251 : {
252 : public:
253 0 : void Init(size_t cd_numEntries, off_t cd_ofs, size_t cd_size)
254 : {
255 0 : m_magic = ecdr_magic;
256 0 : m_diskNum = to_le16(0);
257 0 : m_cd_diskNum = to_le16(0);
258 0 : m_cd_numEntriesOnDisk = to_le16(u16_from_larger(cd_numEntries));
259 0 : m_cd_numEntries = m_cd_numEntriesOnDisk;
260 0 : m_cd_size = to_le32(u32_from_larger(cd_size));
261 0 : m_cd_ofs = to_le32(u32_from_larger(cd_ofs));
262 0 : m_comment_len = to_le16(0);
263 0 : }
264 :
265 1 : void Decompose(size_t& cd_numEntries, off_t& cd_ofs, size_t& cd_size) const
266 : {
267 1 : cd_numEntries = (size_t)read_le16(&m_cd_numEntries);
268 1 : cd_ofs = (off_t)read_le32(&m_cd_ofs);
269 1 : cd_size = (size_t)read_le32(&m_cd_size);
270 1 : }
271 :
272 1 : off_t GetCommentLength() const
273 : {
274 1 : return static_cast<off_t>(read_le16(&m_comment_len));
275 : }
276 :
277 : private:
278 : u32 m_magic;
279 : u16 m_diskNum;
280 : u16 m_cd_diskNum;
281 : u16 m_cd_numEntriesOnDisk;
282 : u16 m_cd_numEntries;
283 : u32 m_cd_size;
284 : u32 m_cd_ofs;
285 : u16 m_comment_len;
286 : };
287 :
288 : cassert(sizeof(ECDR) == 22);
289 :
290 : #pragma pack(pop)
291 :
292 :
293 : //-----------------------------------------------------------------------------
294 : // ArchiveFile_Zip
295 : //-----------------------------------------------------------------------------
296 :
297 1 : class ArchiveFile_Zip : public IArchiveFile
298 : {
299 : public:
300 1 : ArchiveFile_Zip(const PFile& file, off_t ofs, off_t csize, u32 checksum, ZipMethod method)
301 1 : : m_file(file), m_ofs(ofs)
302 : , m_csize(csize), m_checksum(checksum), m_method((u16)method)
303 1 : , m_flags(NeedsFixup)
304 : {
305 1 : }
306 :
307 0 : virtual size_t Precedence() const
308 : {
309 0 : return 2u;
310 : }
311 :
312 0 : virtual wchar_t LocationCode() const
313 : {
314 0 : return 'A';
315 : }
316 :
317 0 : virtual OsPath Path() const
318 : {
319 0 : return m_file->Pathname();
320 : }
321 :
322 0 : virtual Status Load(const OsPath& UNUSED(name), const std::shared_ptr<u8>& buf, size_t size) const
323 : {
324 0 : AdjustOffset();
325 :
326 0 : PICodec codec;
327 0 : switch(m_method)
328 : {
329 0 : case ZIP_METHOD_NONE:
330 0 : codec = CreateCodec_ZLibNone();
331 0 : break;
332 0 : case ZIP_METHOD_DEFLATE:
333 0 : codec = CreateDecompressor_ZLibDeflate();
334 0 : break;
335 0 : default:
336 0 : WARN_RETURN(ERR::ARCHIVE_UNKNOWN_METHOD);
337 : }
338 :
339 0 : Stream stream(codec);
340 0 : stream.SetOutputBuffer(buf.get(), size);
341 0 : io::Operation op(*m_file.get(), 0, m_csize, m_ofs);
342 0 : StreamFeeder streamFeeder(stream);
343 0 : RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder));
344 0 : RETURN_STATUS_IF_ERR(stream.Finish());
345 : #if CODEC_COMPUTE_CHECKSUM
346 0 : ENSURE(m_checksum == stream.Checksum());
347 : #endif
348 :
349 0 : return INFO::OK;
350 : }
351 :
352 : private:
353 : enum Flags
354 : {
355 : // indicates m_ofs points to a "local file header" instead of
356 : // the file data. a fixup routine is called when reading the file;
357 : // it skips past the LFH and clears this flag.
358 : // this is somewhat of a hack, but vital to archive open performance.
359 : // without it, we'd have to scan through the entire archive file,
360 : // which can take *seconds*.
361 : // (we cannot use the information in CDFH, because its 'extra' field
362 : // has been observed to differ from that of the LFH)
363 : // since we read the LFH right before the rest of the file, the block
364 : // cache will absorb the IO cost.
365 : NeedsFixup = 1
366 : };
367 :
368 : struct LFH_Copier
369 : {
370 0 : LFH_Copier(u8* lfh_dst, size_t lfh_bytes_remaining)
371 0 : : lfh_dst(lfh_dst), lfh_bytes_remaining(lfh_bytes_remaining)
372 : {
373 0 : }
374 :
375 : // this code grabs an LFH struct from file block(s) that are
376 : // passed to the callback. usually, one call copies the whole thing,
377 : // but the LFH may straddle a block boundary.
378 : //
379 : // rationale: this allows using temp buffers for zip_fixup_lfh,
380 : // which avoids involving the file buffer manager and thus
381 : // avoids cluttering the trace and cache contents.
382 0 : Status operator()(const u8* block, size_t size) const
383 : {
384 0 : ENSURE(size <= lfh_bytes_remaining);
385 0 : memcpy(lfh_dst, block, size);
386 0 : lfh_dst += size;
387 0 : lfh_bytes_remaining -= size;
388 :
389 0 : return INFO::OK;
390 : }
391 :
392 : mutable u8* lfh_dst;
393 : mutable size_t lfh_bytes_remaining;
394 : };
395 :
396 : /**
397 : * fix up m_ofs (adjust it to point to cdata instead of the LFH).
398 : *
399 : * note: we cannot use CDFH filename and extra field lengths to skip
400 : * past LFH since that may not mirror CDFH (has happened).
401 : *
402 : * this is called at file-open time instead of while mounting to
403 : * reduce seeks: since reading the file will typically follow, the
404 : * block cache entirely absorbs the IO cost.
405 : **/
406 0 : void AdjustOffset() const
407 : {
408 0 : if(!(m_flags & NeedsFixup))
409 0 : return;
410 0 : m_flags &= ~NeedsFixup;
411 :
412 : // performance note: this ends up reading one file block, which is
413 : // only in the block cache if the file starts in the same block as a
414 : // previously read file (i.e. both are small).
415 : LFH lfh;
416 0 : io::Operation op(*m_file.get(), 0, sizeof(LFH), m_ofs);
417 0 : if(io::Run(op, io::Parameters(), LFH_Copier((u8*)&lfh, sizeof(LFH))) == INFO::OK)
418 0 : m_ofs += (off_t)lfh.Size();
419 : }
420 :
421 : PFile m_file;
422 :
423 : // all relevant LFH/CDFH fields not covered by CFileInfo
424 : mutable off_t m_ofs;
425 : off_t m_csize;
426 : u32 m_checksum;
427 : u16 m_method;
428 : mutable u16 m_flags;
429 : };
430 :
431 :
432 : //-----------------------------------------------------------------------------
433 : // ArchiveReader_Zip
434 : //-----------------------------------------------------------------------------
435 :
436 2 : class ArchiveReader_Zip : public IArchiveReader
437 : {
438 : public:
439 1 : ArchiveReader_Zip(const OsPath& pathname)
440 1 : : m_file(new File(pathname, O_RDONLY))
441 : {
442 2 : CFileInfo fileInfo;
443 1 : GetFileInfo(pathname, &fileInfo);
444 1 : m_fileSize = fileInfo.Size();
445 1 : const size_t minFileSize = sizeof(LFH)+sizeof(CDFH)+sizeof(ECDR);
446 1 : ENSURE(m_fileSize >= off_t(minFileSize));
447 1 : }
448 :
449 1 : virtual Status ReadEntries(ArchiveEntryCallback cb, uintptr_t cbData)
450 : {
451 : // locate and read Central Directory
452 1 : off_t cd_ofs = 0;
453 1 : size_t cd_numEntries = 0;
454 1 : size_t cd_size = 0;
455 1 : RETURN_STATUS_IF_ERR(LocateCentralDirectory(m_file, m_fileSize, cd_ofs, cd_numEntries, cd_size));
456 2 : io::BufferPtr buf(io::Allocate(cd_size));
457 :
458 1 : io::Operation op(*m_file.get(), buf.get(), cd_size, cd_ofs);
459 1 : RETURN_STATUS_IF_ERR(io::Run(op));
460 :
461 : // iterate over Central Directory
462 1 : const u8* pos = buf.get();
463 2 : for(size_t i = 0; i < cd_numEntries; i++)
464 : {
465 : // scan for next CDFH
466 1 : CDFH* cdfh = (CDFH*)FindRecord(buf.get(), cd_size, pos, cdfh_magic, sizeof(CDFH));
467 1 : if(!cdfh)
468 0 : WARN_RETURN(ERR::CORRUPTED);
469 :
470 2 : const Path relativePathname(cdfh->Pathname());
471 1 : if(!relativePathname.IsDirectory())
472 : {
473 2 : const OsPath name = relativePathname.Filename();
474 2 : CFileInfo fileInfo(name, cdfh->USize(), cdfh->MTime());
475 2 : std::shared_ptr<ArchiveFile_Zip> archiveFile = std::make_shared<ArchiveFile_Zip>(m_file, cdfh->HeaderOffset(), cdfh->CSize(), cdfh->Checksum(), cdfh->Method());
476 1 : cb(relativePathname, fileInfo, archiveFile, cbData);
477 : }
478 :
479 1 : pos += cdfh->Size();
480 : }
481 :
482 1 : return INFO::OK;
483 : }
484 :
485 : private:
486 : /**
487 : * Scan buffer for a Zip file record.
488 : *
489 : * @param buf
490 : * @param size
491 : * @param start position within buffer
492 : * @param magic signature of record
493 : * @param recordSize size of record (including signature)
494 : * @return pointer to record within buffer or 0 if not found.
495 : **/
496 1 : static const u8* FindRecord(const u8* buf, size_t size, const u8* start, u32 magic, size_t recordSize)
497 : {
498 : // (don't use <start> as the counter - otherwise we can't tell if
499 : // scanning within the buffer was necessary.)
500 1 : for(const u8* p = start; p <= buf+size-recordSize; p++)
501 : {
502 : // found it
503 1 : if(*(u32*)p == magic)
504 : {
505 1 : ENSURE(p == start); // otherwise, the archive is a bit broken
506 1 : return p;
507 : }
508 : }
509 :
510 : // passed EOF, didn't find it.
511 : // note: do not warn - this happens in the initial ECDR search at
512 : // EOF if the archive contains a comment field.
513 0 : return 0;
514 : }
515 :
516 : // search for ECDR in the last <maxScanSize> bytes of the file.
517 : // if found, fill <dst_ecdr> with a copy of the (little-endian) ECDR and
518 : // return INFO::OK, otherwise IO error or ERR::CORRUPTED.
519 1 : static Status ScanForEcdr(const PFile& file, off_t fileSize, u8* buf, size_t maxScanSize, size_t& cd_numEntries, off_t& cd_ofs, size_t& cd_size)
520 : {
521 : // don't scan more than the entire file
522 1 : const size_t scanSize = std::min(maxScanSize, size_t(fileSize));
523 :
524 : // read desired chunk of file into memory
525 1 : const off_t ofs = fileSize - off_t(scanSize);
526 1 : io::Operation op(*file.get(), buf, scanSize, ofs);
527 1 : RETURN_STATUS_IF_ERR(io::Run(op));
528 :
529 : // Scanning for ECDR first assumes no comment exists
530 : // (standard case), so ECDR structure exists right at
531 : // end of file
532 1 : off_t offsetInBlock = scanSize - sizeof(ECDR);
533 1 : const ECDR* ecdr = nullptr;
534 :
535 63 : for (off_t commentSize = 0; commentSize <= offsetInBlock && !ecdr; ++commentSize)
536 : {
537 63 : const u8 *pECDRTest = buf + offsetInBlock - commentSize;
538 63 : if (*reinterpret_cast<const u32*>(pECDRTest) == ecdr_magic)
539 : {
540 : // Signature matches, test whether comment
541 : // fills up the whole space following the
542 : // ECDR
543 1 : ecdr = reinterpret_cast<const ECDR*>(pECDRTest);
544 1 : if (commentSize != ecdr->GetCommentLength())
545 : {
546 : // Signature matches but there is some other data between
547 : // header, comment and EOF. There are three possibilities
548 : // for this:
549 : // 1) Header file format and size differ from what we expect
550 : // 2) File has been truncated
551 : // 3) The magic id occurs inside a zip comment
552 0 : ecdr = nullptr;
553 : }
554 : else
555 : {
556 : // Seems like a valid archive header before an archive-level
557 : // comment
558 1 : break;
559 : }
560 : }
561 : }
562 :
563 1 : if(!ecdr)
564 0 : return INFO::CANNOT_HANDLE;
565 :
566 1 : ecdr->Decompose(cd_numEntries, cd_ofs, cd_size);
567 1 : return INFO::OK;
568 : }
569 :
570 1 : static Status LocateCentralDirectory(const PFile& file, off_t fileSize, off_t& cd_ofs, size_t& cd_numEntries, size_t& cd_size)
571 : {
572 1 : const size_t maxScanSize = 66000u; // see below
573 2 : io::BufferPtr buf(io::Allocate(maxScanSize));
574 1 : Status ret = ScanForEcdr(file, fileSize, static_cast<u8*>(buf.get()), maxScanSize, cd_numEntries, cd_ofs, cd_size);
575 1 : if(ret == INFO::OK)
576 1 : return INFO::OK;
577 :
578 0 : io::Operation op(*file.get(), buf.get(), sizeof(LFH));
579 0 : RETURN_STATUS_IF_ERR(io::Run(op));
580 : // the Zip file has an LFH but lacks an ECDR. this can happen if
581 : // the user hard-exits while an archive is being written.
582 : // notes:
583 : // - return ERR::CORRUPTED so VFS will not include this file.
584 : // - we could work around this by scanning all LFHs, but won't bother
585 : // because it'd be slow.
586 : // - do not warn - the corrupt archive will be deleted on next
587 : // successful archive builder run anyway.
588 0 : if(FindRecord(buf.get(), sizeof(LFH), buf.get(), lfh_magic, sizeof(LFH)))
589 0 : return ERR::CORRUPTED; // NOWARN
590 : // totally bogus
591 : else
592 0 : WARN_RETURN(ERR::ARCHIVE_UNKNOWN_FORMAT);
593 : }
594 :
595 : PFile m_file;
596 : off_t m_fileSize;
597 : };
598 :
599 1 : PIArchiveReader CreateArchiveReader_Zip(const OsPath& archivePathname)
600 : {
601 : try
602 : {
603 1 : return PIArchiveReader(new ArchiveReader_Zip(archivePathname));
604 : }
605 0 : catch(Status)
606 : {
607 0 : return PIArchiveReader();
608 : }
609 : }
610 :
611 :
612 : //-----------------------------------------------------------------------------
613 : // ArchiveWriter_Zip
614 : //-----------------------------------------------------------------------------
615 :
616 : class ArchiveWriter_Zip : public IArchiveWriter
617 : {
618 : public:
619 0 : ArchiveWriter_Zip(const OsPath& archivePathname, bool noDeflate)
620 0 : : m_file(new File(archivePathname, O_WRONLY)), m_fileSize(0)
621 0 : , m_numEntries(0), m_noDeflate(noDeflate)
622 : {
623 0 : THROW_STATUS_IF_ERR(pool_create(&m_cdfhPool, 10*MiB, 0));
624 0 : }
625 :
626 0 : ~ArchiveWriter_Zip()
627 0 : {
628 : // append an ECDR to the CDFH list (this allows us to
629 : // write out both to the archive file in one burst)
630 0 : const size_t cd_size = m_cdfhPool.da.pos;
631 0 : ECDR* ecdr = (ECDR*)pool_alloc(&m_cdfhPool, sizeof(ECDR));
632 0 : if(!ecdr)
633 0 : std::terminate();
634 0 : const off_t cd_ofs = m_fileSize;
635 0 : ecdr->Init(m_numEntries, cd_ofs, cd_size);
636 :
637 0 : if(write(m_file->Descriptor(), m_cdfhPool.da.base, cd_size+sizeof(ECDR)) < 0)
638 0 : DEBUG_WARN_ERR(ERR::IO); // no way to return error code
639 :
640 0 : (void)pool_destroy(&m_cdfhPool);
641 0 : }
642 :
643 0 : Status AddFile(const OsPath& pathname, const OsPath& pathnameInArchive)
644 : {
645 0 : CFileInfo fileInfo;
646 0 : RETURN_STATUS_IF_ERR(GetFileInfo(pathname, &fileInfo));
647 :
648 0 : PFile file(new File);
649 0 : RETURN_STATUS_IF_ERR(file->Open(pathname, O_RDONLY));
650 :
651 0 : return AddFileOrMemory(fileInfo, pathnameInArchive, file, NULL);
652 : }
653 :
654 0 : Status AddMemory(const u8* data, size_t size, time_t mtime, const OsPath& pathnameInArchive)
655 : {
656 0 : CFileInfo fileInfo(pathnameInArchive, size, mtime);
657 :
658 0 : return AddFileOrMemory(fileInfo, pathnameInArchive, PFile(), data);
659 : }
660 :
661 0 : Status AddFileOrMemory(const CFileInfo& fileInfo, const OsPath& pathnameInArchive, const PFile& file, const u8* data)
662 : {
663 0 : ENSURE((file && !data) || (data && !file));
664 :
665 0 : const off_t usize = fileInfo.Size();
666 : // skip 0-length files.
667 : // rationale: zip.cpp needs to determine whether a CDFH entry is
668 : // a file or directory (the latter are written by some programs but
669 : // not needed - they'd only pollute the file table).
670 : // it looks like checking for usize=csize=0 is the safest way -
671 : // relying on file attributes (which are system-dependent!) is
672 : // even less safe.
673 : // we thus skip 0-length files to avoid confusing them with directories.
674 0 : if(!usize)
675 0 : return INFO::SKIPPED;
676 :
677 0 : const size_t pathnameLength = pathnameInArchive.string().length();
678 :
679 : // choose method and the corresponding codec
680 : ZipMethod method;
681 0 : PICodec codec;
682 0 : if(m_noDeflate || IsFileTypeIncompressible(pathnameInArchive))
683 : {
684 0 : method = ZIP_METHOD_NONE;
685 0 : codec = CreateCodec_ZLibNone();
686 : }
687 : else
688 : {
689 0 : method = ZIP_METHOD_DEFLATE;
690 0 : codec = CreateCompressor_ZLibDeflate();
691 : }
692 :
693 : // allocate memory
694 0 : const size_t csizeMax = codec->MaxOutputSize(size_t(usize));
695 0 : io::BufferPtr buf(io::Allocate(sizeof(LFH) + pathnameLength + csizeMax));
696 :
697 : // read and compress file contents
698 : size_t csize; u32 checksum;
699 : {
700 0 : u8* cdata = buf.get() + sizeof(LFH) + pathnameLength;
701 0 : Stream stream(codec);
702 0 : stream.SetOutputBuffer(cdata, csizeMax);
703 0 : StreamFeeder streamFeeder(stream);
704 0 : if(file)
705 : {
706 0 : io::Operation op(*file.get(), 0, usize);
707 0 : RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder));
708 : }
709 : else
710 : {
711 0 : RETURN_STATUS_IF_ERR(streamFeeder(data, usize));
712 : }
713 0 : RETURN_STATUS_IF_ERR(stream.Finish());
714 0 : csize = stream.OutSize();
715 0 : checksum = stream.Checksum();
716 : }
717 :
718 : // build LFH
719 : {
720 0 : LFH* lfh = reinterpret_cast<LFH*>(buf.get());
721 0 : lfh->Init(fileInfo, (off_t)csize, method, checksum, pathnameInArchive);
722 : }
723 :
724 : // append a CDFH to the central directory (in memory)
725 0 : const off_t ofs = m_fileSize;
726 0 : const size_t prev_pos = m_cdfhPool.da.pos; // (required to determine padding size)
727 0 : const size_t cdfhSize = sizeof(CDFH) + pathnameLength;
728 0 : CDFH* cdfh = (CDFH*)pool_alloc(&m_cdfhPool, cdfhSize);
729 0 : if(!cdfh)
730 0 : WARN_RETURN(ERR::NO_MEM);
731 0 : const size_t slack = m_cdfhPool.da.pos - prev_pos - cdfhSize;
732 0 : cdfh->Init(fileInfo, ofs, (off_t)csize, method, checksum, pathnameInArchive, slack);
733 0 : m_numEntries++;
734 :
735 : // write LFH, pathname and cdata to file
736 0 : const size_t packageSize = sizeof(LFH) + pathnameLength + csize;
737 0 : if(write(m_file->Descriptor(), buf.get(), packageSize) < 0)
738 0 : WARN_RETURN(ERR::IO);
739 0 : m_fileSize += (off_t)packageSize;
740 :
741 0 : return INFO::OK;
742 : }
743 :
744 : private:
745 0 : static bool IsFileTypeIncompressible(const OsPath& pathname)
746 : {
747 0 : const OsPath extension = pathname.Extension();
748 :
749 : // file extensions that we don't want to compress
750 : static const wchar_t* incompressibleExtensions[] =
751 : {
752 : L".zip", L".rar",
753 : L".jpg", L".jpeg", L".png",
754 : L".ogg", L".mp3"
755 : };
756 :
757 0 : for(size_t i = 0; i < ARRAY_SIZE(incompressibleExtensions); i++)
758 : {
759 0 : if(extension == incompressibleExtensions[i])
760 0 : return true;
761 : }
762 :
763 0 : return false;
764 : }
765 :
766 : PFile m_file;
767 : off_t m_fileSize;
768 :
769 : Pool m_cdfhPool;
770 : size_t m_numEntries;
771 :
772 : bool m_noDeflate;
773 : };
774 :
775 0 : PIArchiveWriter CreateArchiveWriter_Zip(const OsPath& archivePathname, bool noDeflate)
776 : {
777 : try
778 : {
779 0 : return PIArchiveWriter(new ArchiveWriter_Zip(archivePathname, noDeflate));
780 : }
781 0 : catch(Status)
782 : {
783 0 : return PIArchiveWriter();
784 : }
785 3 : }
|