The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
// VhdHandler.cpp

#include "StdAfx.h"

#include "../../../C/CpuArch.h"

#include "Common/Buffer.h"
#include "Common/ComTry.h"
#include "Common/IntToString.h"
#include "Common/MyString.h"

#include "Windows/PropVariant.h"

#include "../Common/LimitedStreams.h"
#include "../Common/ProgressUtils.h"
#include "../Common/RegisterArc.h"
#include "../Common/StreamUtils.h"

#include "../Compress/CopyCoder.h"

#define Get16(p) GetBe16(p)
#define Get32(p) GetBe32(p)
#define Get64(p) GetBe64(p)

#define G32(p, dest) dest = Get32(p);
#define G64(p, dest) dest = Get64(p);

using namespace NWindows;

namespace NArchive {
namespace NVhd {

static const UInt32 kUnusedBlock = 0xFFFFFFFF;

static const UInt32 kDiskType_Fixed = 2;
static const UInt32 kDiskType_Dynamic = 3;
static const UInt32 kDiskType_Diff = 4;

static const char *kDiskTypes[] =
{
  "0",
  "1",
  "Fixed",
  "Dynamic",
  "Differencing"
};

struct CFooter
{
  // UInt32 Features;
  // UInt32 FormatVersion;
  UInt64 DataOffset;
  UInt32 CTime;
  UInt32 CreatorApp;
  UInt32 CreatorVersion;
  UInt32 CreatorHostOS;
  // UInt64 OriginalSize;
  UInt64 CurrentSize;
  UInt32 DiskGeometry;
  UInt32 Type;
  Byte Id[16];
  Byte SavedState;

  bool IsFixed() const { return Type == kDiskType_Fixed; }
  bool ThereIsDynamic() const { return Type == kDiskType_Dynamic || Type == kDiskType_Diff; }
  // bool IsSupported() const { return Type == kDiskType_Fixed || Type == kDiskType_Dynamic || Type == kDiskType_Diff; }
  UInt32 NumCyls() const { return DiskGeometry >> 16; }
  UInt32 NumHeads() const { return (DiskGeometry >> 8) & 0xFF; }
  UInt32 NumSectorsPerTrack() const { return DiskGeometry & 0xFF; }
  AString GetTypeString() const;
  bool Parse(const Byte *p);
};

AString CFooter::GetTypeString() const
{
  if (Type < sizeof(kDiskTypes) / sizeof(kDiskTypes[0]))
    return kDiskTypes[Type];
  char s[16];
  ConvertUInt32ToString(Type, s);
  return s;
}

static bool CheckBlock(const Byte *p, unsigned size, unsigned checkSumOffset, unsigned zeroOffset)
{
  UInt32 sum = 0;
  unsigned i;
  for (i = 0; i < checkSumOffset; i++)
    sum += p[i];
  for (i = checkSumOffset + 4; i < size; i++)
    sum += p[i];
  if (~sum != Get32(p + checkSumOffset))
    return false;
  for (i = zeroOffset; i < size; i++)
    if (p[i] != 0)
      return false;
  return true;
}

bool CFooter::Parse(const Byte *p)
{
  if (memcmp(p, "conectix", 8) != 0)
    return false;
  // G32(p + 0x08, Features);
  // G32(p + 0x0C, FormatVersion);
  G64(p + 0x10, DataOffset);
  G32(p + 0x18, CTime);
  G32(p + 0x1C, CreatorApp);
  G32(p + 0x20, CreatorVersion);
  G32(p + 0x24, CreatorHostOS);
  // G64(p + 0x28, OriginalSize);
  G64(p + 0x30, CurrentSize);
  G32(p + 0x38, DiskGeometry);
  G32(p + 0x3C, Type);
  memcpy(Id, p + 0x44, 16);
  SavedState = p[0x54];
  return CheckBlock(p, 512, 0x40, 0x55);
}

/*
struct CParentLocatorEntry
{
  UInt32 Code;
  UInt32 DataSpace;
  UInt32 DataLen;
  UInt64 DataOffset;

  bool Parse(const Byte *p);
};
bool CParentLocatorEntry::Parse(const Byte *p)
{
  G32(p + 0x00, Code);
  G32(p + 0x04, DataSpace);
  G32(p + 0x08, DataLen);
  G32(p + 0x10, DataOffset);
  return (Get32(p + 0x0C) == 0); // Resrved
}
*/

struct CDynHeader
{
  // UInt64 DataOffset;
  UInt64 TableOffset;
  // UInt32 HeaderVersion;
  UInt32 NumBlocks;
  int BlockSizeLog;
  UInt32 ParentTime;
  Byte ParentId[16];
  UString ParentName;
  // CParentLocatorEntry ParentLocators[8];

