// DmgHandler.cpp #include "StdAfx.h" #include "../../../C/CpuArch.h" #include "Common/Buffer.h" #include "Common/ComTry.h" #include "Common/IntToString.h" #include "Common/MyXml.h" #include "Common/UTFConvert.h" #include "Windows/PropVariant.h" #include "../Common/LimitedStreams.h" #include "../Common/ProgressUtils.h" #include "../Common/RegisterArc.h" #include "../Common/StreamUtils.h" #include "../Compress/BZip2Decoder.h" #include "../Compress/CopyCoder.h" #include "../Compress/ZlibDecoder.h" // #define DMG_SHOW_RAW // #include #define PRF(x) // x #define Get32(p) GetBe32(p) #define Get64(p) GetBe64(p) static int Base64ToByte(char c) { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= 'a' && c <= 'z') return c - 'a' + 26; if (c >= '0' && c <= '9') return c - '0' + 52; if (c == '+') return 62; if (c == '/') return 63; if (c == '=') return 0; return -1; } static int Base64ToBin(Byte *dest, const char *src, int srcLen) { int srcPos = 0; int destPos = 0; while (srcPos < srcLen) { Byte buf[4]; int filled = 0; while (srcPos < srcLen) { int n = Base64ToByte(src[srcPos++]); if (n >= 0) { buf[filled++] = (Byte)n; if (filled == 4) break; } } if (filled >= 2) { if (dest) dest[destPos] = (buf[0] << 2) | (buf[1] >> 4); destPos++; } if (filled >= 3) { if (dest) dest[destPos] = (buf[1] << 4) | (buf[2] >> 2); destPos++; } if (filled >= 4) { if (dest) dest[destPos] = (buf[2] << 6) | (buf[3] ); destPos++; } } return destPos; } static UString GetSizeString(UInt64 value) { wchar_t s[32]; wchar_t c; if (value < (UInt64)20000) c = 0; else if (value < ((UInt64)20000 << 10)) { value >>= 10; c = L'K'; } else if (value < ((UInt64)20000 << 20)) { value >>= 20; c = L'M'; } else { value >>= 30; c = L'G'; } ConvertUInt64ToString(value, s); int p = MyStringLen(s); s[p++] = c; s[p++] = L'\0'; return s; } namespace NArchive { namespace NDmg { struct CBlock { UInt32 Type; UInt64 UnpPos; UInt64 UnpSize; UInt64 PackPos; UInt64 PackSize; UInt64 GetNextPackOffset() const { return PackPos + PackSize; } }; struct CFile { CByteBuffer Raw; UInt64 StartPos; CRecordVector Blocks; UInt64 GetUnpackSize() const { UInt64 size = 0; for (int i = 0; i < Blocks.Size(); i++) size += Blocks[i].UnpSize; return size; }; UInt64 GetPackSize() const { UInt64 size = 0; for (int i = 0; i < Blocks.Size(); i++) size += Blocks[i].PackSize; return size; }; AString Name; }; class CHandler: public IInArchive, public CMyUnknownImp { CMyComPtr _inStream; AString _xml; CObjectVector _files; CRecordVector _fileIndices; HRESULT Open2(IInStream *stream); HRESULT Extract(IInStream *stream); public: MY_UNKNOWN_IMP1(IInArchive) INTERFACE_IInArchive(;) }; const UInt32 kXmlSizeMax = ((UInt32)1 << 31) - (1 << 14); enum { METHOD_ZERO_0 = 0, METHOD_COPY = 1, METHOD_ZERO_2 = 2, METHOD_ADC = 0x80000004, METHOD_ZLIB = 0x80000005, METHOD_BZIP2 = 0x80000006, METHOD_DUMMY = 0x7FFFFFFE, METHOD_END = 0xFFFFFFFF }; struct CMethodStat { UInt32 NumBlocks; UInt64 PackSize; UInt64 UnpSize; CMethodStat(): NumBlocks(0), PackSize(0), UnpSize(0) {} }; struct CMethods { CRecordVector Stats; CRecordVector Types; void Update(const CFile &file); UString GetString() const; }; void CMethods::Update(const CFile &file) { for (int i = 0; i < file.Blocks.Size(); i++) { const CBlock &b = file.Blocks[i]; int index = Types.FindInSorted(b.Type); if (index < 0) { index = Types.AddToUniqueSorted(b.Type); Stats.Insert(index, CMethodStat()); } CMethodStat &m = Stats[index]; m.PackSize += b.PackSize; m.UnpSize += b.UnpSize; m.NumBlocks++; } } UString CMethods::GetString() const { UString res; for (int i = 0; i < Types.Size(); i++) { if (i != 0) res += L' '; wchar_t buf[32]; const wchar_t *s; const CMethodStat &m = Stats[i]; bool showPack = true; UInt32 type = Types[i]; switch(type) { case METHOD_ZERO_0: s = L"zero0"; showPack = (m.PackSize != 0); break; case METHOD_ZERO_2: s = L"zero2"; showPack = (m.PackSize != 0); break; case METHOD_COPY: s = L"copy"; showPack = (m.UnpSize != m.PackSize); break; case METHOD_ADC: s = L"adc"; break; case METHOD_ZLIB: s = L"zlib"; break; case METHOD_BZIP2: s = L"bzip2"; break; default: ConvertUInt64ToString(type, buf); s = buf; } res += s; if (m.NumBlocks != 1) { res += L'['; ConvertUInt64ToString(m.NumBlocks, buf); res += buf; res += L']'; } res += L'-'; res += GetSizeString(m.UnpSize); if (showPack) { res += L'-'; res += GetSizeString(m.PackSize); } } return res; } STATPROPSTG kProps[] = { { NULL, kpidPath, VT_BSTR}, { NULL, kpidSize, VT_UI8}, { NULL, kpidPackSize, VT_UI8}, { NULL, kpidComment, VT_BSTR}, { NULL, kpidMethod, VT_BSTR} }; IMP_IInArchive_Props STATPROPSTG kArcProps[] = { { NULL, kpidMethod, VT_BSTR}, { NULL, kpidNumBlocks, VT_UI4} }; STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value) { COM_TRY_BEGIN NWindows::NCOM::CPropVariant prop; switch(propID) { case kpidMethod: { CMethods m; for (int i = 0; i < _files.Size(); i++) m.Update(_files[i]); prop = m.GetString(); break; } case kpidNumBlocks: { UInt64 numBlocks = 0; for (int i = 0; i < _files.Size(); i++) numBlocks += _files[i].Blocks.Size(); prop = numBlocks; break; } } prop.Detach(value); return S_OK; COM_TRY_END } IMP_IInArchive_ArcProps static int FindKeyPair(const CXmlItem &item, const AString &key, const AString &nextTag) { for (int i = 0; i + 1 < item.SubItems.Size(); i++) { const CXmlItem &si = item.SubItems[i]; if (si.IsTagged("key") && si.GetSubString() == key && item.SubItems[i + 1].IsTagged(nextTag)) return i + 1; } return -1; } static AString GetStringFromKeyPair(const CXmlItem &item, const AString &key, const AString &nextTag) { int index = FindKeyPair(item, key, nextTag); if (index >= 0) return item.SubItems[index].GetSubString(); return AString(); } HRESULT CHandler::Open2(IInStream *stream) { const int HEADER_SIZE = 0x1E0; UInt64 headerPos; RINOK(stream->Seek(-HEADER_SIZE, STREAM_SEEK_END, &headerPos)); Byte buf[HEADER_SIZE]; RINOK(ReadStream_FALSE(stream, buf, HEADER_SIZE)); UInt64 address1 = Get64(buf + 0); UInt64 address2 = Get64(buf + 0xB8); UInt64 size64 = Get64(buf + 0xC0); if (address1 != address2 || size64 >= kXmlSizeMax || size64 == 0 || address1 >= headerPos || address1 + size64 > headerPos) return S_FALSE; RINOK(stream->Seek(address1, STREAM_SEEK_SET, NULL)); size_t size = (size_t)size64; char *ss = _xml.GetBuffer((int)size + 1); RINOK(ReadStream_FALSE(stream, ss, size)); ss[size] = 0; _xml.ReleaseBuffer(); CXml xml; if (!xml.Parse(_xml)) return S_FALSE; if (xml.Root.Name != "plist") return S_FALSE; int dictIndex = xml.Root.FindSubTag("dict"); if (dictIndex < 0) return S_FALSE; const CXmlItem &dictItem = xml.Root.SubItems[dictIndex]; int rfDictIndex = FindKeyPair(dictItem, "resource-fork", "dict"); if (rfDictIndex < 0) return S_FALSE; const CXmlItem &rfDictItem = dictItem.SubItems[rfDictIndex]; int arrIndex = FindKeyPair(rfDictItem, "blkx", "array"); if (arrIndex < 0) return S_FALSE; const CXmlItem &arrItem = rfDictItem.SubItems[arrIndex]; int i; for (i = 0; i < arrItem.SubItems.Size(); i++) { const CXmlItem &item = arrItem.SubItems[i]; if (!item.IsTagged("dict")) continue; CFile file; file.StartPos = 0; int destLen; { AString dataString; AString name = GetStringFromKeyPair(item, "Name", "string"); if (name.IsEmpty()) name = GetStringFromKeyPair(item, "CFName", "string"); file.Name = name; dataString = GetStringFromKeyPair(item, "Data", "data"); destLen = Base64ToBin(NULL, dataString, dataString.Length()); file.Raw.SetCapacity(destLen); Base64ToBin(file.Raw, dataString, dataString.Length()); } if (destLen > 0xCC && Get32(file.Raw) == 0x6D697368) { PRF(printf("\n\n index = %d", _files.Size())); const int kRecordSize = 40; for (int offset = 0xCC; offset + kRecordSize <= destLen; offset += kRecordSize) { const Byte *p = (const Byte *)file.Raw + offset; CBlock b; b.Type = Get32(p); if (b.Type == METHOD_END) break; if (b.Type == METHOD_DUMMY) continue; b.UnpPos = Get64(p + 0x08) << 9; b.UnpSize = Get64(p + 0x10) << 9; b.PackPos = Get64(p + 0x18); b.PackSize = Get64(p + 0x20); file.Blocks.Add(b); PRF(printf("\nType=%8x m[1]=%8x uPos=%8x uSize=%7x pPos=%8x pSize=%7x", b.Type, Get32(p + 4), (UInt32)b.UnpPos, (UInt32)b.UnpSize, (UInt32)b.PackPos, (UInt32)b.PackSize)); } } int itemIndex = _files.Add(file); if (file.Blocks.Size() > 0) { // if (file.Name.Find("HFS") >= 0) _fileIndices.Add(itemIndex); } } // PackPos for each new file is 0 in some DMG files. So we use additional StartPos bool allStartAreZeros = true; for (i = 0; i < _files.Size(); i++) { const CFile &file = _files[i]; if (!file.Blocks.IsEmpty() && file.Blocks[0].PackPos != 0) allStartAreZeros = false; } UInt64 startPos = 0; if (allStartAreZeros) { for (i = 0; i < _files.Size(); i++) { CFile &file = _files[i]; file.StartPos = startPos; if (!file.Blocks.IsEmpty()) startPos += file.Blocks.Back().GetNextPackOffset(); } } return S_OK; } STDMETHODIMP CHandler::Open(IInStream *stream, const UInt64 * /* maxCheckStartPosition */, IArchiveOpenCallback * /* openArchiveCallback */) { COM_TRY_BEGIN { Close(); if (Open2(stream) != S_OK) return S_FALSE; _inStream = stream; } return S_OK; COM_TRY_END } STDMETHODIMP CHandler::Close() { _inStream.Release(); _fileIndices.Clear(); _files.Clear(); _xml.Empty(); return S_OK; } STDMETHODIMP CHandler::GetNumberOfItems(UInt32 *numItems) { *numItems = _fileIndices.Size() #ifdef DMG_SHOW_RAW + _files.Size() + 1; #endif ; return S_OK; } #define RAW_PREFIX L"raw" WSTRING_PATH_SEPARATOR STDMETHODIMP CHandler::GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value) { COM_TRY_BEGIN NWindows::NCOM::CPropVariant prop; #ifdef DMG_SHOW_RAW if ((int)index == _fileIndices.Size()) { switch(propID) { case kpidPath: prop = RAW_PREFIX L"a.xml"; break; case kpidSize: case kpidPackSize: prop = (UInt64)_xml.Length(); break; } } else if ((int)index > _fileIndices.Size()) { int rawIndex = (int)index - (_fileIndices.Size() + 1); switch(propID) { case kpidPath: { wchar_t s[32] = RAW_PREFIX; ConvertUInt64ToString(rawIndex, s + MyStringLen(s)); prop = s; break; } case kpidSize: case kpidPackSize: prop = (UInt64)_files[rawIndex].Raw.GetCapacity(); break; } } else #endif { int itemIndex = _fileIndices[index]; const CFile &item = _files[itemIndex]; switch(propID) { case kpidMethod: { CMethods m; m.Update(item); UString resString = m.GetString(); if (!resString.IsEmpty()) prop = resString; break; } // case kpidExtension: prop = L"hfs"; break; case kpidPath: { // break; UString name; wchar_t s[32]; ConvertUInt64ToString(index, s); name = s; int num = 10; int numDigits; for (numDigits = 1; num < _fileIndices.Size(); numDigits++) num *= 10; while (name.Length() < numDigits) name = L'0' + name; AString subName; int pos1 = item.Name.Find('('); if (pos1 >= 0) { pos1++; int pos2 = item.Name.Find(')', pos1); if (pos2 >= 0) { subName = item.Name.Mid(pos1, pos2 - pos1); pos1 = subName.Find(':'); if (pos1 >= 0) subName = subName.Left(pos1); } } subName.Trim(); if (!subName.IsEmpty()) { if (subName == "Apple_HFS") subName = "hfs"; else if (subName == "Apple_HFSX") subName = "hfsx"; else if (subName == "Apple_Free") subName = "free"; else if (subName == "DDM") subName = "ddm"; UString name2; ConvertUTF8ToUnicode(subName, name2); name += L'.'; name += name2; } else { UString name2; ConvertUTF8ToUnicode(item.Name, name2); if (!name2.IsEmpty()) name += L" - "; name += name2; } prop = name; break; } case kpidComment: { UString name; ConvertUTF8ToUnicode(item.Name, name); prop = name; break; } case kpidSize: prop = item.GetUnpackSize(); break; case kpidPackSize: prop = item.GetPackSize(); break; } } prop.Detach(value); return S_OK; COM_TRY_END } class CAdcDecoder: public ICompressCoder, public CMyUnknownImp { CLzOutWindow m_OutWindowStream; CInBuffer m_InStream; void ReleaseStreams() { m_OutWindowStream.ReleaseStream(); m_InStream.ReleaseStream(); } class CCoderReleaser { CAdcDecoder *m_Coder; public: bool NeedFlush; CCoderReleaser(CAdcDecoder *coder): m_Coder(coder), NeedFlush(true) {} ~CCoderReleaser() { if (NeedFlush) m_Coder->m_OutWindowStream.Flush(); m_Coder->ReleaseStreams(); } }; friend class CCoderReleaser; public: MY_UNKNOWN_IMP STDMETHOD(CodeReal)(ISequentialInStream *inStream, ISequentialOutStream *outStream, const UInt64 *inSize, const UInt64 *outSize, ICompressProgressInfo *progress); STDMETHOD(Code)(ISequentialInStream *inStream, ISequentialOutStream *outStream, const UInt64 *inSize, const UInt64 *outSize, ICompressProgressInfo *progress); }; STDMETHODIMP CAdcDecoder::CodeReal(ISequentialInStream *inStream, ISequentialOutStream *outStream, const UInt64 *inSize, const UInt64 *outSize, ICompressProgressInfo *progress) { if (!m_OutWindowStream.Create(1 << 18)) return E_OUTOFMEMORY; if (!m_InStream.Create(1 << 18)) return E_OUTOFMEMORY; m_OutWindowStream.SetStream(outStream); m_OutWindowStream.Init(false); m_InStream.SetStream(inStream); m_InStream.Init(); CCoderReleaser coderReleaser(this); const UInt32 kStep = (1 << 20); UInt64 nextLimit = kStep; UInt64 pos = 0; while (pos < *outSize) { if (pos > nextLimit && progress) { UInt64 packSize = m_InStream.GetProcessedSize(); RINOK(progress->SetRatioInfo(&packSize, &pos)); nextLimit += kStep; } Byte b; if (!m_InStream.ReadByte(b)) return S_FALSE; UInt64 rem = *outSize - pos; if (b & 0x80) { unsigned num = (b & 0x7F) + 1; if (num > rem) return S_FALSE; for (unsigned i = 0; i < num; i++) { if (!m_InStream.ReadByte(b)) return S_FALSE; m_OutWindowStream.PutByte(b); } pos += num; continue; } Byte b1; if (!m_InStream.ReadByte(b1)) return S_FALSE; UInt32 len, distance; if (b & 0x40) { len = ((UInt32)b & 0x3F) + 4; Byte b2; if (!m_InStream.ReadByte(b2)) return S_FALSE; distance = ((UInt32)b1 << 8) + b2; } else { b &= 0x3F; len = ((UInt32)b >> 2) + 3; distance = (((UInt32)b & 3) << 8) + b1; } if (distance >= pos || len > rem) return S_FALSE; m_OutWindowStream.CopyBlock(distance, len); pos += len; } if (*inSize != m_InStream.GetProcessedSize()) return S_FALSE; coderReleaser.NeedFlush = false; return m_OutWindowStream.Flush(); } STDMETHODIMP CAdcDecoder::Code(ISequentialInStream *inStream, ISequentialOutStream *outStream, const UInt64 *inSize, const UInt64 *outSize, ICompressProgressInfo *progress) { try { return CodeReal(inStream, outStream, inSize, outSize, progress);} catch(const CInBufferException &e) { return e.ErrorCode; } catch(const CLzOutWindowException &e) { return e.ErrorCode; } catch(...) { return S_FALSE; } } STDMETHODIMP CHandler::Extract(const UInt32 *indices, UInt32 numItems, Int32 testMode, IArchiveExtractCallback *extractCallback) { COM_TRY_BEGIN bool allFilesMode = (numItems == (UInt32)-1); if (allFilesMode) numItems = _files.Size(); if (numItems == 0) return S_OK; UInt64 totalSize = 0; UInt32 i; for (i = 0; i < numItems; i++) { int index = (int)(allFilesMode ? i : indices[i]); #ifdef DMG_SHOW_RAW if (index == _fileIndices.Size()) totalSize += _xml.Length(); else if (index > _fileIndices.Size()) totalSize += _files[index - (_fileIndices.Size() + 1)].Raw.GetCapacity(); else #endif totalSize += _files[_fileIndices[index]].GetUnpackSize(); } extractCallback->SetTotal(totalSize); UInt64 currentPackTotal = 0; UInt64 currentUnpTotal = 0; UInt64 currentPackSize = 0; UInt64 currentUnpSize = 0; const UInt32 kZeroBufSize = (1 << 14); CByteBuffer zeroBuf; zeroBuf.SetCapacity(kZeroBufSize); memset(zeroBuf, 0, kZeroBufSize); NCompress::CCopyCoder *copyCoderSpec = new NCompress::CCopyCoder(); CMyComPtr copyCoder = copyCoderSpec; NCompress::NBZip2::CDecoder *bzip2CoderSpec = new NCompress::NBZip2::CDecoder(); CMyComPtr bzip2Coder = bzip2CoderSpec; NCompress::NZlib::CDecoder *zlibCoderSpec = new NCompress::NZlib::CDecoder(); CMyComPtr zlibCoder = zlibCoderSpec; CAdcDecoder *adcCoderSpec = new CAdcDecoder(); CMyComPtr adcCoder = adcCoderSpec; CLocalProgress *lps = new CLocalProgress; CMyComPtr progress = lps; lps->Init(extractCallback, false); CLimitedSequentialInStream *streamSpec = new CLimitedSequentialInStream; CMyComPtr inStream(streamSpec); streamSpec->SetStream(_inStream); for (i = 0; i < numItems; i++, currentPackTotal += currentPackSize, currentUnpTotal += currentUnpSize) { lps->InSize = currentPackTotal; lps->OutSize = currentUnpTotal; currentPackSize = 0; currentUnpSize = 0; RINOK(lps->SetCur()); CMyComPtr realOutStream; Int32 askMode = testMode ? NExtract::NAskMode::kTest : NExtract::NAskMode::kExtract; Int32 index = allFilesMode ? i : indices[i]; // const CItemEx &item = _files[index]; RINOK(extractCallback->GetStream(index, &realOutStream, askMode)); if (!testMode && !realOutStream) continue; RINOK(extractCallback->PrepareOperation(askMode)); CLimitedSequentialOutStream *outStreamSpec = new CLimitedSequentialOutStream; CMyComPtr outStream(outStreamSpec); outStreamSpec->SetStream(realOutStream); realOutStream.Release(); Int32 opRes = NExtract::NOperationResult::kOK; #ifdef DMG_SHOW_RAW if (index > _fileIndices.Size()) { const CByteBuffer &buf = _files[index - (_fileIndices.Size() + 1)].Raw; outStreamSpec->Init(buf.GetCapacity()); RINOK(WriteStream(outStream, buf, buf.GetCapacity())); currentPackSize = currentUnpSize = buf.GetCapacity(); } else if (index == _fileIndices.Size()) { outStreamSpec->Init(_xml.Length()); RINOK(WriteStream(outStream, (const char *)_xml, _xml.Length())); currentPackSize = currentUnpSize = _xml.Length(); } else #endif { const CFile &item = _files[_fileIndices[index]]; currentPackSize = item.GetPackSize(); currentUnpSize = item.GetUnpackSize(); UInt64 unpPos = 0; UInt64 packPos = 0; { for (int j = 0; j < item.Blocks.Size(); j++) { lps->InSize = currentPackTotal + packPos; lps->OutSize = currentUnpTotal + unpPos; RINOK(lps->SetCur()); const CBlock &block = item.Blocks[j]; packPos += block.PackSize; if (block.UnpPos != unpPos) { opRes = NExtract::NOperationResult::kDataError; break; } RINOK(_inStream->Seek(item.StartPos + block.PackPos, STREAM_SEEK_SET, NULL)); streamSpec->Init(block.PackSize); // UInt64 startSize = outStreamSpec->GetSize(); bool realMethod = true; outStreamSpec->Init(block.UnpSize); HRESULT res = S_OK; switch(block.Type) { case METHOD_ZERO_0: case METHOD_ZERO_2: realMethod = false; if (block.PackSize != 0) opRes = NExtract::NOperationResult::kUnSupportedMethod; break; case METHOD_COPY: if (block.UnpSize != block.PackSize) { opRes = NExtract::NOperationResult::kUnSupportedMethod; break; } res = copyCoder->Code(inStream, outStream, NULL, NULL, progress); break; case METHOD_ADC: { res = adcCoder->Code(inStream, outStream, &block.PackSize, &block.UnpSize, progress); break; } case METHOD_ZLIB: { res = zlibCoder->Code(inStream, outStream, NULL, NULL, progress); break; } case METHOD_BZIP2: { res = bzip2Coder->Code(inStream, outStream, NULL, NULL, progress); if (res == S_OK) if (streamSpec->GetSize() != block.PackSize) opRes = NExtract::NOperationResult::kDataError; break; } default: opRes = NExtract::NOperationResult::kUnSupportedMethod; break; } if (res != S_OK) { if (res != S_FALSE) return res; if (opRes == NExtract::NOperationResult::kOK) opRes = NExtract::NOperationResult::kDataError; } unpPos += block.UnpSize; if (!outStreamSpec->IsFinishedOK()) { if (realMethod && opRes == NExtract::NOperationResult::kOK) opRes = NExtract::NOperationResult::kDataError; while (outStreamSpec->GetRem() != 0) { UInt64 rem = outStreamSpec->GetRem(); UInt32 size = (UInt32)MyMin(rem, (UInt64)kZeroBufSize); RINOK(WriteStream(outStream, zeroBuf, size)); } } } } } outStream.Release(); RINOK(extractCallback->SetOperationResult(opRes)); } return S_OK; COM_TRY_END } static IInArchive *CreateArc() { return new CHandler; } static CArcInfo g_ArcInfo = { L"Dmg", L"dmg", 0, 0xE4, { 0 }, 0, false, CreateArc, 0 }; REGISTER_ARC(Dmg) }}