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