  bool Parse(const Byte *p);
  UInt32 NumBitMapSectors() const
  {
    UInt32 numSectorsInBlock = (1 << (BlockSizeLog - 9));
    return (numSectorsInBlock + 512 * 8 - 1) / (512 * 8);
  }
};

static int GetLog(UInt32 num)
{
  for (int i = 0; i < 31; i++)
    if (((UInt32)1 << i) == num)
      return i;
  return -1;
}

bool CDynHeader::Parse(const Byte *p)
{
  if (memcmp(p, "cxsparse", 8) != 0)
    return false;
  // G64(p + 0x08, DataOffset);
  G64(p + 0x10, TableOffset);
  // G32(p + 0x18, HeaderVersion);
  G32(p + 0x1C, NumBlocks);
  BlockSizeLog = GetLog(Get32(p + 0x20));
  if (BlockSizeLog < 9 || BlockSizeLog > 30)
    return false;
  G32(p + 0x38, ParentTime);
  if (Get32(p + 0x3C) != 0) // reserved
    return false;
  memcpy(ParentId, p + 0x28, 16);
  {
    const int kNameLength = 256;
    wchar_t *s = ParentName.GetBuffer(kNameLength);
    for (unsigned i = 0; i < kNameLength; i++)
      s[i] = Get16(p + 0x40 + i * 2);
    s[kNameLength] = 0;
    ParentName.ReleaseBuffer();
  }
  /*
  for (int i = 0; i < 8; i++)
    if (!ParentLocators[i].Parse(p + 0x240 + i * 24))
      return false;
  */
  return CheckBlock(p, 1024, 0x24, 0x240 + 8 * 24);
}

class CHandler:
  public IInStream,
  public IInArchive,
  public IInArchiveGetStream,
  public CMyUnknownImp
{
  UInt64 _virtPos;
  UInt64 _phyPos;
  UInt64 _phyLimit;

  CFooter Footer;
  CDynHeader Dyn;
  CRecordVector<UInt32> Bat;
  CByteBuffer BitMap;
  UInt32 BitMapTag;
  UInt32 NumUsedBlocks;
  CMyComPtr<IInStream> Stream;
  CMyComPtr<IInStream> ParentStream;
  CHandler *Parent;

  HRESULT Seek(UInt64 offset);
  HRESULT InitAndSeek();
  HRESULT ReadPhy(UInt64 offset, void *data, UInt32 size);

  bool NeedParent() const { return Footer.Type == kDiskType_Diff; }
  UInt64 GetPackSize() const
    { return Footer.ThereIsDynamic() ? ((UInt64)NumUsedBlocks << Dyn.BlockSizeLog) : Footer.CurrentSize; }

  UString GetParentName() const
  {
    const CHandler *p = this;
    UString res;
    while (p && p->NeedParent())
    {
      if (!res.IsEmpty())
        res += L" -> ";
      res += p->Dyn.ParentName;
      p = p->Parent;
    }
    return res;
  }

  bool IsOK() const
  {
    const CHandler *p = this;
    while (p->NeedParent())
    {
      p = p->Parent;
      if (p == 0)
        return false;
    }
    return true;
  }

  HRESULT Open3();
  HRESULT Open2(IInStream *stream, CHandler *child, IArchiveOpenCallback *openArchiveCallback, int level);

public:
  MY_UNKNOWN_IMP3(IInArchive, IInArchiveGetStream, IInStream)

  INTERFACE_IInArchive(;)
  STDMETHOD(GetStream)(UInt32 index, ISequentialInStream **stream);
  STDMETHOD(Read)(void *data, UInt32 size, UInt32 *processedSize);
  STDMETHOD(Seek)(Int64 offset, UInt32 seekOrigin, UInt64 *newPosition);
};

HRESULT CHandler::Seek(UInt64 offset) { return Stream->Seek(offset, STREAM_SEEK_SET, NULL); }

HRESULT CHandler::InitAndSeek()
{
  if (ParentStream)
  {
    RINOK(Parent->InitAndSeek());
  }
  _virtPos = _phyPos = 0;
  BitMapTag = kUnusedBlock;
  BitMap.SetCapacity(Dyn.NumBitMapSectors() << 9);
  return Seek(0);
}

HRESULT CHandler::ReadPhy(UInt64 offset, void *data, UInt32 size)
{
  if (offset + size > _phyLimit)
    return S_FALSE;
  if (offset != _phyPos)
  {
    _phyPos = offset;
    RINOK(Seek(offset));
  }
  HRESULT res = ReadStream_FALSE(Stream, data, size);
  _phyPos += size;
  return res;
}

HRESULT CHandler::Open3()
{
  RINOK(Stream->Seek(0, STREAM_SEEK_END, &_phyPos));
  if (_phyPos < 512)
    return S_FALSE;
  const UInt32 kDynSize = 1024;
  Byte buf[kDynSize];

  _phyLimit = _phyPos;
  RINOK(ReadPhy(_phyLimit - 512, buf, 512));
  if (!Footer.Parse(buf))
    return S_FALSE;
  _phyLimit -= 512;

  if (!Footer.ThereIsDynamic())
    return S_OK;

  RINOK(ReadPhy(0, buf + 512, 512));
  if (memcmp(buf, buf + 512, 512) != 0)
    return S_FALSE;

  RINOK(ReadPhy(Footer.DataOffset, buf, kDynSize));
  if (!Dyn.Parse(buf))
    return S_FALSE;
  
  if (Dyn.NumBlocks >= (UInt32)1 << 31)
    return S_FALSE;
  if (Footer.CurrentSize == 0)
  {
    if (Dyn.NumBlocks != 0)
      return S_FALSE;
  }
  else if (((Footer.CurrentSize - 1) >> Dyn.BlockSizeLog) + 1 != Dyn.NumBlocks)
    return S_FALSE;

  Bat.Reserve(Dyn.NumBlocks);
  while ((UInt32)Bat.Size() < Dyn.NumBlocks)
  {
    RINOK(ReadPhy(Dyn.TableOffset + (UInt64)Bat.Size() * 4, buf, 512));
    for (UInt32 j = 0; j < 512; j += 4)
    {
      UInt32 v = Get32(buf + j);
      if (v != kUnusedBlock)
        NumUsedBlocks++;
      Bat.Add(v);
      if ((UInt32)Bat.Size() >= Dyn.NumBlocks)
        break;
    }
  }
  return S_OK;
}

STDMETHODIMP CHandler::Read(void *data, UInt32 size, UInt32 *processedSize)
{
  if (processedSize != NULL)
    *processedSize = 0;
  if (_virtPos >= Footer.CurrentSize)
    return (Footer.CurrentSize == _virtPos) ? S_OK: E_FAIL;
  UInt64 rem = Footer.CurrentSize - _virtPos;
  if (size > rem)
    size = (UInt32)rem;
  if (size == 0)
    return S_OK;
  UInt32 blockIndex = (UInt32)(_virtPos >> Dyn.BlockSizeLog);
  UInt32 blockSectIndex = Bat[blockIndex];
  UInt32 blockSize = (UInt32)1 << Dyn.BlockSizeLog;
  UInt32 offsetInBlock = (UInt32)_virtPos & (blockSize - 1);
  size = MyMin(blockSize - offsetInBlock, size);

  HRESULT res = S_OK;
  if (blockSectIndex == kUnusedBlock)
  {
    if (ParentStream)
    {
      RINOK(ParentStream->Seek(_virtPos, STREAM_SEEK_SET, NULL));
      res = ParentStream->Read(data, size, &size);
    }
    else
      memset(data, 0, size);
  }
  else
  {
    UInt64 newPos = (UInt64)blockSectIndex << 9;
    if (BitMapTag != blockIndex)
    {
      RINOK(ReadPhy(newPos, BitMap, (UInt32)BitMap.GetCapacity()));
      BitMapTag = blockIndex;
    }
    RINOK(ReadPhy(newPos + BitMap.GetCapacity() + offsetInBlock, data, size));
    for (UInt32 cur = 0; cur < size;)
    {
      UInt32 rem = MyMin(0x200 - (offsetInBlock & 0x1FF), size - cur);
      UInt32 bmi = offsetInBlock >> 9;
      if (((BitMap[bmi >> 3] >> (7 - (bmi & 7))) & 1) == 0)
      {
        if (ParentStream)
        {
          RINOK(ParentStream->Seek(_virtPos + cur, STREAM_SEEK_SET, NULL));
          RINOK(ReadStream_FALSE(ParentStream, (Byte *)data + cur, rem));
        }
        else
        {
          const Byte *p = (const Byte *)data + cur;
          for (UInt32 i = 0; i < rem; i++)
            if (p[i] != 0)
              return S_FALSE;
        }
      }
      offsetInBlock += rem;
      cur += rem;
    }
  }
  if (processedSize != NULL)
    *processedSize = size;
  _virtPos += size;
  return res;
}

STDMETHODIMP CHandler::Seek(Int64 offset, UInt32 seekOrigin, UInt64 *newPosition)
{
  switch(seekOrigin)
  {
    case STREAM_SEEK_SET: _virtPos = offset; break;
    case STREAM_SEEK_CUR: _virtPos += offset; break;
    case STREAM_SEEK_END: _virtPos = Footer.CurrentSize + offset; break;
    default: return STG_E_INVALIDFUNCTION;
  }
  if (newPosition)
    *newPosition = _virtPos;
  return S_OK;
}

enum
{
  kpidParent = kpidUserDefined,
  kpidSavedState
};

STATPROPSTG kArcProps[] =
{
  { NULL, kpidSize, VT_UI8},
  { NULL, kpidCTime, VT_FILETIME},
  { NULL, kpidClusterSize, VT_UI8},
  { NULL, kpidMethod, VT_BSTR},
  { L"Parent", kpidParent, VT_BSTR},
  { NULL, kpidCreatorApp, VT_BSTR},
  { NULL, kpidHostOS, VT_BSTR},
  { L"Saved State", kpidSavedState, VT_BOOL},
  { NULL, kpidId, VT_BSTR}
 };

STATPROPSTG kProps[] =
{
  { NULL, kpidSize, VT_UI8},
  { NULL, kpidPackSize, VT_UI8},
  { NULL, kpidCTime, VT_FILETIME}
  
  /*
  { NULL, kpidNumCyls, VT_UI4},
  { NULL, kpidNumHeads, VT_UI4},
  { NULL, kpidSectorsPerTrack, VT_UI4}
  */
};

IMP_IInArchive_Props
IMP_IInArchive_ArcProps_WITH_NAME

// VHD start time: 2000-01-01
static const UInt64 kVhdTimeStartValue = (UInt64)3600 * 24 * (399 * 365 + 24 * 4);

static void VhdTimeToFileTime(UInt32 vhdTime, NCOM::CPropVariant &prop)
{
  FILETIME ft, utc;
  UInt64 v = (kVhdTimeStartValue + vhdTime) * 10000000;
  ft.dwLowDateTime = (DWORD)v;
  ft.dwHighDateTime = (DWORD)(v >> 32);
  // specification says that it's UTC time, but Virtual PC 6 writes local time. Why?
  LocalFileTimeToFileTime(&ft, &utc);
  prop = utc;
}

static void StringToAString(char *dest, UInt32 s)
{
  for (int i = 24; i >= 0; i -= 8)
  {
    Byte b = (Byte)((s >> i) & 0xFF);
    if (b < 0x20 || b > 0x7F)
      break;
    *dest++ = b;
  }
  *dest = 0;
}

static void ConvertByteToHex(unsigned value, char *s)
{
  for (int i = 0; i < 2; i++)
  {
    unsigned t = value & 0xF;
    value >>= 4;
    s[1 - i] = (char)((t < 10) ? ('0' + t) : ('A' + (t - 10)));
  }
}

STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NCOM::CPropVariant prop;
  switch(propID)
  {
    case kpidMainSubfile: prop = (UInt32)0; break;
    case kpidCTime: VhdTimeToFileTime(Footer.CTime, prop); break;
    case kpidClusterSize: if (Footer.ThereIsDynamic()) prop = (UInt32)1 << Dyn.BlockSizeLog; break;
    case kpidMethod:
    {
      AString s = Footer.GetTypeString();
      if (NeedParent())
      {
        s += " -> ";
        const CHandler *p = this;
        while (p != 0 && p->NeedParent())
          p = p->Parent;
        if (p == 0)
          s += '?';
        else
          s += p->Footer.GetTypeString();
      }
      prop = s;
      break;
    }
    case kpidCreatorApp:
    {
      char s[16];
      StringToAString(s, Footer.CreatorApp);
      AString res = s;
      res.Trim();
      ConvertUInt32ToString(Footer.CreatorVersion >> 16, s);
      res += ' ';
      res += s;
      res += '.';
      ConvertUInt32ToString(Footer.CreatorVersion & 0xFFFF, s);
      res += s;
      prop = res;
      break;
    }
    case kpidHostOS:
    {
      if (Footer.CreatorHostOS == 0x5769326b)
        prop = "Windows";
      else
      {
        char s[16];
        StringToAString(s, Footer.CreatorHostOS);
        prop = s;
      }
      break;
    }
    case kpidId:
    {
      char s[32 + 4];
      for (int i = 0; i < 16; i++)
        ConvertByteToHex(Footer.Id[i], s + i * 2);
      s[32] = 0;
      prop = s;
      break;
    }
    case kpidSavedState: prop = Footer.SavedState ? true : false; break;
    case kpidParent: if (NeedParent()) prop = GetParentName(); break;
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

HRESULT CHandler::Open2(IInStream *stream, CHandler *child, IArchiveOpenCallback *openArchiveCallback, int level)
{
  Close();
  Stream = stream;
  if (level > 32)
    return S_FALSE;
  RINOK(Open3());
  if (child && memcmp(child->Dyn.ParentId, Footer.Id, 16) != 0)
    return S_FALSE;
  if (Footer.Type != kDiskType_Diff)
    return S_OK;
  CMyComPtr<IArchiveOpenVolumeCallback> openVolumeCallback;
  if (openArchiveCallback->QueryInterface(IID_IArchiveOpenVolumeCallback, (void **)&openVolumeCallback) != S_OK)
    return S_FALSE;
  CMyComPtr<IInStream> nextStream;
  HRESULT res = openVolumeCallback->GetStream(Dyn.ParentName, &nextStream);
  if (res == S_FALSE)
    return S_OK;
  RINOK(res);

  Parent = new CHandler;
  ParentStream = Parent;
  return Parent->Open2(nextStream, this, openArchiveCallback, level + 1);
}

STDMETHODIMP CHandler::Open(IInStream *stream,
    const UInt64 * /* maxCheckStartPosition */,
    IArchiveOpenCallback * openArchiveCallback)
{
  COM_TRY_BEGIN
  {
    HRESULT res;
    try
    {
      res = Open2(stream, NULL, openArchiveCallback, 0);
      if (res == S_OK)
        return S_OK;
    }
    catch(...)
    {
      Close();
      throw;
    }
    Close();
    return res;
  }
  COM_TRY_END
}

STDMETHODIMP CHandler::Close()
{
  Bat.Clear();
  NumUsedBlocks = 0;
  Parent = 0;
  Stream.Release();
  ParentStream.Release();
  return S_OK;
}

STDMETHODIMP CHandler::GetNumberOfItems(UInt32 *numItems)
{
  *numItems = 1;
  return S_OK;
}

STDMETHODIMP CHandler::GetProperty(UInt32 /* index */, PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NWindows::NCOM::CPropVariant prop;

  switch(propID)
  {
    case kpidSize: prop = Footer.CurrentSize; break;
    case kpidPackSize: prop = GetPackSize(); break;
    case kpidCTime: VhdTimeToFileTime(Footer.CTime, prop); break;
    /*
    case kpidNumCyls: prop = Footer.NumCyls(); break;
    case kpidNumHeads: prop = Footer.NumHeads(); break;
    case kpidSectorsPerTrack: prop = Footer.NumSectorsPerTrack(); break;
    */
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::Extract(const UInt32 *indices, UInt32 numItems,
    Int32 testMode, IArchiveExtractCallback *extractCallback)
{
  COM_TRY_BEGIN
  if (numItems == 0)
    return S_OK;
  if (numItems != (UInt32)-1 && (numItems != 1 || indices[0] != 0))
    return E_INVALIDARG;

  RINOK(extractCallback->SetTotal(Footer.CurrentSize));
  CMyComPtr<ISequentialOutStream> outStream;
  Int32 askMode = testMode ?
      NExtract::NAskMode::kTest :
      NExtract::NAskMode::kExtract;
  RINOK(extractCallback->GetStream(0, &outStream, askMode));
  if (!testMode && !outStream)
    return S_OK;
  RINOK(extractCallback->PrepareOperation(askMode));

  NCompress::CCopyCoder *copyCoderSpec = new NCompress::CCopyCoder();
  CMyComPtr<ICompressCoder> copyCoder = copyCoderSpec;

  CLocalProgress *lps = new CLocalProgress;
  CMyComPtr<ICompressProgressInfo> progress = lps;
  lps->Init(extractCallback, false);

  int res = NExtract::NOperationResult::kDataError;
  CMyComPtr<ISequentialInStream> inStream;
  HRESULT hres = GetStream(0, &inStream);
  if (hres == S_FALSE)
    res = NExtract::NOperationResult::kUnSupportedMethod;
  else
  {
    RINOK(hres);
    HRESULT hres = copyCoder->Code(inStream, outStream, NULL, NULL, progress);
    if (hres == S_OK)
    {
      if (copyCoderSpec->TotalSize == Footer.CurrentSize)
        res = NExtract::NOperationResult::kOK;
    }
    else
    {
      if (hres != S_FALSE)
      {
        RINOK(hres);
      }
    }
  }
  outStream.Release();
  return extractCallback->SetOperationResult(res);
  COM_TRY_END
}

STDMETHODIMP CHandler::GetStream(UInt32 /* index */, ISequentialInStream **stream)
{
  COM_TRY_BEGIN
  *stream = 0;
  if (Footer.IsFixed())
  {
    CLimitedInStream *streamSpec = new CLimitedInStream;
    CMyComPtr<ISequentialInStream> streamTemp = streamSpec;
    streamSpec->SetStream(Stream);
    streamSpec->InitAndSeek(0, Footer.CurrentSize);
    RINOK(streamSpec->SeekToStart());
    *stream = streamTemp.Detach();
    return S_OK;
  }
  if (!Footer.ThereIsDynamic() || !IsOK())
    return S_FALSE;
  CMyComPtr<ISequentialInStream> streamTemp = this;
  RINOK(InitAndSeek());
  *stream = streamTemp.Detach();
  return S_OK;
  COM_TRY_END
}

static IInArchive *CreateArc() { return new CHandler; }

static CArcInfo g_ArcInfo =
  { L"VHD", L"vhd", L".mbr", 0xDC, { 'c', 'o', 'n', 'e', 'c', 't', 'i', 'x', 0, 0 }, 10, false, CreateArc, 0 };

REGISTER_ARC(Vhd)

}}