1 /*****************************************************************************/
2 /* SFileOpenArchive.cpp Copyright Ladislav Zezula 1999 */
3 /* */
4 /* Author : Ladislav Zezula */
5 /* E-mail : ladik@zezula.net */
6 /* WWW : www.zezula.net */
7 /*---------------------------------------------------------------------------*/
8 /* Archive functions of Storm.dll */
9 /*---------------------------------------------------------------------------*/
10 /* Date Ver Who Comment */
11 /* -------- ---- --- ------- */
12 /* xx.xx.xx 1.00 Lad The first version of SFileOpenArchive.cpp */
13 /* 19.11.03 1.01 Dan Big endian handling */
14 /*****************************************************************************/
15
16 #ifndef FULL
17 #include <algorithm>
18 #include <string>
19 #endif
20
21 #define __STORMLIB_SELF__
22 #include "StormLib.h"
23 #include "StormCommon.h"
24
25 #define HEADER_SEARCH_BUFFER_SIZE 0x1000
26
27 /*****************************************************************************/
28 /* Local functions */
29 /*****************************************************************************/
30
IsAviFile(DWORD * HeaderData)31 static bool IsAviFile(DWORD * HeaderData)
32 {
33 DWORD DwordValue0 = BSWAP_INT32_UNSIGNED(HeaderData[0]);
34 DWORD DwordValue2 = BSWAP_INT32_UNSIGNED(HeaderData[2]);
35 DWORD DwordValue3 = BSWAP_INT32_UNSIGNED(HeaderData[3]);
36
37 // Test for 'RIFF', 'AVI ' or 'LIST'
38 return (DwordValue0 == 0x46464952 && DwordValue2 == 0x20495641 && DwordValue3 == 0x5453494C);
39 }
40
IsWarcraft3Map(DWORD * HeaderData)41 static bool IsWarcraft3Map(DWORD * HeaderData)
42 {
43 DWORD DwordValue0 = BSWAP_INT32_UNSIGNED(HeaderData[0]);
44 DWORD DwordValue1 = BSWAP_INT32_UNSIGNED(HeaderData[1]);
45
46 return (DwordValue0 == 0x57334D48 && DwordValue1 == 0x00000000);
47 }
48
IsValidMpqUserData(ULONGLONG ByteOffset,ULONGLONG FileSize,void * pvUserData)49 static TMPQUserData * IsValidMpqUserData(ULONGLONG ByteOffset, ULONGLONG FileSize, void * pvUserData)
50 {
51 TMPQUserData * pUserData;
52
53 // BSWAP the source data and copy them to our buffer
54 BSWAP_ARRAY32_UNSIGNED(&pvUserData, sizeof(TMPQUserData));
55 pUserData = (TMPQUserData *)pvUserData;
56
57 // Check the sizes
58 if(pUserData->cbUserDataHeader <= pUserData->cbUserDataSize && pUserData->cbUserDataSize <= pUserData->dwHeaderOffs)
59 {
60 // Move to the position given by the userdata
61 ByteOffset += pUserData->dwHeaderOffs;
62
63 // The MPQ header should be within range of the file size
64 if((ByteOffset + MPQ_HEADER_SIZE_V1) < FileSize)
65 {
66 // Note: We should verify if there is the MPQ header.
67 // However, the header could be at any position below that
68 // that is multiplier of 0x200
69 return (TMPQUserData *)pvUserData;
70 }
71 }
72
73 return NULL;
74 }
75
76 // This function gets the right positions of the hash table and the block table.
VerifyMpqTablePositions(TMPQArchive * ha,ULONGLONG FileSize)77 static int VerifyMpqTablePositions(TMPQArchive * ha, ULONGLONG FileSize)
78 {
79 TMPQHeader * pHeader = ha->pHeader;
80 ULONGLONG ByteOffset;
81
82 // Check the begin of HET table
83 if(pHeader->HetTablePos64)
84 {
85 ByteOffset = ha->MpqPos + pHeader->HetTablePos64;
86 if(ByteOffset > FileSize)
87 return ERROR_BAD_FORMAT;
88 }
89
90 // Check the begin of BET table
91 if(pHeader->BetTablePos64)
92 {
93 ByteOffset = ha->MpqPos + pHeader->BetTablePos64;
94 if(ByteOffset > FileSize)
95 return ERROR_BAD_FORMAT;
96 }
97
98 // Check the begin of hash table
99 if(pHeader->wHashTablePosHi || pHeader->dwHashTablePos)
100 {
101 ByteOffset = FileOffsetFromMpqOffset(ha, MAKE_OFFSET64(pHeader->wHashTablePosHi, pHeader->dwHashTablePos));
102 if(ByteOffset > FileSize)
103 return ERROR_BAD_FORMAT;
104 }
105
106 // Check the begin of block table
107 if(pHeader->wBlockTablePosHi || pHeader->dwBlockTablePos)
108 {
109 ByteOffset = FileOffsetFromMpqOffset(ha, MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos));
110 if(ByteOffset > FileSize)
111 return ERROR_BAD_FORMAT;
112 }
113
114 // Check the begin of hi-block table
115 if(pHeader->HiBlockTablePos64 != 0)
116 {
117 ByteOffset = ha->MpqPos + pHeader->HiBlockTablePos64;
118 if(ByteOffset > FileSize)
119 return ERROR_BAD_FORMAT;
120 }
121
122 // All OK.
123 return ERROR_SUCCESS;
124 }
125
126
127 /*****************************************************************************/
128 /* Public functions */
129 /*****************************************************************************/
130
131 //-----------------------------------------------------------------------------
132 // SFileGetLocale and SFileSetLocale
133 // Set the locale for all newly opened files
134
SFileGetLocale()135 LCID STORMAPI SFileGetLocale()
136 {
137 return lcFileLocale;
138 }
139
SFileSetLocale(LCID lcNewLocale)140 LCID STORMAPI SFileSetLocale(LCID lcNewLocale)
141 {
142 lcFileLocale = lcNewLocale;
143 return lcFileLocale;
144 }
145
146 //-----------------------------------------------------------------------------
147 // SFileOpenArchive
148 //
149 // szFileName - MPQ archive file name to open
150 // dwPriority - When SFileOpenFileEx called, this contains the search priority for searched archives
151 // dwFlags - See MPQ_OPEN_XXX in StormLib.h
152 // phMpq - Pointer to store open archive handle
153
SFileOpenArchive(const TCHAR * szMpqName,DWORD dwPriority,DWORD dwFlags,HANDLE * phMpq)154 bool STORMAPI SFileOpenArchive(
155 const TCHAR * szMpqName,
156 DWORD dwPriority,
157 DWORD dwFlags,
158 HANDLE * phMpq)
159 {
160 TMPQUserData * pUserData;
161 TFileStream * pStream = NULL; // Open file stream
162 TMPQArchive * ha = NULL; // Archive handle
163 TFileEntry * pFileEntry;
164 ULONGLONG FileSize = 0; // Size of the file
165 LPBYTE pbHeaderBuffer = NULL; // Buffer for searching MPQ header
166 DWORD dwStreamFlags = (dwFlags & STREAM_FLAGS_MASK);
167 bool bIsWarcraft3Map = false;
168 int nError = ERROR_SUCCESS;
169
170 // Verify the parameters
171 if(szMpqName == NULL || *szMpqName == 0 || phMpq == NULL)
172 {
173 SetLastError(ERROR_INVALID_PARAMETER);
174 return false;
175 }
176
177 // One time initialization of MPQ cryptography
178 InitializeMpqCryptography();
179 dwPriority = dwPriority;
180
181 // If not forcing MPQ v 1.0, also use file bitmap
182 dwStreamFlags |= (dwFlags & MPQ_OPEN_FORCE_MPQ_V1) ? 0 : STREAM_FLAG_USE_BITMAP;
183
184 #ifndef FULL
185 std::string name = szMpqName;
186 std::replace(name.begin(), name.end(), '\\', '/');
187 szMpqName = name.c_str();
188 #endif
189
190 // Open the MPQ archive file
191 pStream = FileStream_OpenFile(szMpqName, dwStreamFlags);
192 if(pStream == NULL)
193 return false;
194
195 // Check the file size. There must be at least 0x20 bytes
196 if(nError == ERROR_SUCCESS)
197 {
198 FileStream_GetSize(pStream, &FileSize);
199 if(FileSize < MPQ_HEADER_SIZE_V1)
200 nError = ERROR_BAD_FORMAT;
201 }
202
203 // Allocate the MPQhandle
204 if(nError == ERROR_SUCCESS)
205 {
206 if((ha = STORM_ALLOC(TMPQArchive, 1)) == NULL)
207 nError = ERROR_NOT_ENOUGH_MEMORY;
208 }
209
210 // Allocate buffer for searching MPQ header
211 if(nError == ERROR_SUCCESS)
212 {
213 pbHeaderBuffer = STORM_ALLOC(BYTE, HEADER_SEARCH_BUFFER_SIZE);
214 if(pbHeaderBuffer == NULL)
215 nError = ERROR_NOT_ENOUGH_MEMORY;
216 }
217
218 // Find the position of MPQ header
219 if(nError == ERROR_SUCCESS)
220 {
221 ULONGLONG SearchOffset = 0;
222 ULONGLONG EndOfSearch = FileSize;
223 DWORD dwStrmFlags = 0;
224 DWORD dwHeaderSize;
225 DWORD dwHeaderID;
226 bool bSearchComplete = false;
227
228 memset(ha, 0, sizeof(TMPQArchive));
229 ha->pfnHashString = HashStringSlash;
230 ha->pStream = pStream;
231 pStream = NULL;
232
233 // Set the archive read only if the stream is read-only
234 FileStream_GetFlags(ha->pStream, &dwStrmFlags);
235 ha->dwFlags |= (dwStrmFlags & STREAM_FLAG_READ_ONLY) ? MPQ_FLAG_READ_ONLY : 0;
236
237 // Also remember if we shall check sector CRCs when reading file
238 ha->dwFlags |= (dwFlags & MPQ_OPEN_CHECK_SECTOR_CRC) ? MPQ_FLAG_CHECK_SECTOR_CRC : 0;
239
240 // Also remember if this MPQ is a patch
241 ha->dwFlags |= (dwFlags & MPQ_OPEN_PATCH) ? MPQ_FLAG_PATCH : 0;
242
243 // Limit the header searching to about 130 MB of data
244 if(EndOfSearch > 0x08000000)
245 EndOfSearch = 0x08000000;
246
247 // Find the offset of MPQ header within the file
248 while(bSearchComplete == false && SearchOffset < EndOfSearch)
249 {
250 // Always read at least 0x1000 bytes for performance.
251 // This is what Storm.dll (2002) does.
252 DWORD dwBytesAvailable = HEADER_SEARCH_BUFFER_SIZE;
253 DWORD dwInBufferOffset = 0;
254
255 // Cut the bytes available, if needed
256 if((FileSize - SearchOffset) < HEADER_SEARCH_BUFFER_SIZE)
257 dwBytesAvailable = (DWORD)(FileSize - SearchOffset);
258
259 // Read the eventual MPQ header
260 if(!FileStream_Read(ha->pStream, &SearchOffset, pbHeaderBuffer, dwBytesAvailable))
261 {
262 nError = GetLastError();
263 break;
264 }
265
266 // There are AVI files from Warcraft III with 'MPQ' extension.
267 if(SearchOffset == 0)
268 {
269 if(IsAviFile((DWORD *)pbHeaderBuffer))
270 {
271 nError = ERROR_AVI_FILE;
272 break;
273 }
274
275 bIsWarcraft3Map = IsWarcraft3Map((DWORD *)pbHeaderBuffer);
276 }
277
278 // Search the header buffer
279 while(dwInBufferOffset < dwBytesAvailable)
280 {
281 // Copy the data from the potential header buffer to the MPQ header
282 memcpy(ha->HeaderData, pbHeaderBuffer + dwInBufferOffset, sizeof(ha->HeaderData));
283
284 // If there is the MPQ user data, process it
285 // Note that Warcraft III does not check for user data, which is abused by many map protectors
286 dwHeaderID = BSWAP_INT32_UNSIGNED(ha->HeaderData[0]);
287 if(bIsWarcraft3Map == false && (dwFlags & MPQ_OPEN_FORCE_MPQ_V1) == 0)
288 {
289 if(ha->pUserData == NULL && dwHeaderID == ID_MPQ_USERDATA)
290 {
291 // Verify if this looks like a valid user data
292 pUserData = IsValidMpqUserData(SearchOffset, FileSize, ha->HeaderData);
293 if(pUserData != NULL)
294 {
295 // Fill the user data header
296 ha->UserDataPos = SearchOffset;
297 ha->pUserData = &ha->UserData;
298 memcpy(ha->pUserData, pUserData, sizeof(TMPQUserData));
299
300 // Continue searching from that position
301 SearchOffset += ha->pUserData->dwHeaderOffs;
302 break;
303 }
304 }
305 }
306
307 // There must be MPQ header signature. Note that STORM.dll from Warcraft III actually
308 // tests the MPQ header size. It must be at least 0x20 bytes in order to load it
309 // Abused by Spazzler Map protector. Note that the size check is not present
310 // in Storm.dll v 1.00, so Diablo I code would load the MPQ anyway.
311 dwHeaderSize = BSWAP_INT32_UNSIGNED(ha->HeaderData[1]);
312 if(dwHeaderID == ID_MPQ && dwHeaderSize >= MPQ_HEADER_SIZE_V1)
313 {
314 // Now convert the header to version 4
315 nError = ConvertMpqHeaderToFormat4(ha, SearchOffset, FileSize, dwFlags, bIsWarcraft3Map);
316 bSearchComplete = true;
317 break;
318 }
319
320 // Check for MPK archives (Longwu Online - MPQ fork)
321 if(bIsWarcraft3Map == false && dwHeaderID == ID_MPK)
322 {
323 // Now convert the MPK header to MPQ Header version 4
324 nError = ConvertMpkHeaderToFormat4(ha, FileSize, dwFlags);
325 bSearchComplete = true;
326 break;
327 }
328
329 // If searching for the MPQ header is disabled, return an error
330 if(dwFlags & MPQ_OPEN_NO_HEADER_SEARCH)
331 {
332 nError = ERROR_NOT_SUPPORTED;
333 bSearchComplete = true;
334 break;
335 }
336
337 // Move the pointers
338 SearchOffset += 0x200;
339 dwInBufferOffset += 0x200;
340 }
341 }
342
343 // Did we identify one of the supported headers?
344 if(nError == ERROR_SUCCESS)
345 {
346 // Set the user data position to the MPQ header, if none
347 if(ha->pUserData == NULL)
348 ha->UserDataPos = SearchOffset;
349
350 // Set the position of the MPQ header
351 ha->pHeader = (TMPQHeader *)ha->HeaderData;
352 ha->MpqPos = SearchOffset;
353 ha->FileSize = FileSize;
354
355 // Sector size must be nonzero.
356 if(SearchOffset >= FileSize || ha->pHeader->wSectorSize == 0)
357 nError = ERROR_BAD_FORMAT;
358 }
359 }
360
361 // Fix table positions according to format
362 if(nError == ERROR_SUCCESS)
363 {
364 // Dump the header
365 // DumpMpqHeader(ha->pHeader);
366
367 // W3x Map Protectors use the fact that War3's Storm.dll ignores the MPQ user data,
368 // and ignores the MPQ format version as well. The trick is to
369 // fake MPQ format 2, with an improper hi-word position of hash table and block table
370 // We can overcome such protectors by forcing opening the archive as MPQ v 1.0
371 if(dwFlags & MPQ_OPEN_FORCE_MPQ_V1)
372 {
373 ha->pHeader->wFormatVersion = MPQ_FORMAT_VERSION_1;
374 ha->pHeader->dwHeaderSize = MPQ_HEADER_SIZE_V1;
375 ha->dwFlags |= MPQ_FLAG_READ_ONLY;
376 ha->pUserData = NULL;
377 }
378
379 // Anti-overflow. If the hash table size in the header is
380 // higher than 0x10000000, it would overflow in 32-bit version
381 // Observed in the malformed Warcraft III maps
382 // Example map: MPQ_2016_v1_ProtectedMap_TableSizeOverflow.w3x
383 ha->pHeader->dwBlockTableSize = (ha->pHeader->dwBlockTableSize & BLOCK_INDEX_MASK);
384 ha->pHeader->dwHashTableSize = (ha->pHeader->dwHashTableSize & BLOCK_INDEX_MASK);
385
386 // Both MPQ_OPEN_NO_LISTFILE or MPQ_OPEN_NO_ATTRIBUTES trigger read only mode
387 if(dwFlags & (MPQ_OPEN_NO_LISTFILE | MPQ_OPEN_NO_ATTRIBUTES))
388 ha->dwFlags |= MPQ_FLAG_READ_ONLY;
389
390 // Remember whether whis is a map for Warcraft III
391 if(bIsWarcraft3Map)
392 ha->dwFlags |= MPQ_FLAG_WAR3_MAP;
393
394 // Set the size of file sector
395 ha->dwSectorSize = (0x200 << ha->pHeader->wSectorSize);
396
397 // Verify if any of the tables doesn't start beyond the end of the file
398 nError = VerifyMpqTablePositions(ha, FileSize);
399 }
400
401 // Read the hash table. Ignore the result, as hash table is no longer required
402 // Read HET table. Ignore the result, as HET table is no longer required
403 if(nError == ERROR_SUCCESS)
404 {
405 nError = LoadAnyHashTable(ha);
406 }
407
408 // Now, build the file table. It will be built by combining
409 // the block table, BET table, hi-block table, (attributes) and (listfile).
410 if(nError == ERROR_SUCCESS)
411 {
412 nError = BuildFileTable(ha);
413 }
414
415 #ifdef FULL
416 // Load the internal listfile and include it to the file table
417 if(nError == ERROR_SUCCESS && (dwFlags & MPQ_OPEN_NO_LISTFILE) == 0)
418 {
419 // Quick check for (listfile)
420 pFileEntry = GetFileEntryLocale(ha, LISTFILE_NAME, LANG_NEUTRAL);
421 if(pFileEntry != NULL)
422 {
423 // Ignore result of the operation. (listfile) is optional.
424 SFileAddListFile((HANDLE)ha, NULL);
425 ha->dwFileFlags1 = pFileEntry->dwFlags;
426 }
427 }
428
429 // Load the "(attributes)" file and merge it to the file table
430 if(nError == ERROR_SUCCESS && (dwFlags & MPQ_OPEN_NO_ATTRIBUTES) == 0 && (ha->dwFlags & MPQ_FLAG_BLOCK_TABLE_CUT) == 0)
431 {
432 // Quick check for (attributes)
433 pFileEntry = GetFileEntryLocale(ha, ATTRIBUTES_NAME, LANG_NEUTRAL);
434 if(pFileEntry != NULL)
435 {
436 // Ignore result of the operation. (attributes) is optional.
437 SAttrLoadAttributes(ha);
438 ha->dwFileFlags2 = pFileEntry->dwFlags;
439 }
440 }
441 #endif
442
443 // Remember whether the archive has weak signature. Only for MPQs format 1.0.
444 if(nError == ERROR_SUCCESS)
445 {
446 // Quick check for (signature)
447 pFileEntry = GetFileEntryLocale(ha, SIGNATURE_NAME, LANG_NEUTRAL);
448 if(pFileEntry != NULL)
449 {
450 // Just remember that the archive is weak-signed
451 assert((pFileEntry->dwFlags & MPQ_FILE_EXISTS) != 0);
452 ha->dwFileFlags3 = pFileEntry->dwFlags;
453 }
454
455 // Finally, set the MPQ_FLAG_READ_ONLY if the MPQ was found malformed
456 ha->dwFlags |= (ha->dwFlags & MPQ_FLAG_MALFORMED) ? MPQ_FLAG_READ_ONLY : 0;
457 }
458
459 // Cleanup and exit
460 if(nError != ERROR_SUCCESS)
461 {
462 FileStream_Close(pStream);
463 FreeArchiveHandle(ha);
464 SetLastError(nError);
465 ha = NULL;
466 }
467
468 // Free the header buffer
469 if(pbHeaderBuffer != NULL)
470 STORM_FREE(pbHeaderBuffer);
471 if(phMpq != NULL)
472 *phMpq = ha;
473 return (nError == ERROR_SUCCESS);
474 }
475
476
477 #ifdef FULL
478 //-----------------------------------------------------------------------------
479 // bool STORMAPI SFileSetDownloadCallback(HANDLE, SFILE_DOWNLOAD_CALLBACK, void *);
480 //
481 // Sets a callback that is called when content is downloaded from the master MPQ
482 //
483
SFileSetDownloadCallback(HANDLE hMpq,SFILE_DOWNLOAD_CALLBACK DownloadCB,void * pvUserData)484 bool STORMAPI SFileSetDownloadCallback(HANDLE hMpq, SFILE_DOWNLOAD_CALLBACK DownloadCB, void * pvUserData)
485 {
486 TMPQArchive * ha = (TMPQArchive *)hMpq;
487
488 // Do nothing if 'hMpq' is bad parameter
489 if(!IsValidMpqHandle(hMpq))
490 {
491 SetLastError(ERROR_INVALID_HANDLE);
492 return false;
493 }
494
495 return FileStream_SetCallback(ha->pStream, DownloadCB, pvUserData);
496 }
497
498 //-----------------------------------------------------------------------------
499 // bool SFileFlushArchive(HANDLE hMpq)
500 //
501 // Saves all dirty data into MPQ archive.
502 // Has similar effect like SFileCloseArchive, but the archive is not closed.
503 // Use on clients who keep MPQ archive open even for write operations,
504 // and terminating without calling SFileCloseArchive might corrupt the archive.
505 //
506
SFileFlushArchive(HANDLE hMpq)507 bool STORMAPI SFileFlushArchive(HANDLE hMpq)
508 {
509 TMPQArchive * ha;
510 int nResultError = ERROR_SUCCESS;
511 int nError;
512
513 // Do nothing if 'hMpq' is bad parameter
514 if((ha = IsValidMpqHandle(hMpq)) == NULL)
515 {
516 SetLastError(ERROR_INVALID_HANDLE);
517 return false;
518 }
519
520 // Only if the MPQ was changed
521 if(ha->dwFlags & MPQ_FLAG_CHANGED)
522 {
523 // Indicate that we are saving MPQ internal structures
524 ha->dwFlags |= MPQ_FLAG_SAVING_TABLES;
525
526 // Defragment the file table. This will allow us to put the internal files to the end
527 DefragmentFileTable(ha);
528
529 //
530 // Create each internal file
531 // Note that the (signature) file is usually before (listfile) in the file table
532 //
533
534 if(ha->dwFlags & MPQ_FLAG_SIGNATURE_NEW)
535 {
536 #ifdef FULL
537 nError = SSignFileCreate(ha);
538 if(nError != ERROR_SUCCESS)
539 nResultError = nError;
540 #else
541 assert(0);
542 #endif
543 }
544
545 if(ha->dwFlags & MPQ_FLAG_LISTFILE_NEW)
546 {
547 nError = SListFileSaveToMpq(ha);
548 if(nError != ERROR_SUCCESS)
549 nResultError = nError;
550 }
551
552 if(ha->dwFlags & MPQ_FLAG_ATTRIBUTES_NEW)
553 {
554 nError = SAttrFileSaveToMpq(ha);
555 if(nError != ERROR_SUCCESS)
556 nResultError = nError;
557 }
558
559 // Save HET table, BET table, hash table, block table, hi-block table
560 if(ha->dwFlags & MPQ_FLAG_CHANGED)
561 {
562 // Rebuild the HET table
563 if(ha->pHetTable != NULL)
564 RebuildHetTable(ha);
565
566 // Save all MPQ tables first
567 nError = SaveMPQTables(ha);
568 if(nError != ERROR_SUCCESS)
569 nResultError = nError;
570
571 // If the archive has weak signature, we need to finish it
572 if(ha->dwFileFlags3 != 0)
573 {
574 #ifdef FULL
575 nError = SSignFileFinish(ha);
576 if(nError != ERROR_SUCCESS)
577 nResultError = nError;
578 #else
579 assert(0);
580 #endif
581 }
582 }
583
584 // We are no longer saving internal MPQ structures
585 ha->dwFlags &= ~MPQ_FLAG_SAVING_TABLES;
586 }
587
588 // Return the error
589 if(nResultError != ERROR_SUCCESS)
590 SetLastError(nResultError);
591 return (nResultError == ERROR_SUCCESS);
592 }
593 #endif
594
595 //-----------------------------------------------------------------------------
596 // bool SFileCloseArchive(HANDLE hMpq);
597 //
598
SFileCloseArchive(HANDLE hMpq)599 bool STORMAPI SFileCloseArchive(HANDLE hMpq)
600 {
601 TMPQArchive * ha = IsValidMpqHandle(hMpq);
602 bool bResult = true;
603
604 // Only if the handle is valid
605 if(ha == NULL)
606 {
607 SetLastError(ERROR_INVALID_HANDLE);
608 return false;
609 }
610
611 // Invalidate the add file callback so it won't be called
612 // when saving (listfile) and (attributes)
613 ha->pfnAddFileCB = NULL;
614 ha->pvAddFileUserData = NULL;
615
616 #ifdef FULL
617 // Flush all unsaved data to the storage
618 bResult = SFileFlushArchive(hMpq);
619 #endif
620
621 // Free all memory used by MPQ archive
622 FreeArchiveHandle(ha);
623 return bResult;
624 }
625