1 /** @file wad.cpp  WAD Archive (file).
2  *
3  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2006-2014 Daniel Swanson <danij@dengine.net>
5  * @authors Copyright © 2006 Jamie Jones <jamie_jones_au@yahoo.com.au>
6  * @authors Copyright © 1993-1996 id Software, Inc.
7  *
8  * @par License
9  * GPL: http://www.gnu.org/licenses/gpl.html
10  *
11  * <small>This program is free software; you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by the
13  * Free Software Foundation; either version 2 of the License, or (at your
14  * option) any later version. This program is distributed in the hope that it
15  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
16  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
17  * Public License for more details. You should have received a copy of the GNU
18  * General Public License along with this program; if not, write to the Free
19  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
20  * 02110-1301 USA</small>
21  */
22 
23 #include "doomsday/filesys/wad.h"
24 
25 #include "doomsday/DoomsdayApp"
26 #include "doomsday/filesys/lumpcache.h"
27 #include <de/ByteOrder>
28 #include <de/NativePath>
29 #include <de/LogBuffer>
30 #include <de/memoryzone.h>
31 #include <cstring> // memcpy
32 
33 namespace de {
34 namespace internal {
35 
36 struct FileHeader
37 {
38     DENG2_ERROR(ReadError);
39 
40     Block identification; // 4 bytes
41     dint32 lumpRecordsCount;
42     dint32 lumpRecordsOffset;
43 
operator <<de::internal::FileHeader44     void operator << (FileHandle &from)
45     {
46         uint8_t buf[12];
47         dsize readBytes = from.read(buf, 12);
48         if (readBytes != 12) throw ReadError("FileHeader::operator << (FileHandle &)", "Source file is truncated");
49 
50         identification    = Block(buf, 4);
51         lumpRecordsCount  = littleEndianByteOrder.toHost(*reinterpret_cast<dint32 *>(buf + 4));
52         lumpRecordsOffset = littleEndianByteOrder.toHost(*reinterpret_cast<dint32 *>(buf + 8));
53     }
54 };
55 
56 struct IndexEntry
57 {
58     DENG2_ERROR(ReadError);
59 
60     dint32 offset;
61     dint32 size;
62     Block name; // 8 bytes
63 
operator <<de::internal::IndexEntry64     void operator << (FileHandle &from)
65     {
66         uint8_t buf[16];
67         dsize readBytes = from.read(buf, 16);
68         if (readBytes != 16) throw ReadError("IndexEntry::operator << (FileHandle &)", "Source file is truncated");
69 
70         name   = Block(buf + 8, 8);
71         dint32 const *off = reinterpret_cast<dint32 const *>(buf);
72         offset = littleEndianByteOrder.toHost(*off);
73         size   = littleEndianByteOrder.toHost(*reinterpret_cast<dint32 const *>(buf + 4));
74     }
75 
76     /// Perform all translations and encodings to the actual lump name.
nameNormalizedde::internal::IndexEntry77     String nameNormalized() const
78     {
79         String normName;
80 
81         // Determine the actual length of the name.
82         int nameLen = 0;
83         while (nameLen < 8 && name[nameLen]) { nameLen++; }
84 
85         for (int i = 0; i < nameLen; ++i)
86         {
87             /// The Hexen demo on Mac uses the 0x80 on some lumps, maybe has significance?
88             /// @todo Ensure that this doesn't break other IWADs. The 0x80-0xff
89             ///       range isn't normally used in lump names, right??
90             char ch = name[i] & 0x7f;
91             normName += ch;
92         }
93 
94         // WAD format allows characters not normally permitted in native paths.
95         // To achieve uniformity we apply a percent encoding to the "raw" names.
96         if (!normName.isEmpty())
97         {
98             normName = QString(normName.toLatin1().toPercentEncoding());
99         }
100         else
101         {
102             /// Zero-length names are not considered valid - replace with *something*.
103             /// @todo fixme: Handle this more elegantly...
104             normName = "________";
105         }
106 
107         // All lumps are ordained with an extension if they don't have one.
108         if (normName.fileNameExtension().isEmpty())
109         {
110             normName += !normName.compareWithoutCase("DEHACKED")? ".deh" : ".lmp";
111         }
112 
113         return normName;
114     }
115 };
116 
Wad_invalidIndexMessage(int invalidIdx,int lastValidIdx)117 static QString Wad_invalidIndexMessage(int invalidIdx, int lastValidIdx)
118 {
119     QString msg = QString("Invalid lump index %1 ").arg(invalidIdx);
120     if (lastValidIdx < 0) msg += "(file is empty)";
121     else                 msg += QString("(valid range: [0..%2])").arg(lastValidIdx);
122     return msg;
123 }
124 
125 } // namespace internal
126 
127 using namespace internal;
128 
LumpFile(Entry & entry,FileHandle * hndl,String path,FileInfo const & info,File1 * container)129 Wad::LumpFile::LumpFile(Entry &entry, FileHandle *hndl, String path,
130     FileInfo const &info, File1 *container)
131     : File1(hndl, path, info, container)
132     , entry(entry)
133 {}
134 
name() const135 String const &Wad::LumpFile::name() const
136 {
137     return directoryNode().name();
138 }
139 
composeUri(QChar delimiter) const140 Uri Wad::LumpFile::composeUri(QChar delimiter) const
141 {
142     return directoryNode().path(delimiter);
143 }
144 
directoryNode() const145 PathTree::Node &Wad::LumpFile::directoryNode() const
146 {
147     return entry;
148 }
149 
read(uint8_t * buffer,bool tryCache)150 size_t Wad::LumpFile::read(uint8_t *buffer, bool tryCache)
151 {
152     return wad().readLump(info_.lumpIdx, buffer, tryCache);
153 }
154 
read(uint8_t * buffer,size_t startOffset,size_t length,bool tryCache)155 size_t Wad::LumpFile::read(uint8_t *buffer, size_t startOffset, size_t length, bool tryCache)
156 {
157     return wad().readLump(info_.lumpIdx, buffer, startOffset, length, tryCache);
158 }
159 
cache()160 uint8_t const *Wad::LumpFile::cache()
161 {
162     return wad().cacheLump(info_.lumpIdx);
163 }
164 
unlock()165 Wad::LumpFile &Wad::LumpFile::unlock()
166 {
167     wad().unlockLump(info_.lumpIdx);
168     return *this;
169 }
170 
wad() const171 Wad &Wad::LumpFile::wad() const
172 {
173     return container().as<Wad>();
174 }
175 
DENG2_PIMPL_NOREF(Wad)176 DENG2_PIMPL_NOREF(Wad)
177 {
178     LumpTree entries;                     ///< Directory structure and entry records for all lumps.
179     QScopedPointer<LumpCache> dataCache;  ///< Data payload cache.
180 
181     Impl() : entries(PathTree::MultiLeaf) {}
182 };
183 
Wad(FileHandle & hndl,String path,FileInfo const & info,File1 * container)184 Wad::Wad(FileHandle &hndl, String path, FileInfo const &info, File1 *container)
185     : File1(&hndl, path, info, container)
186     , LumpIndex()
187     , d(new Impl)
188 {
189     LOG_AS("Wad");
190 
191     // Seek to the start of the header.
192     handle_->seek(0, SeekSet);
193     FileHeader hdr;
194     hdr << *handle_;
195 
196     // Read the lump entries:
197     if (hdr.lumpRecordsCount <= 0) return;
198 
199     // Seek to the start of the lump index.
200     handle_->seek(hdr.lumpRecordsOffset, SeekSet);
201     for (int i = 0; i < hdr.lumpRecordsCount; ++i)
202     {
203         IndexEntry lump;
204         lump << *handle_;
205 
206         // Determine the name for this lump in the VFS.
207         String absPath = String::fromStdString(DoomsdayApp::app().doomsdayBasePath()) / lump.nameNormalized();
208 
209         // Make an index entry for this lump.
210         Entry &entry = d->entries.insert(absPath);
211 
212         entry.offset = lump.offset;
213         entry.size   = lump.size;
214 
215         LumpFile *lumpFile = new LumpFile(entry, nullptr, entry.path(),
216                                           FileInfo(lastModified(), // Inherited from the container (note recursion).
217                                                    i, entry.offset, entry.size, entry.size),
218                                           this);
219         entry.lumpFile.reset(lumpFile); // takes ownership
220 
221         catalogLump(*lumpFile);
222     }
223 }
224 
~Wad()225 Wad::~Wad()
226 {}
227 
clearCachedLump(int lumpIndex,bool * retCleared)228 void Wad::clearCachedLump(int lumpIndex, bool *retCleared)
229 {
230     LOG_AS("Wad::clearCachedLump");
231 
232     if (retCleared) *retCleared = false;
233 
234     if (hasLump(lumpIndex))
235     {
236         if (!d->dataCache.isNull())
237         {
238             d->dataCache->remove(lumpIndex, retCleared);
239         }
240     }
241     else
242     {
243         LOGDEV_RES_WARNING(Wad_invalidIndexMessage(lumpIndex, lastIndex()));
244     }
245 }
246 
clearLumpCache()247 void Wad::clearLumpCache()
248 {
249     LOG_AS("Wad::clearLumpCache");
250     if (!d->dataCache.isNull())
251     {
252         d->dataCache->clear();
253     }
254 }
255 
cacheLump(int lumpIndex)256 uint8_t const *Wad::cacheLump(int lumpIndex)
257 {
258     LOG_AS("Wad::cacheLump");
259 
260     LumpFile const &lumpFile = static_cast<LumpFile &>(lump(lumpIndex));
261     LOGDEV_RES_XVERBOSE("\"%s:%s\" (%u bytes%s)",
262                NativePath(composePath()).pretty()
263             << NativePath(lumpFile.composePath()).pretty()
264             << (unsigned long) lumpFile.info().size
265             << (lumpFile.info().isCompressed()? ", compressed" : ""));
266 
267     // Time to create the cache?
268     if (d->dataCache.isNull())
269     {
270         d->dataCache.reset(new LumpCache(LumpIndex::size()));
271     }
272 
273     uint8_t const *data = d->dataCache->data(lumpIndex);
274     if (data) return data;
275 
276     uint8_t *region = (uint8_t *) Z_Malloc(lumpFile.info().size, PU_APPSTATIC, 0);
277     if (!region) throw Error("Wad::cacheLump", QString("Failed on allocation of %1 bytes for cache copy of lump #%2").arg(lumpFile.info().size).arg(lumpIndex));
278 
279     readLump(lumpIndex, region, false);
280     d->dataCache->insert(lumpIndex, region);
281 
282     return region;
283 }
284 
unlockLump(int lumpIndex)285 void Wad::unlockLump(int lumpIndex)
286 {
287     LOG_AS("Wad::unlockLump");
288     LOGDEV_RES_XVERBOSE("\"%s:%s\"",
289                         NativePath(composePath()).pretty() <<
290                         NativePath(lump(lumpIndex).composePath()).pretty());
291 
292     if (hasLump(lumpIndex))
293     {
294         if (!d->dataCache.isNull())
295         {
296             d->dataCache->unlock(lumpIndex);
297         }
298     }
299     else
300     {
301         LOGDEV_RES_WARNING(Wad_invalidIndexMessage(lumpIndex, lastIndex()));
302     }
303 }
304 
readLump(int lumpIndex,uint8_t * buffer,bool tryCache)305 size_t Wad::readLump(int lumpIndex, uint8_t *buffer, bool tryCache)
306 {
307     LOG_AS("Wad::readLump");
308     return readLump(lumpIndex, buffer, 0, lump(lumpIndex).size(), tryCache);
309 }
310 
readLump(int lumpIndex,uint8_t * buffer,size_t startOffset,size_t length,bool tryCache)311 size_t Wad::readLump(int lumpIndex, uint8_t *buffer, size_t startOffset,
312     size_t length, bool tryCache)
313 {
314     LOG_AS("Wad::readLump");
315 
316     LumpFile const &lumpFile = static_cast<LumpFile &>(lump(lumpIndex));
317     LOGDEV_RES_XVERBOSE("\"%s:%s\" (%u bytes%s) [%u +%u]",
318                         NativePath(composePath()).pretty()
319                         << NativePath(lumpFile.composePath()).pretty()
320                         << (unsigned long) lumpFile.size()
321                         << (lumpFile.isCompressed()? ", compressed" : "")
322                         << startOffset
323                         << length);
324 
325     // Try to avoid a file system read by checking for a cached copy.
326     if (tryCache)
327     {
328         uint8_t const *data = (!d->dataCache.isNull() ? d->dataCache->data(lumpIndex) : 0);
329         LOGDEV_RES_XVERBOSE("Cache %s on #%i", (data? "hit" : "miss") << lumpIndex);
330         if (data)
331         {
332             size_t readBytes = de::min(size_t(lumpFile.size()), length);
333             std::memcpy(buffer, data + startOffset, readBytes);
334             return readBytes;
335         }
336     }
337 
338     handle_->seek(lumpFile.info().baseOffset + startOffset, SeekSet);
339     size_t readBytes = handle_->read(buffer, length);
340 
341     /// @todo Do not check the read length here.
342     if (readBytes < length)
343         throw Error("Wad::readLumpSection", QString("Only read %1 of %2 bytes of lump #%3").arg(readBytes).arg(length).arg(lumpIndex));
344 
345     return readBytes;
346 }
347 
calculateCRC()348 uint Wad::calculateCRC()
349 {
350     uint crc = 0;
351     foreach (File1 *file, allLumps())
352     {
353         Entry &entry = static_cast<Entry &>(file->as<LumpFile>().directoryNode());
354         entry.update();
355         crc += entry.crc;
356     }
357     return crc;
358 }
359 
recognise(FileHandle & file)360 bool Wad::recognise(FileHandle &file)
361 {
362     // Seek to the start of the header.
363     size_t initPos = file.tell();
364     file.seek(0, SeekSet);
365 
366     // Attempt to read the header.
367     bool readOk = false;
368     FileHeader hdr;
369     try
370     {
371         hdr << file;
372         readOk = true;
373     }
374     catch (FileHeader::ReadError const &)
375     {} // Ignore
376 
377     // Return the stream to its original position.
378     file.seek(initPos, SeekSet);
379 
380     if (!readOk) return false;
381 
382     return (hdr.identification == "IWAD" || hdr.identification == "PWAD");
383 }
384 
lumpTree() const385 Wad::LumpTree const &Wad::lumpTree() const
386 {
387     return d->entries;
388 }
389 
file() const390 Wad::LumpFile &Wad::Entry::file() const
391 {
392     DENG2_ASSERT(!lumpFile.isNull());
393     return *lumpFile;
394 }
395 
update()396 void Wad::Entry::update()
397 {
398     crc = uint(file().size());
399     String const lumpName = Node::name();
400     int const nameLen = lumpName.length();
401     for (int i = 0; i < nameLen; ++i)
402     {
403         crc += lumpName.at(i).unicode();
404     }
405 }
406 
407 } // namespace de
408