// HfsHandler.cpp

#include "StdAfx.h"

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

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

#include "../../Windows/PropVariantUtils.h"

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

#include "HfsHandler.h"

/* if HFS_SHOW_ALT_STREAMS is defined, the handler will show attribute files
   and resource forks. In most cases it looks useless. So we disable it. */
 
#define HFS_SHOW_ALT_STREAMS

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

#define Get16a(p) GetBe16a(p)
#define Get32a(p) GetBe32a(p)

namespace NArchive {
namespace NHfs {

static const char * const kResFileName = "rsrc"; // "com.apple.ResourceFork";

struct CExtent
{
  UInt32 Pos;
  UInt32 NumBlocks;
};

struct CIdExtents
{
  UInt32 ID;
  UInt32 StartBlock;
  CRecordVector<CExtent> Extents;
};

struct CFork
{
  UInt64 Size;
  UInt32 NumBlocks;
  // UInt32 ClumpSize;
  CRecordVector<CExtent> Extents;

  CFork(): Size(0), NumBlocks(0) {}

  void Parse(const Byte *p);

  bool IsEmpty() const { return Size == 0 && NumBlocks == 0 && Extents.Size() == 0; }

  UInt32 Calc_NumBlocks_from_Extents() const;
  bool Check_NumBlocks() const;

  bool Check_Size_with_NumBlocks(unsigned blockSizeLog) const
  {
    return Size <= ((UInt64)NumBlocks << blockSizeLog);
  }

  bool IsOk(unsigned blockSizeLog) const
  {
    // we don't check cases with extra (empty) blocks in last extent
    return Check_NumBlocks() && Check_Size_with_NumBlocks(blockSizeLog);
  }

  bool Upgrade(const CObjectVector<CIdExtents> &items, UInt32 id);
  bool UpgradeAndTest(const CObjectVector<CIdExtents> &items, UInt32 id, unsigned blockSizeLog)
  {
    if (!Upgrade(items, id))
      return false;
    return IsOk(blockSizeLog);
  }
};

static const unsigned kNumFixedExtents = 8;
static const unsigned kForkRecSize = 16 + kNumFixedExtents * 8;


void CFork::Parse(const Byte *p)
{
  Extents.Clear();
  Size = Get64(p);
  // ClumpSize = Get32(p + 8);
  NumBlocks = Get32(p + 12);
  p += 16;
  for (unsigned i = 0; i < kNumFixedExtents; i++, p += 8)
  {
    CExtent e;
    e.Pos = Get32(p);
    e.NumBlocks = Get32(p + 4);
    if (e.NumBlocks != 0)
      Extents.Add(e);
  }
}

UInt32 CFork::Calc_NumBlocks_from_Extents() const
{
  UInt32 num = 0;
  FOR_VECTOR (i, Extents)
    num += Extents[i].NumBlocks;
  return num;
}

bool CFork::Check_NumBlocks() const
{
  UInt32 num = NumBlocks;
  FOR_VECTOR (i, Extents)
  {
    const UInt32 cur = Extents[i].NumBlocks;
    if (num < cur)
      return false;
    num -= cur;
  }
  return num == 0;
}

struct CIdIndexPair
{
  UInt32 ID;
  unsigned Index;

  int Compare(const CIdIndexPair &a) const;
};

#define RINOZ(x) { const int _t_ = (x); if (_t_ != 0) return _t_; }

int CIdIndexPair::Compare(const CIdIndexPair &a) const
{
  RINOZ(MyCompare(ID, a.ID))
  return MyCompare(Index, a.Index);
}

static int FindItemIndex(const CRecordVector<CIdIndexPair> &items, UInt32 id)
{
  unsigned left = 0, right = items.Size();
  while (left != right)
  {
    const unsigned mid = (left + right) / 2;
    const UInt32 midVal = items[mid].ID;
    if (id == midVal)
      return (int)items[mid].Index;
    if (id < midVal)
      right = mid;
    else
      left = mid + 1;
  }
  return -1;
}

static int Find_in_IdExtents(const CObjectVector<CIdExtents> &items, UInt32 id)
{
  unsigned left = 0, right = items.Size();
  while (left != right)
  {
    const unsigned mid = (left + right) / 2;
    const UInt32 midVal = items[mid].ID;
    if (id == midVal)
      return (int)mid;
    if (id < midVal)
      right = mid;
    else
      left = mid + 1;
  }
  return -1;
}

bool CFork::Upgrade(const CObjectVector<CIdExtents> &items, UInt32 id)
{
  const int index = Find_in_IdExtents(items, id);
  if (index < 0)
    return true;
  const CIdExtents &item = items[index];
  if (Calc_NumBlocks_from_Extents() != item.StartBlock)
    return false;
  Extents += item.Extents;
  return true;
}


struct CVolHeader
{
  unsigned BlockSizeLog;
  UInt32 NumFiles;
  UInt32 NumFolders;
  UInt32 NumBlocks;
  UInt32 NumFreeBlocks;

  bool Is_Hsfx_ver5;
  // UInt32 Attr;
  // UInt32 LastMountedVersion;
  // UInt32 JournalInfoBlock;

  UInt32 CTime;
  UInt32 MTime;
  // UInt32 BackupTime;
  // UInt32 CheckedTime;
  
  // UInt32 WriteCount;
  // UInt32 FinderInfo[8];
  // UInt64 VolID;

  UInt64 GetPhySize() const { return (UInt64)NumBlocks << BlockSizeLog; }
  UInt64 GetFreeSize() const { return (UInt64)NumFreeBlocks << BlockSizeLog; }
  bool IsHfsX() const { return Is_Hsfx_ver5; }
};

inline void HfsTimeToFileTime(UInt32 hfsTime, FILETIME &ft)
{
  UInt64 v = ((UInt64)3600 * 24 * (365 * 303 + 24 * 3) + hfsTime) * 10000000;
  ft.dwLowDateTime = (DWORD)v;
  ft.dwHighDateTime = (DWORD)(v >> 32);
}

enum ERecordType
{
  RECORD_TYPE_FOLDER = 1,
  RECORD_TYPE_FILE,
  RECORD_TYPE_FOLDER_THREAD,
  RECORD_TYPE_FILE_THREAD
};



// static const UInt32 kMethod_1_NO_COMPRESSION = 1; // in xattr
static const UInt32 kMethod_ZLIB_ATTR = 3;
static const UInt32 kMethod_ZLIB_RSRC = 4;
// static const UInt32 kMethod_DEDUP = 5; // de-dup within the generation store
// macos 10.10
static const UInt32 kMethod_LZVN_ATTR = 7;
static const UInt32 kMethod_LZVN_RSRC = 8;
static const UInt32 kMethod_COPY_ATTR = 9;
static const UInt32 kMethod_COPY_RSRC = 10;
// macos 10.11
// static const UInt32 kMethod_LZFSE_ATTR = 11;
static const UInt32 kMethod_LZFSE_RSRC = 12;

// static const UInt32 kMethod_ZBM_RSRC = 14;

static const char * const g_Methods[] =
{
    NULL
  , NULL
  , NULL
  , "ZLIB-attr"
  , "ZLIB-rsrc"
  , NULL
  , NULL
  , "LZVN-attr"
  , "LZVN-rsrc"
  , "COPY-attr"
  , "COPY-rsrc"
  , "LZFSE-attr"
  , "LZFSE-rsrc"
  , NULL
  , "ZBM-rsrc"
};


static const Byte k_COPY_Uncompressed_Marker = 0xcc;
static const Byte k_LZVN_Uncompressed_Marker = 6;

void CCompressHeader::Parse(const Byte *p, size_t dataSize)
{
  Clear();

  if (dataSize < k_decmpfs_HeaderSize)
    return;
  if (GetUi32(p) != 0x636D7066) // magic == "fpmc"
    return;
  Method = GetUi32(p + 4);
  UnpackSize = GetUi64(p + 8);
  dataSize -= k_decmpfs_HeaderSize;
  IsCorrect = true;
  
  if (   Method == kMethod_ZLIB_RSRC
      || Method == kMethod_COPY_RSRC
      || Method == kMethod_LZVN_RSRC
      || Method == kMethod_LZFSE_RSRC
      // || Method == kMethod_ZBM_RSRC // for debug
      )
  {
    IsResource = true;
    if (dataSize == 0)
      IsSupported = (
          Method != kMethod_LZFSE_RSRC &&
          Method != kMethod_COPY_RSRC);
    return;
  }
  
  if (   Method == kMethod_ZLIB_ATTR
      || Method == kMethod_COPY_ATTR
      || Method == kMethod_LZVN_ATTR
      // || Method == kMethod_LZFSE_ATTR
    )
  {
    if (dataSize == 0)
      return;
    const Byte b = p[k_decmpfs_HeaderSize];
    if (   (Method == kMethod_ZLIB_ATTR && (b & 0xf) == 0xf)
        || (Method == kMethod_COPY_ATTR && b == k_COPY_Uncompressed_Marker)
        || (Method == kMethod_LZVN_ATTR && b == k_LZVN_Uncompressed_Marker))
    {
      dataSize--;
      // if (UnpackSize > dataSize)
      if (UnpackSize != dataSize)
        return;
      DataPos = k_decmpfs_HeaderSize + 1;
      IsSupported = true;
    }
    else
    {
      if (Method != kMethod_COPY_ATTR)
        IsSupported = true;
      DataPos = k_decmpfs_HeaderSize;
    }
  }
}


void CCompressHeader::MethodToProp(NWindows::NCOM::CPropVariant &prop) const
{
  if (!IsCorrect)
    return;
  const UInt32 method = Method;
  const char *p = NULL;
  if (method < Z7_ARRAY_SIZE(g_Methods))
    p = g_Methods[method];
  AString s;
  if (p)
    s = p;
  else
    s.Add_UInt32(method);
  // if (!IsSupported) s += "-unsuported";
  prop = s;
}

void MethodsMaskToProp(UInt32 methodsMask, NWindows::NCOM::CPropVariant &prop)
{
  FLAGS_TO_PROP(g_Methods, methodsMask, prop);
}


struct CItem
{
  UString Name;
  
  UInt32 ParentID;

  UInt16 Type;
  UInt16 FileMode;
  // UInt16 Flags;
  // UInt32 Valence;
  UInt32 ID;
  UInt32 CTime;
  UInt32 MTime;
  UInt32 AttrMTime;
  UInt32 ATime;
  // UInt32 BackupDate;

  /*
  UInt32 OwnerID;
  UInt32 GroupID;
  Byte AdminFlags;
  Byte OwnerFlags;
  union
  {
    UInt32  iNodeNum;
    UInt32  LinkCount;
    UInt32  RawDevice;
  } special;

  UInt32 FileType;
  UInt32 FileCreator;
  UInt16 FinderFlags;
  UInt16 Point[2];
  */

  CFork DataFork;
  CFork ResourceFork;

  // for compressed attribute (decmpfs)
  int decmpfs_AttrIndex;
  CCompressHeader CompressHeader;

  CItem():
      decmpfs_AttrIndex(-1)
      {}
  bool IsDir() const { return Type == RECORD_TYPE_FOLDER; }
  // const CFork *GetFork(bool isResource) const { return (isResource ? &ResourceFork: &DataFork); }
};


struct CAttr
{
  UInt32 ID;
  bool Fork_defined;
  
  // UInt32 Size;    // for (Fork_defined == false) case
  // size_t DataPos; // for (Fork_defined == false) case
  CByteBuffer Data;

  CFork Fork;

  UString Name;

  UInt64 GetSize() const
  {
    if (Fork_defined)
      return Fork.Size;
    return Data.Size();
  }

  CAttr():
      Fork_defined(false)
      // Size(0),
      // DataPos(0),
      {}
};


static const int kAttrIndex_Item     = -1;
static const int kAttrIndex_Resource = -2;

struct CRef
{
  unsigned ItemIndex;
  int AttrIndex;
  int Parent;

  CRef(): AttrIndex(kAttrIndex_Item), Parent(-1) {}
  bool IsResource() const { return AttrIndex == kAttrIndex_Resource; }
  bool IsAltStream() const { return AttrIndex != kAttrIndex_Item; }
  bool IsItem() const { return AttrIndex == kAttrIndex_Item; }
};


class CDatabase
{
  HRESULT ReadFile(const CFork &fork, CByteBuffer &buf, IInStream *inStream);
  HRESULT LoadExtentFile(const CFork &fork, IInStream *inStream, CObjectVector<CIdExtents> *overflowExtentsArray);
  HRESULT LoadAttrs(const CFork &fork, IInStream *inStream, IArchiveOpenCallback *progress);
  HRESULT LoadCatalog(const CFork &fork, const CObjectVector<CIdExtents> *overflowExtentsArray, IInStream *inStream, IArchiveOpenCallback *progress);
  bool Parse_decmpgfs(unsigned attrIndex, CItem &item, bool &skip);
public:
  CRecordVector<CRef> Refs;
  CObjectVector<CItem> Items;
  CObjectVector<CAttr> Attrs;

  // CByteBuffer AttrBuf;

  CVolHeader Header;
  bool HeadersError;
  bool UnsupportedFeature;
  bool ThereAreAltStreams;
  // bool CaseSensetive;
  UInt32 MethodsMask;
  UString ResFileName;

  UInt64 SpecOffset;
  // UInt64 PhySize;
  UInt64 PhySize2;
  UInt64 ArcFileSize;

  void Clear()
  {
    SpecOffset = 0;
    // PhySize = 0;
    PhySize2 = 0;
    ArcFileSize = 0;
    MethodsMask = 0;
    HeadersError = false;
    UnsupportedFeature = false;
    ThereAreAltStreams = false;
    // CaseSensetive = false;

    Refs.Clear();
    Items.Clear();
    Attrs.Clear();
    // AttrBuf.Free();
  }

  UInt64 Get_UnpackSize_of_Ref(const CRef &ref) const
  {
    if (ref.AttrIndex >= 0)
      return Attrs[ref.AttrIndex].GetSize();
    const CItem &item = Items[ref.ItemIndex];
    if (ref.IsResource())
      return item.ResourceFork.Size;
    if (item.IsDir())
      return 0;
    else if (item.CompressHeader.IsCorrect)
      return item.CompressHeader.UnpackSize;
    return item.DataFork.Size;
  }

  void GetItemPath(unsigned index, NWindows::NCOM::CPropVariant &path) const;
  HRESULT Open2(IInStream *inStream, IArchiveOpenCallback *progress);
};

enum
{
  kHfsID_Root                  = 1,
  kHfsID_RootFolder            = 2,
  kHfsID_ExtentsFile           = 3,
  kHfsID_CatalogFile           = 4,
  kHfsID_BadBlockFile          = 5,
  kHfsID_AllocationFile        = 6,
  kHfsID_StartupFile           = 7,
  kHfsID_AttributesFile        = 8,
  kHfsID_RepairCatalogFile     = 14,
  kHfsID_BogusExtentFile       = 15,
  kHfsID_FirstUserCatalogNode  = 16
};

void CDatabase::GetItemPath(unsigned index, NWindows::NCOM::CPropVariant &path) const
{
  unsigned len = 0;
  const unsigned kNumLevelsMax = (1 << 10);
  unsigned cur = index;
  unsigned i;
  
  for (i = 0; i < kNumLevelsMax; i++)
  {
    const CRef &ref = Refs[cur];
    const UString *s;

    if (ref.IsResource())
      s = &ResFileName;
    else if (ref.AttrIndex >= 0)
      s = &Attrs[ref.AttrIndex].Name;
    else
      s = &Items[ref.ItemIndex].Name;

    len += s->Len();
    len++;
    cur = (unsigned)ref.Parent;
    if (ref.Parent < 0)
      break;
  }
  
  len--;
  wchar_t *p = path.AllocBstr(len);
  p[len] = 0;
  cur = index;
  
  for (;;)
  {
    const CRef &ref = Refs[cur];
    const UString *s;
    wchar_t delimChar = L':';

    if (ref.IsResource())
      s = &ResFileName;
    else if (ref.AttrIndex >= 0)
      s = &Attrs[ref.AttrIndex].Name;
    else
    {
      delimChar = WCHAR_PATH_SEPARATOR;
      s = &Items[ref.ItemIndex].Name;
    }

    unsigned curLen = s->Len();
    len -= curLen;
    
    const wchar_t *src = (const wchar_t *)*s;
    wchar_t *dest = p + len;
    for (unsigned j = 0; j < curLen; j++)
    {
      wchar_t c = src[j];
      // 18.06
      if (c == CHAR_PATH_SEPARATOR || c == '/')
        c = '_';
      dest[j] = c;
    }
    
    if (len == 0)
      break;
    p[--len] = delimChar;
    cur = (unsigned)ref.Parent;
  }
}

// Actually we read all blocks. It can be larger than fork.Size

HRESULT CDatabase::ReadFile(const CFork &fork, CByteBuffer &buf, IInStream *inStream)
{
  if (fork.NumBlocks >= Header.NumBlocks)
    return S_FALSE;
  if (((ArcFileSize - SpecOffset) >> Header.BlockSizeLog) + 1 < fork.NumBlocks)
    return S_FALSE;

  const size_t totalSize = (size_t)fork.NumBlocks << Header.BlockSizeLog;
  if ((totalSize >> Header.BlockSizeLog) != fork.NumBlocks)
    return S_FALSE;
  buf.Alloc(totalSize);
  UInt32 curBlock = 0;
  FOR_VECTOR (i, fork.Extents)
  {
    if (curBlock >= fork.NumBlocks)
      return S_FALSE;
    const CExtent &e = fork.Extents[i];
    if (e.Pos > Header.NumBlocks ||
        e.NumBlocks > fork.NumBlocks - curBlock ||
        e.NumBlocks > Header.NumBlocks - e.Pos)
      return S_FALSE;
    RINOK(InStream_SeekSet(inStream, SpecOffset + ((UInt64)e.Pos << Header.BlockSizeLog)))
    RINOK(ReadStream_FALSE(inStream,
        (Byte *)buf + ((size_t)curBlock << Header.BlockSizeLog),
        (size_t)e.NumBlocks << Header.BlockSizeLog))
    curBlock += e.NumBlocks;
  }
  return S_OK;
}

static const unsigned kNodeDescriptor_Size = 14;

struct CNodeDescriptor
{
  UInt32 fLink;
  // UInt32 bLink;
  Byte Kind;
  // Byte Height;
  unsigned NumRecords;

  bool Parse(const Byte *p, unsigned nodeSizeLog);
};


bool CNodeDescriptor::Parse(const Byte *p, unsigned nodeSizeLog)
{
  fLink = Get32(p);
  // bLink = Get32(p + 4);
  Kind = p[8];
  // Height = p[9];
  NumRecords = Get16(p + 10);

  const size_t nodeSize = (size_t)1 << nodeSizeLog;
  if (kNodeDescriptor_Size + ((UInt32)NumRecords + 1) * 2 > nodeSize)
    return false;
  const size_t limit = nodeSize - ((UInt32)NumRecords + 1) * 2;

  p += nodeSize - 2;

  for (unsigned i = 0; i < NumRecords; i++)
  {
    const UInt32 offs = Get16(p);
    p -= 2;
    const UInt32 offsNext = Get16(p);
    if (offs < kNodeDescriptor_Size
        || offs >= offsNext
        || offsNext > limit)
      return false;
  }
  return true;
}

struct CHeaderRec
{
  // UInt16 TreeDepth;
  // UInt32 RootNode;
  // UInt32 LeafRecords;
  UInt32 FirstLeafNode;
  // UInt32 LastLeafNode;
  unsigned NodeSizeLog;
  // UInt16 MaxKeyLength;
  UInt32 TotalNodes;
  // UInt32 FreeNodes;
  // UInt16 Reserved1;
  // UInt32 ClumpSize;
  // Byte BtreeType;
  // Byte KeyCompareType;
  // UInt32 Attributes;
  // UInt32 Reserved3[16];
  
  HRESULT Parse2(const CByteBuffer &buf);
};

HRESULT CHeaderRec::Parse2(const CByteBuffer &buf)
{
  if (buf.Size() < kNodeDescriptor_Size + 0x2A + 16 * 4)
    return S_FALSE;
  const Byte * p = (const Byte *)buf + kNodeDescriptor_Size;
  // TreeDepth = Get16(p);
  // RootNode = Get32(p + 2);
  // LeafRecords = Get32(p + 6);
  FirstLeafNode = Get32(p + 0xA);
  // LastLeafNode = Get32(p + 0xE);
  const UInt32 nodeSize = Get16(p + 0x12);

  unsigned i;
  for (i = 9; ((UInt32)1 << i) != nodeSize; i++)
    if (i == 16)
      return S_FALSE;
  NodeSizeLog = i;

  // MaxKeyLength = Get16(p + 0x14);
  TotalNodes = Get32(p + 0x16);
  // FreeNodes = Get32(p + 0x1A);
  // Reserved1 = Get16(p + 0x1E);
  // ClumpSize = Get32(p + 0x20);
  // BtreeType = p[0x24];
  // KeyCompareType = p[0x25];
  // Attributes = Get32(p + 0x26);
  /*
  for (int i = 0; i < 16; i++)
    Reserved3[i] = Get32(p + 0x2A + i * 4);
  */

  if ((buf.Size() >> NodeSizeLog) < TotalNodes)
    return S_FALSE;

  return S_OK;
}


static const Byte kNodeType_Leaf   = 0xFF;
// static const Byte kNodeType_Index  = 0;
// static const Byte kNodeType_Header = 1;
// static const Byte kNodeType_Mode   = 2;

static const Byte kExtentForkType_Data = 0;
static const Byte kExtentForkType_Resource = 0xFF;

/* It loads data extents from Extents Overflow File
   Most dmg installers are not fragmented. So there are no extents in Overflow File. */

HRESULT CDatabase::LoadExtentFile(const CFork &fork, IInStream *inStream, CObjectVector<CIdExtents> *overflowExtentsArray)
{
  if (fork.NumBlocks == 0)
    return S_OK;
  CByteBuffer buf;
  RINOK(ReadFile(fork, buf, inStream))
  const Byte *p = (const Byte *)buf;

  // CNodeDescriptor nodeDesc;
  // nodeDesc.Parse(p);
  CHeaderRec hr;
  RINOK(hr.Parse2(buf))

  UInt32 node = hr.FirstLeafNode;
  if (node == 0)
    return S_OK;
  if (hr.TotalNodes == 0)
    return S_FALSE;

  CByteArr usedBuf(hr.TotalNodes);
  memset(usedBuf, 0, hr.TotalNodes);

  while (node != 0)
  {
    if (node >= hr.TotalNodes || usedBuf[node] != 0)
      return S_FALSE;
    usedBuf[node] = 1;

    const size_t nodeOffset = (size_t)node << hr.NodeSizeLog;
    CNodeDescriptor desc;
    if (!desc.Parse(p + nodeOffset, hr.NodeSizeLog))
      return S_FALSE;
    if (desc.Kind != kNodeType_Leaf)
      return S_FALSE;
    
    UInt32 endBlock = 0;

    for (unsigned i = 0; i < desc.NumRecords; i++)
    {
      const UInt32 nodeSize = ((UInt32)1 << hr.NodeSizeLog);
      const Byte *r = p + nodeOffset + nodeSize - i * 2;
      const UInt32 offs = Get16(r - 2);
      UInt32 recSize = Get16(r - 4) - offs;
      const unsigned kKeyLen = 10;

      if (recSize != 2 + kKeyLen + kNumFixedExtents * 8)
        return S_FALSE;

      r = p + nodeOffset + offs;
      if (Get16(r) != kKeyLen)
        return S_FALSE;
      
      const Byte forkType = r[2];
      unsigned forkTypeIndex;
      if (forkType == kExtentForkType_Data)
        forkTypeIndex = 0;
      else if (forkType == kExtentForkType_Resource)
        forkTypeIndex = 1;
      else
        continue;
      CObjectVector<CIdExtents> &overflowExtents = overflowExtentsArray[forkTypeIndex];

      const UInt32 id = Get32(r + 4);
      const UInt32 startBlock = Get32(r + 8);
      r += 2 + kKeyLen;
      
      bool needNew = true;
      
      if (overflowExtents.Size() != 0)
      {
        CIdExtents &e = overflowExtents.Back();
        if (e.ID == id)
        {
          if (endBlock != startBlock)
            return S_FALSE;
          needNew = false;
        }
      }
      
      if (needNew)
      {
        CIdExtents &e = overflowExtents.AddNew();
        e.ID = id;
        e.StartBlock = startBlock;
        endBlock = startBlock;
      }
      
      CIdExtents &e = overflowExtents.Back();
      
      for (unsigned k = 0; k < kNumFixedExtents; k++, r += 8)
      {
        CExtent ee;
        ee.Pos = Get32(r);
        ee.NumBlocks = Get32(r + 4);
        if (ee.NumBlocks != 0)
        {
          e.Extents.Add(ee);
          endBlock += ee.NumBlocks;
        }
      }
    }

    node = desc.fLink;
  }
  return S_OK;
}

static void LoadName(const Byte *data, unsigned len, UString &dest)
{
  wchar_t *p = dest.GetBuf(len);
  unsigned i;
  for (i = 0; i < len; i++)
  {
    const wchar_t c = Get16(data + i * 2);
    if (c == 0)
      break;
    p[i] = c;
  }
  p[i] = 0;
  dest.ReleaseBuf_SetLen(i);
}

static bool IsNameEqualTo(const Byte *data, const char *name)
{
  for (unsigned i = 0;; i++)
  {
    const char c = name[i];
    if (c == 0)
      return true;
    if (Get16(data + i * 2) != (Byte)c)
      return false;
  }
}

static const UInt32 kAttrRecordType_Inline = 0x10;
static const UInt32 kAttrRecordType_Fork = 0x20;
// static const UInt32 kAttrRecordType_Extents = 0x30;

HRESULT CDatabase::LoadAttrs(const CFork &fork, IInStream *inStream, IArchiveOpenCallback *progress)
{
  if (fork.NumBlocks == 0)
    return S_OK;

  CByteBuffer AttrBuf;
  RINOK(ReadFile(fork, AttrBuf, inStream))
  const Byte *p = (const Byte *)AttrBuf;

  // CNodeDescriptor nodeDesc;
  // nodeDesc.Parse(p);
  CHeaderRec hr;
  RINOK(hr.Parse2(AttrBuf))
  
  // CaseSensetive = (Header.IsHfsX() && hr.KeyCompareType == 0xBC);

  UInt32 node = hr.FirstLeafNode;
  if (node == 0)
    return S_OK;
  if (hr.TotalNodes == 0)
    return S_FALSE;

  CByteArr usedBuf(hr.TotalNodes);
  memset(usedBuf, 0, hr.TotalNodes);

  CFork resFork;

  while (node != 0)
  {
    if (node >= hr.TotalNodes || usedBuf[node] != 0)
      return S_FALSE;
    usedBuf[node] = 1;
    
    const size_t nodeOffset = (size_t)node << hr.NodeSizeLog;
    CNodeDescriptor desc;
    if (!desc.Parse(p + nodeOffset, hr.NodeSizeLog))
      return S_FALSE;
    if (desc.Kind != kNodeType_Leaf)
      return S_FALSE;
    
    for (unsigned i = 0; i < desc.NumRecords; i++)
    {
      const UInt32 nodeSize = ((UInt32)1 << hr.NodeSizeLog);
      const Byte *r = p + nodeOffset + nodeSize - i * 2;
      const UInt32 offs = Get16(r - 2);
      UInt32 recSize = Get16(r - 4) - offs;
      const unsigned kHeadSize = 14;
      if (recSize < kHeadSize)
        return S_FALSE;

      r = p + nodeOffset + offs;
      const UInt32 keyLen = Get16(r);

      // UInt16 pad = Get16(r + 2);
      const UInt32 fileID = Get32(r + 4);
      const unsigned startBlock = Get32(r + 8);
      if (startBlock != 0)
      {
        // that case is still unsupported
        UnsupportedFeature = true;
        continue;
      }
      const unsigned nameLen = Get16(r + 12);

      if (keyLen + 2 > recSize ||
          keyLen != kHeadSize - 2 + nameLen * 2)
        return S_FALSE;
      r += kHeadSize;
      recSize -= kHeadSize;

      const Byte *name = r;
      r += nameLen * 2;
      recSize -= nameLen * 2;

      if (recSize < 4)
        return S_FALSE;

      const UInt32 recordType = Get32(r);

      if (progress && (Attrs.Size() & 0xFFF) == 0)
      {
        const UInt64 numFiles = 0;
        RINOK(progress->SetCompleted(&numFiles, NULL))
      }

      if (Attrs.Size() >= ((UInt32)1 << 31))
        return S_FALSE;

      CAttr &attr = Attrs.AddNew();
      attr.ID = fileID;
      LoadName(name, nameLen, attr.Name);

      if (recordType == kAttrRecordType_Fork)
      {
        // 22.00 : some hfs files contain it;
        /* spec: If the attribute has more than 8 extents, there will be additional
            records (of type kAttrRecordType_Extents) for this attribute. */
        if (recSize != 8 + kForkRecSize)
          return S_FALSE;
        if (Get32(r + 4) != 0) // reserved
          return S_FALSE;
        attr.Fork.Parse(r + 8);
        attr.Fork_defined = true;
        continue;
      }
      else if (recordType != kAttrRecordType_Inline)
      {
        UnsupportedFeature = true;
        continue;
      }

      const unsigned kRecordHeaderSize = 16;
      if (recSize < kRecordHeaderSize)
        return S_FALSE;
      if (Get32(r + 4) != 0 || Get32(r + 8) != 0) // reserved
        return S_FALSE;
      const UInt32 dataSize = Get32(r + 12);
      
      r += kRecordHeaderSize;
      recSize -= kRecordHeaderSize;
      
      if (recSize < dataSize)
        return S_FALSE;

      attr.Data.CopyFrom(r, dataSize);
      // attr.DataPos = nodeOffset + offs + 2 + keyLen + kRecordHeaderSize;
      // attr.Size = dataSize;
    }

    node = desc.fLink;
  }
  return S_OK;
}


bool CDatabase::Parse_decmpgfs(unsigned attrIndex, CItem &item, bool &skip)
{
  const CAttr &attr = Attrs[attrIndex];
  skip = false;
  if (item.CompressHeader.IsCorrect || !item.DataFork.IsEmpty())
    return false;

  item.CompressHeader.Parse(attr.Data, attr.Data.Size());

  if (item.CompressHeader.IsCorrect)
  {
    item.decmpfs_AttrIndex = (int)attrIndex;
    skip = true;
    if (item.CompressHeader.Method < sizeof(MethodsMask) * 8)
      MethodsMask |= ((UInt32)1 << item.CompressHeader.Method);
  }

  return true;
}


HRESULT CDatabase::LoadCatalog(const CFork &fork, const CObjectVector<CIdExtents> *overflowExtentsArray, IInStream *inStream, IArchiveOpenCallback *progress)
{
  CByteBuffer buf;
  RINOK(ReadFile(fork, buf, inStream))
  const Byte *p = (const Byte *)buf;

  // CNodeDescriptor nodeDesc;
  // nodeDesc.Parse(p);
  CHeaderRec hr;
  RINOK(hr.Parse2(buf))

  CRecordVector<CIdIndexPair> IdToIndexMap;

  const unsigned reserveSize = (unsigned)(Header.NumFolders + 1 + Header.NumFiles);

  const unsigned kBasicRecSize = 0x58;
  const unsigned kMinRecSize = kBasicRecSize + 10;

  if ((UInt64)reserveSize * kMinRecSize < buf.Size())
  {
    Items.ClearAndReserve(reserveSize);
    Refs.ClearAndReserve(reserveSize);
    IdToIndexMap.ClearAndReserve(reserveSize);
  }
 
  // CaseSensetive = (Header.IsHfsX() && hr.KeyCompareType == 0xBC);

  CByteArr usedBuf(hr.TotalNodes);
  if (hr.TotalNodes != 0)
    memset(usedBuf, 0, hr.TotalNodes);

  CFork resFork;

  UInt32 node = hr.FirstLeafNode;
  UInt32 numFiles = 0;
  UInt32 numFolders = 0;
  
  while (node != 0)
  {
    if (node >= hr.TotalNodes || usedBuf[node] != 0)
      return S_FALSE;
    usedBuf[node] = 1;
    
    const size_t nodeOffset = (size_t)node << hr.NodeSizeLog;
    CNodeDescriptor desc;
    if (!desc.Parse(p + nodeOffset, hr.NodeSizeLog))
      return S_FALSE;
    if (desc.Kind != kNodeType_Leaf)
      return S_FALSE;
    
    for (unsigned i = 0; i < desc.NumRecords; i++)
    {
      const UInt32 nodeSize = (1 << hr.NodeSizeLog);
      const Byte *r = p + nodeOffset + nodeSize - i * 2;
      const UInt32 offs = Get16(r - 2);
      UInt32 recSize = Get16(r - 4) - offs;
      if (recSize < 6)
        return S_FALSE;

      r = p + nodeOffset + offs;
      UInt32 keyLen = Get16(r);
      UInt32 parentID = Get32(r + 2);
      if (keyLen < 6 || (keyLen & 1) != 0 || keyLen + 2 > recSize)
        return S_FALSE;
      r += 6;
      recSize -= 6;
      keyLen -= 6;
      
      unsigned nameLen = Get16(r);
      if (nameLen * 2 != (unsigned)keyLen)
        return S_FALSE;
      r += 2;
      recSize -= 2;
     
      r += nameLen * 2;
      recSize -= nameLen * 2;

      if (recSize < 2)
        return S_FALSE;
      UInt16 type = Get16(r);

      if (type != RECORD_TYPE_FOLDER &&
          type != RECORD_TYPE_FILE)
        continue;

      if (recSize < kBasicRecSize)
        return S_FALSE;

      CItem &item = Items.AddNew();
      item.ParentID = parentID;
      item.Type = type;
      // item.Flags = Get16(r + 2);
      // item.Valence = Get32(r + 4);
      item.ID = Get32(r + 8);
      {
        const Byte *name = r - (nameLen * 2);
        LoadName(name, nameLen, item.Name);
        if (item.Name.Len() <= 1)
        {
          if (item.Name.IsEmpty() && nameLen == 21)
          {
            if (GetUi32(name) == 0 &&
                GetUi32(name + 4) == 0 &&
                IsNameEqualTo(name + 8, "HFS+ Private Data"))
            {
              // it's folder for "Hard Links" files
              item.Name = "[HFS+ Private Data]";
            }
          }

          // Some dmg files have ' ' folder item.
          if (item.Name.IsEmpty() || item.Name[0] == L' ')
            item.Name = "[]";
        }
      }

      item.CTime = Get32(r + 0xC);
      item.MTime = Get32(r + 0x10);
      item.AttrMTime = Get32(r + 0x14);
      item.ATime = Get32(r + 0x18);
      // item.BackupDate = Get32(r + 0x1C);
      
      /*
      item.OwnerID = Get32(r + 0x20);
      item.GroupID = Get32(r + 0x24);
      item.AdminFlags = r[0x28];
      item.OwnerFlags = r[0x29];
      */
      item.FileMode = Get16(r + 0x2A);
      /*
      item.special.iNodeNum = Get16(r + 0x2C); // or .linkCount
      item.FileType = Get32(r + 0x30);
      item.FileCreator = Get32(r + 0x34);
      item.FinderFlags = Get16(r + 0x38);
      item.Point[0] = Get16(r + 0x3A); // v
      item.Point[1] = Get16(r + 0x3C); // h
      */

      // const refIndex = Refs.Size();
      CIdIndexPair pair;
      pair.ID = item.ID;
      pair.Index = Items.Size() - 1;
      IdToIndexMap.Add(pair);

      recSize -= kBasicRecSize;
      r += kBasicRecSize;
      if (item.IsDir())
      {
        numFolders++;
        if (recSize != 0)
          return S_FALSE;
      }
      else
      {
        numFiles++;
        if (recSize != kForkRecSize * 2)
          return S_FALSE;
        
        item.DataFork.Parse(r);

        if (!item.DataFork.UpgradeAndTest(overflowExtentsArray[0], item.ID, Header.BlockSizeLog))
          HeadersError = true;
  
        item.ResourceFork.Parse(r + kForkRecSize);
        if (!item.ResourceFork.IsEmpty())
        {
          if (!item.ResourceFork.UpgradeAndTest(overflowExtentsArray[1], item.ID, Header.BlockSizeLog))
            HeadersError = true;
          // ThereAreAltStreams = true;
        }
      }
      if (progress && (Items.Size() & 0xFFF) == 0)
      {
        const UInt64 numItems = Items.Size();
        RINOK(progress->SetCompleted(&numItems, NULL))
      }
    }
    node = desc.fLink;
  }

  if (Header.NumFiles != numFiles ||
      Header.NumFolders + 1 != numFolders)
    HeadersError = true;

  IdToIndexMap.Sort2();
  {
    for (unsigned i = 1; i < IdToIndexMap.Size(); i++)
      if (IdToIndexMap[i - 1].ID == IdToIndexMap[i].ID)
        return S_FALSE;
  }

 
  CBoolArr skipAttr(Attrs.Size());
  {
    for (unsigned i = 0; i < Attrs.Size(); i++)
      skipAttr[i] = false;
  }

  {
    FOR_VECTOR (i, Attrs)
    {
      const CAttr &attr = Attrs[i];

      const int itemIndex = FindItemIndex(IdToIndexMap, attr.ID);
      if (itemIndex < 0)
      {
        HeadersError = true;
        continue;
      }
  
      if (attr.Name.IsEqualTo("com.apple.decmpfs"))
      {
        if (!Parse_decmpgfs(i, Items[itemIndex], skipAttr[i]))
          HeadersError = true;
      }
    }
  }

  IdToIndexMap.ClearAndReserve(Items.Size());

  {
    FOR_VECTOR (i, Items)
    {
      const CItem &item = Items[i];

      CIdIndexPair pair;
      pair.ID = item.ID;
      pair.Index = Refs.Size();
      IdToIndexMap.Add(pair);

      CRef ref;
      ref.ItemIndex = i;
      Refs.Add(ref);
      
      #ifdef HFS_SHOW_ALT_STREAMS
     
      if (item.ResourceFork.IsEmpty())
        continue;
      if (item.CompressHeader.IsSupported && item.CompressHeader.IsMethod_Resource())
        continue;

      ThereAreAltStreams = true;
      ref.AttrIndex = kAttrIndex_Resource;
      ref.Parent = (int)(Refs.Size() - 1);
      Refs.Add(ref);
      
      #endif
    }
  }

  IdToIndexMap.Sort2();

  {
    FOR_VECTOR (i, Refs)
    {
      CRef &ref = Refs[i];
      if (ref.IsResource())
        continue;
      const CItem &item = Items[ref.ItemIndex];
      ref.Parent = FindItemIndex(IdToIndexMap, item.ParentID);
      if (ref.Parent >= 0)
      {
        if (!Items[Refs[ref.Parent].ItemIndex].IsDir())
        {
          ref.Parent = -1;
          HeadersError = true;
        }
      }
    }
  }

  #ifdef HFS_SHOW_ALT_STREAMS
  {
    FOR_VECTOR (i, Attrs)
    {
      if (skipAttr[i])
        continue;
      const CAttr &attr = Attrs[i];

      const int refIndex = FindItemIndex(IdToIndexMap, attr.ID);
      if (refIndex < 0)
      {
        HeadersError = true;
        continue;
      }

      ThereAreAltStreams = true;

      CRef ref;
      ref.AttrIndex = (int)i;
      ref.Parent = refIndex;
      ref.ItemIndex = Refs[refIndex].ItemIndex;
      Refs.Add(ref);
    }
  }
  #endif

  return S_OK;
}

static const unsigned kHeaderPadSize = 1 << 10;
static const unsigned kMainHeaderSize = 512;
static const unsigned kHfsHeaderSize = kHeaderPadSize + kMainHeaderSize;

static const unsigned k_Signature_LE16_HFS_BD = 'B' + ((unsigned)'D' << 8);
static const unsigned k_Signature_LE16_HPLUS  = 'H' + ((unsigned)'+' << 8);
static const UInt32   k_Signature_LE32_HFSP_VER4 = 'H' + ((UInt32)'+' << 8) + ((UInt32)4 << 24);
static const UInt32   k_Signature_LE32_HFSX_VER5 = 'H' + ((UInt32)'X' << 8) + ((UInt32)5 << 24);

API_FUNC_static_IsArc IsArc_HFS(const Byte *p, size_t size)
{
  if (size < kHfsHeaderSize)
    return k_IsArc_Res_NEED_MORE;
  p += kHeaderPadSize;
  const UInt32 sig = GetUi32(p);
  if (sig != k_Signature_LE32_HFSP_VER4)
  if (sig != k_Signature_LE32_HFSX_VER5)
  if ((UInt16)sig != k_Signature_LE16_HFS_BD
      || GetUi16(p + 0x7c) != k_Signature_LE16_HPLUS)
    return k_IsArc_Res_NO;
  return k_IsArc_Res_YES;
}
}

HRESULT CDatabase::Open2(IInStream *inStream, IArchiveOpenCallback *progress)
{
  Clear();
  UInt32 buf32[kHfsHeaderSize / 4];
  RINOK(ReadStream_FALSE(inStream, buf32, kHfsHeaderSize))
  const Byte *p = (const Byte *)buf32 + kHeaderPadSize;
  CVolHeader &h = Header;

  if (GetUi16a(p) == k_Signature_LE16_HFS_BD)
  {
    /*
    It's header for old HFS format.
    We don't support old HFS format, but we support
    special HFS volume that contains embedded HFS+ volume.
    HFS MDB : Master directory block
    HFS VIB : Volume information block
    some old images contain boot data with "LK" signature at start of buf32.
    */
#if 1
    // here we check first bytes of archive,
    // because start data can contain signature of some another
    // archive type that could have priority over HFS.
    const void *buf_ptr = (const void *)buf32;
    const unsigned sig = GetUi16a(buf_ptr);
    if (sig != 'L' + ((unsigned)'K' << 8))
    {
      // some old HFS (non HFS+) files have no "LK" signature,
      // but have non-zero data after 2 first bytes in start 1 KiB.
      if (sig != 0)
        return S_FALSE;
/*
      for (unsigned i = 0; i < kHeaderPadSize / 4; i++)
        if (buf32[i] != 0)
          return S_FALSE;
*/
    }
#endif
    if (GetUi16a(p + 0x7c) != k_Signature_LE16_HPLUS) // signature of embedded HFS+ volume
      return S_FALSE;
    /*
    h.CTime = Get32(p + 0x2);
    h.MTime = Get32(p + 0x6);
    
    h.NumFiles = Get32(p + 0x54);
    h.NumFolders = Get32(p + 0x58);
    
    if (h.NumFolders > ((UInt32)1 << 29) ||
        h.NumFiles > ((UInt32)1 << 30))
      return S_FALSE;
    if (progress)
    {
      UInt64 numFiles = (UInt64)h.NumFiles + h.NumFolders + 1;
      RINOK(progress->SetTotal(&numFiles, NULL))
    }
    h.NumFreeBlocks = Get16(p + 0x22);
    */
    
    // v24.09: blockSize in old HFS image can be non-power of 2.
    const UInt32 blockSize = Get32a(p + 0x14); // drAlBlkSiz
    if (blockSize == 0 || (blockSize & 0x1ff))
      return S_FALSE;
    const unsigned numBlocks = Get16a(p + 0x12); // drNmAlBlks
    // UInt16 drFreeBks = Get16a(p + 0x22); // number of unused allocation blocks
    /*
    we suppose that it has the following layout:
    {
      start data with header
      blocks[h.NumBlocks]
      end data with header (probably size_of_footer <= blockSize).
    }
    */
    // PhySize2 = ((UInt64)numBlocks + 2) * blockSize;
    const unsigned sector_of_FirstBlock = Get16a(p + 0x1c); // drAlBlSt : first allocation block in volume
    const UInt32 startBlock = Get16a(p + 0x7c + 2);
    const UInt32 blockCount = Get16a(p + 0x7c + 4);
    SpecOffset = (UInt32)sector_of_FirstBlock << 9; // it's 32-bit here
    PhySize2 = SpecOffset + (UInt64)numBlocks * blockSize;
    SpecOffset += (UInt64)startBlock * blockSize;
    // before v24.09: // SpecOffset = (UInt64)(1 + startBlock) * blockSize;
    const UInt64 phy = SpecOffset + (UInt64)blockCount * blockSize;
    if (PhySize2 < phy)
        PhySize2 = phy;
    UInt32 tail = 1 << 10; // at least 1 KiB tail (for footer MDB) is expected.
    if (tail < blockSize)
        tail = blockSize;
    RINOK(InStream_GetSize_SeekToEnd(inStream, ArcFileSize))
    if (ArcFileSize > PhySize2 &&
        ArcFileSize - PhySize2 <= tail)
    {
      // data after blocks[h.NumBlocks] must contain another copy of MDB.
      // In example where blockSize is not power of 2, we have
      //   (ArcFileSize - PhySize2) < blockSize.
      // We suppose that data after blocks[h.NumBlocks] is part of HFS archive.
      // Maybe we should scan for footer MDB data (in last 1 KiB)?
      PhySize2 = ArcFileSize;
    }
    RINOK(InStream_SeekSet(inStream, SpecOffset))
    RINOK(ReadStream_FALSE(inStream, buf32, kHfsHeaderSize))
  }

  // HFS+ / HFSX volume header (starting from offset==1024):
  {
    // v24.09: we use strict condition test for pair signature(Version):
    // H+(4), HX(5):
    const UInt32 sig = GetUi32a(p);
    // h.Version = Get16(p + 2);
    h.Is_Hsfx_ver5 = false;
    if (sig != k_Signature_LE32_HFSP_VER4)
    {
      if (sig != k_Signature_LE32_HFSX_VER5)
        return S_FALSE;
      h.Is_Hsfx_ver5 = true;
    }
  }
  {
    const UInt32 blockSize = Get32a(p + 0x28);
    unsigned i;
    for (i = 9; ((UInt32)1 << i) != blockSize; i++)
      if (i == 31)
        return S_FALSE;
    h.BlockSizeLog = i;
  }
#if 1
  // HFS Plus DOCs: The first 1024 bytes are reserved for use as boot blocks
  // v24.09: we don't check starting 1 KiB before old (HFS MDB) block ("BD" signture) .
  //     but we still check starting 1 KiB before HFS+ / HFSX volume header.
  // are there HFS+ / HFSX images with non-zero data in this reserved area?
  {
    for (unsigned i = 0; i < kHeaderPadSize / 4; i++)
      if (buf32[i] != 0)
        return S_FALSE;
  }
#endif
  // h.Attr = Get32a(p + 4);
  // h.LastMountedVersion = Get32a(p + 8);
  // h.JournalInfoBlock = Get32a(p + 0xC);
  h.CTime = Get32a(p + 0x10);
  h.MTime = Get32a(p + 0x14);
  // h.BackupTime = Get32a(p + 0x18);
  // h.CheckedTime = Get32a(p + 0x1C);
  h.NumFiles = Get32a(p + 0x20);
  h.NumFolders = Get32a(p + 0x24);
  if (h.NumFolders > ((UInt32)1 << 29) ||
      h.NumFiles > ((UInt32)1 << 30))
    return S_FALSE;

  RINOK(InStream_GetSize_SeekToEnd(inStream, ArcFileSize))
  if (progress)
  {
    const UInt64 numFiles = (UInt64)h.NumFiles + h.NumFolders + 1;
    RINOK(progress->SetTotal(&numFiles, NULL))
  }

  h.NumBlocks = Get32a(p + 0x2C);
  h.NumFreeBlocks = Get32a(p + 0x30);
  /*
  h.NextCalatlogNodeID = Get32(p + 0x40);
  h.WriteCount = Get32(p + 0x44);
  for (i = 0; i < 6; i++)
    h.FinderInfo[i] = Get32(p + 0x50 + i * 4);
  h.VolID = Get64(p + 0x68);
  */

  ResFileName = kResFileName;

  CFork extentsFork, catalogFork, attrFork;
  // allocationFork.Parse(p + 0x70 + 0x50 * 0);
  extentsFork.Parse(p + 0x70 + 0x50 * 1);
  catalogFork.Parse(p + 0x70 + 0x50 * 2);
  attrFork.Parse   (p + 0x70 + 0x50 * 3);
  // startupFork.Parse(p + 0x70 + 0x50 * 4);

  CObjectVector<CIdExtents> overflowExtents[2];
  if (!extentsFork.IsOk(Header.BlockSizeLog))
    HeadersError = true;
  else
  {
    const HRESULT res = LoadExtentFile(extentsFork, inStream, overflowExtents);
    if (res == S_FALSE)
      HeadersError = true;
    else if (res != S_OK)
      return res;
  }

  if (!catalogFork.UpgradeAndTest(overflowExtents[0], kHfsID_CatalogFile, Header.BlockSizeLog))
    return S_FALSE;
  
  if (!attrFork.UpgradeAndTest(overflowExtents[0], kHfsID_AttributesFile, Header.BlockSizeLog))
    HeadersError = true;
  else
  {
    if (attrFork.Size != 0)
      RINOK(LoadAttrs(attrFork, inStream, progress))
  }
  
  RINOK(LoadCatalog(catalogFork, overflowExtents, inStream, progress))

  // PhySize = Header.GetPhySize();
  return S_OK;
}



Z7_class_CHandler_final:
  public IInArchive,
  public IArchiveGetRawProps,
  public IInArchiveGetStream,
  public CMyUnknownImp,
  public CDatabase
{
  Z7_IFACES_IMP_UNK_3(
      IInArchive,
      IArchiveGetRawProps,
      IInArchiveGetStream)

  CMyComPtr<IInStream> _stream;
  HRESULT GetForkStream(const CFork &fork, ISequentialInStream **stream);
};

static const Byte kProps[] =
{
  kpidPath,
  kpidIsDir,
  kpidSize,
  kpidPackSize,
  kpidCTime,
  kpidMTime,
  kpidATime,
  kpidChangeTime,
  kpidPosixAttrib,
  /*
  kpidUserId,
  kpidGroupId,
  */
#ifdef HFS_SHOW_ALT_STREAMS
  kpidIsAltStream,
#endif
  kpidMethod
};

static const Byte kArcProps[] =
{
  kpidMethod,
  kpidCharacts,
  kpidClusterSize,
  kpidFreeSpace,
  kpidCTime,
  kpidMTime
};

IMP_IInArchive_Props
IMP_IInArchive_ArcProps

static void HfsTimeToProp(UInt32 hfsTime, NWindows::NCOM::CPropVariant &prop)
{
  if (hfsTime == 0)
    return;
  FILETIME ft;
  HfsTimeToFileTime(hfsTime, ft);
  prop.SetAsTimeFrom_FT_Prec(ft, k_PropVar_TimePrec_Base);
}

Z7_COM7F_IMF(CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value))
{
  COM_TRY_BEGIN
  NWindows::NCOM::CPropVariant prop;
  switch (propID)
  {
    case kpidExtension: prop = Header.IsHfsX() ? "hfsx" : "hfs"; break;
    case kpidMethod: prop = Header.IsHfsX() ? "HFSX" : "HFS+"; break;
    case kpidCharacts: MethodsMaskToProp(MethodsMask, prop); break;
    case kpidPhySize:
    {
      UInt64 v = SpecOffset + Header.GetPhySize(); // PhySize;
      if (v < PhySize2)
        v = PhySize2;
      prop = v;
      break;
    }
    case kpidClusterSize: prop = (UInt32)1 << Header.BlockSizeLog; break;
    case kpidFreeSpace: prop = (UInt64)Header.GetFreeSize(); break;
    case kpidMTime: HfsTimeToProp(Header.MTime, prop); break;
    case kpidCTime:
    {
      if (Header.CTime != 0)
      {
        FILETIME localFt, ft;
        HfsTimeToFileTime(Header.CTime, localFt);
        if (LocalFileTimeToFileTime(&localFt, &ft))
          prop.SetAsTimeFrom_FT_Prec(ft, k_PropVar_TimePrec_Base);
      }
      break;
    }
    case kpidIsTree: prop = true; break;
    case kpidErrorFlags:
    {
      UInt32 flags = 0;
      if (HeadersError) flags |= kpv_ErrorFlags_HeadersError;
      if (UnsupportedFeature) flags |= kpv_ErrorFlags_UnsupportedFeature;
      if (flags != 0)
        prop = flags;
      break;
    }
    case kpidIsAltStream: prop = ThereAreAltStreams; break;
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

Z7_COM7F_IMF(CHandler::GetNumRawProps(UInt32 *numProps))
{
  *numProps = 0;
  return S_OK;
}

Z7_COM7F_IMF(CHandler::GetRawPropInfo(UInt32 /* index */, BSTR *name, PROPID *propID))
{
  *name = NULL;
  *propID = 0;
  return S_OK;
}

Z7_COM7F_IMF(CHandler::GetParent(UInt32 index, UInt32 *parent, UInt32 *parentType))
{
  const CRef &ref = Refs[index];
  *parentType = ref.IsAltStream() ?
      NParentType::kAltStream :
      NParentType::kDir;
  *parent = (UInt32)(Int32)ref.Parent;
  return S_OK;
}

Z7_COM7F_IMF(CHandler::GetRawProp(UInt32 index, PROPID propID, const void **data, UInt32 *dataSize, UInt32 *propType))
{
  *data = NULL;
  *dataSize = 0;
  *propType = 0;
  #ifdef MY_CPU_LE
  if (propID == kpidName)
  {
    const CRef &ref = Refs[index];
    const UString *s;
    if (ref.IsResource())
      s = &ResFileName;
    else if (ref.AttrIndex >= 0)
      s = &Attrs[ref.AttrIndex].Name;
    else
      s = &Items[ref.ItemIndex].Name;
    *data = (const wchar_t *)(*s);
    *dataSize = (s->Len() + 1) * (UInt32)sizeof(wchar_t);
    *propType = PROP_DATA_TYPE_wchar_t_PTR_Z_LE;
    return S_OK;
  }
  #else
  UNUSED_VAR(index)
  UNUSED_VAR(propID)
  #endif
  return S_OK;
}

Z7_COM7F_IMF(CHandler::GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value))
{
  COM_TRY_BEGIN
  NWindows::NCOM::CPropVariant prop;
  const CRef &ref = Refs[index];
  const CItem &item = Items[ref.ItemIndex];
  switch (propID)
  {
    case kpidPath: GetItemPath(index, prop); break;
    case kpidName:
    {
      const UString *s;
      if (ref.IsResource())
        s = &ResFileName;
      else if (ref.AttrIndex >= 0)
        s = &Attrs[ref.AttrIndex].Name;
      else
        s = &item.Name;
      prop = *s;
      break;
    }
    case kpidPackSize:
      {
        UInt64 size;
        if (ref.AttrIndex >= 0)
          size = Attrs[ref.AttrIndex].GetSize();
        else if (ref.IsResource())
          size = (UInt64)item.ResourceFork.NumBlocks << Header.BlockSizeLog;
        else if (item.IsDir())
          break;
        else if (item.CompressHeader.IsCorrect)
        {
          if (item.CompressHeader.IsMethod_Resource())
            size = (UInt64)item.ResourceFork.NumBlocks << Header.BlockSizeLog;
          else if (item.decmpfs_AttrIndex >= 0)
          {
            // size = item.PackSize;
            const CAttr &attr = Attrs[item.decmpfs_AttrIndex];
            size = attr.Data.Size() - item.CompressHeader.DataPos;
          }
          else
            size = 0;
        }
        else
          size = (UInt64)item.DataFork.NumBlocks << Header.BlockSizeLog;
        prop = size;
        break;
      }
    case kpidSize:
      {
        UInt64 size;
        if (ref.AttrIndex >= 0)
          size = Attrs[ref.AttrIndex].GetSize();
        else if (ref.IsResource())
          size = item.ResourceFork.Size;
        else if (item.IsDir())
          break;
        else if (item.CompressHeader.IsCorrect)
          size = item.CompressHeader.UnpackSize;
        else
          size = item.DataFork.Size;
        prop = size;
        break;
      }
    case kpidIsDir: prop = (ref.IsItem() && item.IsDir()); break;
    case kpidIsAltStream: prop = ref.IsAltStream(); break;
    case kpidCTime: HfsTimeToProp(item.CTime, prop); break;
    case kpidMTime: HfsTimeToProp(item.MTime, prop); break;
    case kpidATime: HfsTimeToProp(item.ATime, prop); break;
    case kpidChangeTime: HfsTimeToProp(item.AttrMTime, prop); break;
    case kpidPosixAttrib: if (ref.IsItem()) prop = (UInt32)item.FileMode; break;
    /*
    case kpidUserId: prop = (UInt32)item.OwnerID; break;
    case kpidGroupId: prop = (UInt32)item.GroupID; break;
    */

    case kpidMethod:
      if (ref.IsItem())
        item.CompressHeader.MethodToProp(prop);
      break;
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

Z7_COM7F_IMF(CHandler::Open(IInStream *inStream,
    const UInt64 * /* maxCheckStartPosition */,
    IArchiveOpenCallback *callback))
{
  COM_TRY_BEGIN
  Close();
  RINOK(Open2(inStream, callback))
  _stream = inStream;
  return S_OK;
  COM_TRY_END
}

Z7_COM7F_IMF(CHandler::Close())
{
  _stream.Release();
  Clear();
  return S_OK;
}

static const UInt32 kCompressionBlockSize = 1 << 16;

CDecoder::CDecoder(bool IsAdlerOptional)
{
  /* Some new hfs files contain zlib resource fork without Adler checksum.
     We do not know how we must detect case where there is Adler
     checksum or there is no Adler checksum.
  */
  _zlibDecoder->IsAdlerOptional = IsAdlerOptional;
  _lzfseDecoder->LzvnMode = true;
}

HRESULT CDecoder::ExtractResourceFork_ZLIB(
    ISequentialInStream *inStream, ISequentialOutStream *outStream,
    UInt64 forkSize, UInt64 unpackSize,
    UInt64 progressStart, IArchiveExtractCallback *extractCallback)
{
  const unsigned kHeaderSize = 0x100 + 8;

  const size_t kBufSize = kCompressionBlockSize;
  _buf.Alloc(kBufSize + 0x10); // we need 1 additional bytes for uncompressed chunk header

  RINOK(ReadStream_FALSE(inStream, _buf, kHeaderSize))
  Byte *buf = _buf;
  const UInt32 dataPos = Get32(buf);
  const UInt32 mapPos = Get32(buf + 4);
  const UInt32 dataSize = Get32(buf + 8);
  const UInt32 mapSize = Get32(buf + 12);

  const UInt32 kResMapSize = 50;

  if (mapSize != kResMapSize
      || dataPos > mapPos
      || dataSize != mapPos - dataPos
      || mapSize > forkSize
      || mapPos != forkSize - mapSize)
    return S_FALSE;
  
  const UInt32 dataSize2 = Get32(buf + 0x100);
  if (4 + dataSize2 != dataSize
      || dataSize2 < 8
      || dataSize2 > dataSize)
    return S_FALSE;
  
  const UInt32 numBlocks = GetUi32(buf + 0x100 + 4);
  if (((dataSize2 - 4) >> 3) < numBlocks)
    return S_FALSE;
  {
    const UInt64 up = unpackSize + kCompressionBlockSize - 1;
    if (up < unpackSize || up / kCompressionBlockSize != numBlocks)
      return S_FALSE;
  }

  const UInt32 tableSize = (numBlocks << 3);

  _tableBuf.AllocAtLeast(tableSize);

  RINOK(ReadStream_FALSE(inStream, _tableBuf, tableSize))
  const Byte *tableBuf = _tableBuf;
  
  UInt32 prev = 4 + tableSize;

  UInt32 i;
  for (i = 0; i < numBlocks; i++)
  {
    const UInt32 offs = GetUi32(tableBuf + i * 8);
    const UInt32 size = GetUi32(tableBuf + i * 8 + 4);
    if (size == 0
        || prev != offs
        || offs > dataSize2
        || size > dataSize2 - offs)
      return S_FALSE;
    prev = offs + size;
  }

  if (prev != dataSize2)
    return S_FALSE;

  CMyComPtr2_Create<ISequentialInStream, CBufInStream> bufInStream;

  // bool padError = false;
  UInt64 outPos = 0;

  for (i = 0; i < numBlocks; i++)
  {
    const UInt64 rem = unpackSize - outPos;
    if (rem == 0)
      return S_FALSE;
    UInt32 blockSize = kCompressionBlockSize;
    if (rem < kCompressionBlockSize)
      blockSize = (UInt32)rem;

    const UInt32 size = GetUi32(tableBuf + i * 8 + 4);

    if (size > kCompressionBlockSize + 1)
      return S_FALSE;

    RINOK(ReadStream_FALSE(inStream, buf, size))

    if ((buf[0] & 0xF) == 0xF)
    {
      // (buf[0] = 0xff) is marker of uncompressed block in APFS
      // that code was not tested in HFS
      if (size - 1 != blockSize)
        return S_FALSE;

      if (outStream)
      {
        RINOK(WriteStream(outStream, buf + 1, blockSize))
      }
    }
    else
    {
      const UInt64 blockSize64 = blockSize;
      bufInStream->Init(buf, size);
      RINOK(_zlibDecoder.Interface()->Code(bufInStream, outStream, NULL, &blockSize64, NULL))
      if (_zlibDecoder->GetOutputProcessedSize() != blockSize)
        return S_FALSE;
      const UInt64 inSize = _zlibDecoder->GetInputProcessedSize();
      if (inSize != size)
      {
        if (inSize > size)
          return S_FALSE;
        // apfs file can contain junk (non-zeros) after data block.
        /*
        if (!padError)
        {
          const Byte *p = buf + (UInt32)inSize;
          const Byte *e = p + (size - (UInt32)inSize);
          do
          {
            if (*p != 0)
            {
              padError = true;
              break;
            }
          }
          while (++p != e);
        }
        */
      }
    }
    
    outPos += blockSize;
    if ((i & 0xFF) == 0)
    {
      const UInt64 progressPos = progressStart + outPos;
      RINOK(extractCallback->SetCompleted(&progressPos))
    }
  }

  if (outPos != unpackSize)
    return S_FALSE;

  // if (padError) return S_FALSE;

  /* We check Resource Map
     Are there HFS files with another values in Resource Map ??? */

  RINOK(ReadStream_FALSE(inStream, buf, mapSize))
  const UInt32 types = Get16(buf + 24);
  const UInt32 names = Get16(buf + 26);
  const UInt32 numTypes = Get16(buf + 28);
  if (numTypes != 0 || types != 28 || names != kResMapSize)
    return S_FALSE;
  const UInt32 resType = Get32(buf + 30);
  const UInt32 numResources = Get16(buf + 34);
  const UInt32 resListOffset = Get16(buf + 36);
  if (resType != 0x636D7066) // cmpf
    return S_FALSE;
  if (numResources != 0 || resListOffset != 10)
    return S_FALSE;

  const UInt32 entryId = Get16(buf + 38);
  const UInt32 nameOffset = Get16(buf + 40);
  // Byte attrib = buf[42];
  const UInt32 resourceOffset = Get32(buf + 42) & 0xFFFFFF;
  if (entryId != 1 || nameOffset != 0xFFFF || resourceOffset != 0)
    return S_FALSE;

  return S_OK;
}



HRESULT CDecoder::ExtractResourceFork_LZFSE(
    ISequentialInStream *inStream, ISequentialOutStream *outStream,
    UInt64 forkSize, UInt64 unpackSize,
    UInt64 progressStart, IArchiveExtractCallback *extractCallback)
{
  const UInt32 kNumBlocksMax = (UInt32)1 << 29;
  if (unpackSize >= (UInt64)kNumBlocksMax * kCompressionBlockSize)
    return S_FALSE;
  const UInt32 numBlocks = (UInt32)((unpackSize + kCompressionBlockSize - 1) / kCompressionBlockSize);
  const UInt32 numBlocks2 = numBlocks + 1;
  const UInt32 tableSize = (numBlocks2 << 2);
  if (tableSize > forkSize)
    return S_FALSE;
  _tableBuf.AllocAtLeast(tableSize);
  RINOK(ReadStream_FALSE(inStream, _tableBuf, tableSize))
  const Byte *tableBuf = _tableBuf;
  
  {
    UInt32 prev = GetUi32(tableBuf);
    if (prev != tableSize)
      return S_FALSE;
    for (UInt32 i = 1; i < numBlocks2; i++)
    {
      const UInt32 offs = GetUi32(tableBuf + i * 4);
      if (offs <= prev)
        return S_FALSE;
      prev = offs;
    }
    if (prev != forkSize)
      return S_FALSE;
  }

  const size_t kBufSize = kCompressionBlockSize;
  _buf.Alloc(kBufSize + 0x10); // we need 1 additional bytes for uncompressed chunk header

  CMyComPtr2_Create<ISequentialInStream, CBufInStream> bufInStream;

  UInt64 outPos = 0;

  for (UInt32 i = 0; i < numBlocks; i++)
  {
    const UInt64 rem = unpackSize - outPos;
    if (rem == 0)
      return S_FALSE;
    UInt32 blockSize = kCompressionBlockSize;
    if (rem < kCompressionBlockSize)
      blockSize = (UInt32)rem;

    const UInt32 size =
        GetUi32(tableBuf + i * 4 + 4) -
        GetUi32(tableBuf + i * 4);

    if (size > kCompressionBlockSize + 1)
      return S_FALSE;

    RINOK(ReadStream_FALSE(inStream, _buf, size))
    const Byte *buf = _buf;

    if (buf[0] == k_LZVN_Uncompressed_Marker)
    {
      if (size - 1 != blockSize)
        return S_FALSE;
      if (outStream)
      {
        RINOK(WriteStream(outStream, buf + 1, blockSize))
      }
    }
    else
    {
      const UInt64 blockSize64 = blockSize;
      const UInt64 packSize64 = size;
      bufInStream->Init(buf, size);
      RINOK(_lzfseDecoder.Interface()->Code(bufInStream, outStream, &packSize64, &blockSize64, NULL))
      // in/out sizes were checked in Code()
    }
    
    outPos += blockSize;
    if ((i & 0xFF) == 0)
    {
      const UInt64 progressPos = progressStart + outPos;
      RINOK(extractCallback->SetCompleted(&progressPos))
    }
  }

  return S_OK;
}


/*
static UInt32 GetUi24(const Byte *p)
{
  return p[0] + ((UInt32)p[1] << 8) + ((UInt32)p[2] << 24);
}

HRESULT CDecoder::ExtractResourceFork_ZBM(
    ISequentialInStream *inStream, ISequentialOutStream *outStream,
    UInt64 forkSize, UInt64 unpackSize,
    UInt64 progressStart, IArchiveExtractCallback *extractCallback)
{
  const UInt32 kNumBlocksMax = (UInt32)1 << 29;
  if (unpackSize >= (UInt64)kNumBlocksMax * kCompressionBlockSize)
    return S_FALSE;
  const UInt32 numBlocks = (UInt32)((unpackSize + kCompressionBlockSize - 1) / kCompressionBlockSize);
  const UInt32 numBlocks2 = numBlocks + 1;
  const UInt32 tableSize = (numBlocks2 << 2);
  if (tableSize > forkSize)
    return S_FALSE;
  _tableBuf.AllocAtLeast(tableSize);
  RINOK(ReadStream_FALSE(inStream, _tableBuf, tableSize));
  const Byte *tableBuf = _tableBuf;
  
  {
    UInt32 prev = GetUi32(tableBuf);
    if (prev != tableSize)
      return S_FALSE;
    for (UInt32 i = 1; i < numBlocks2; i++)
    {
      const UInt32 offs = GetUi32(tableBuf + i * 4);
      if (offs <= prev)
        return S_FALSE;
      prev = offs;
    }
    if (prev != forkSize)
      return S_FALSE;
  }

  const size_t kBufSize = kCompressionBlockSize;
  _buf.Alloc(kBufSize + 0x10); // we need 1 additional bytes for uncompressed chunk header

  CBufInStream *bufInStream = new CBufInStream;
  CMyComPtr<ISequentialInStream> bufInStream = bufInStream;

  UInt64 outPos = 0;

  for (UInt32 i = 0; i < numBlocks; i++)
  {
    const UInt64 rem = unpackSize - outPos;
    if (rem == 0)
      return S_FALSE;
    UInt32 blockSize = kCompressionBlockSize;
    if (rem < kCompressionBlockSize)
      blockSize = (UInt32)rem;

    const UInt32 size =
        GetUi32(tableBuf + i * 4 + 4) -
        GetUi32(tableBuf + i * 4);

    // if (size > kCompressionBlockSize + 1)
    if (size > blockSize + 1)
      return S_FALSE; // we don't expect it, because encode will use uncompressed chunk

    RINOK(ReadStream_FALSE(inStream, _buf, size));
    const Byte *buf = _buf;

    // (size != 0)
    // if (size == 0) return S_FALSE;

    if (buf[0] == 0xFF) // uncompressed marker
    {
      if (size != blockSize + 1)
        return S_FALSE;
      if (outStream)
      {
        RINOK(WriteStream(outStream, buf + 1, blockSize));
      }
    }
    else
    {
      if (size < 4)
        return S_FALSE;
      if (buf[0] != 'Z' ||
          buf[1] != 'B' ||
          buf[2] != 'M' ||
          buf[3] != 9)
        return S_FALSE;
      // for debug:
      unsigned packPos = 4;
      unsigned unpackPos = 0;
      unsigned packRem = size - packPos;
      for (;;)
      {
        if (packRem < 6)
          return S_FALSE;
        const UInt32 packSize = GetUi24(buf + packPos);
        const UInt32 chunkUnpackSize = GetUi24(buf + packPos + 3);
        if (packSize < 6)
          return S_FALSE;
        if (packSize > packRem)
          return S_FALSE;
        if (chunkUnpackSize > blockSize - unpackPos)
          return S_FALSE;
        packPos += packSize;
        packRem -= packSize;
        unpackPos += chunkUnpackSize;
        if (packSize == 6)
        {
          if (chunkUnpackSize != 0)
            return S_FALSE;
          break;
        }
        if (packSize >= chunkUnpackSize + 6)
        {
          if (packSize > chunkUnpackSize + 6)
            return S_FALSE;
          // uncompressed chunk;
        }
        else
        {
          // compressed chunk
          const Byte *t = buf + packPos - packSize + 6;
          UInt32 r = packSize - 6;
          if (r < 9)
            return S_FALSE;
          const UInt32 v0 = GetUi24(t);
          const UInt32 v1 = GetUi24(t + 3);
          const UInt32 v2 = GetUi24(t + 6);
          if (v0 > v1 || v1 > v2 || v2 > packSize)
            return S_FALSE;
          // here we need the code that will decompress ZBM chunk
        }
      }

      if (unpackPos != blockSize)
        return S_FALSE;

      UInt32 size1 = size;
      if (size1 > kCompressionBlockSize)
      {
        size1 = kCompressionBlockSize;
        // return S_FALSE;
      }
      if (outStream)
      {
        RINOK(WriteStream(outStream, buf, size1))

        const UInt32 kTempSize = 1 << 16;
        Byte temp[kTempSize];
        memset(temp, 0, kTempSize);

        for (UInt32 k = size1; k < kCompressionBlockSize; k++)
        {
          UInt32 cur = kCompressionBlockSize - k;
          if (cur > kTempSize)
            cur = kTempSize;
          RINOK(WriteStream(outStream, temp, cur))
          k += cur;
        }
      }

      // const UInt64 blockSize64 = blockSize;
      // const UInt64 packSize64 = size;
      // bufInStream->Init(buf, size);
      // RINOK(_zbmDecoderSpec->Code(bufInStream, outStream, &packSize64, &blockSize64, NULL));
      // in/out sizes were checked in Code()
    }
    
    outPos += blockSize;
    if ((i & 0xFF) == 0)
    {
      const UInt64 progressPos = progressStart + outPos;
      RINOK(extractCallback->SetCompleted(&progressPos));
    }
  }

  return S_OK;
}
*/

HRESULT CDecoder::Extract(
    ISequentialInStream *inStreamFork, ISequentialOutStream *realOutStream,
    UInt64 forkSize,
    const CCompressHeader &compressHeader,
    const CByteBuffer *data,
    UInt64 progressStart, IArchiveExtractCallback *extractCallback,
    int &opRes)
{
  opRes = NExtract::NOperationResult::kDataError;

  if (compressHeader.IsMethod_Uncompressed_Inline())
  {
    const size_t packSize = data->Size() - compressHeader.DataPos;
    if (realOutStream)
    {
      RINOK(WriteStream(realOutStream, *data + compressHeader.DataPos, packSize))
    }
    opRes = NExtract::NOperationResult::kOK;
    return S_OK;
  }

  if (compressHeader.Method == kMethod_ZLIB_ATTR ||
      compressHeader.Method == kMethod_LZVN_ATTR)
  {
    CMyComPtr2_Create<ISequentialInStream, CBufInStream> bufInStream;
    const size_t packSize = data->Size() - compressHeader.DataPos;
    bufInStream->Init(*data + compressHeader.DataPos, packSize);
    
    if (compressHeader.Method == kMethod_ZLIB_ATTR)
    {
      const HRESULT hres = _zlibDecoder.Interface()->Code(bufInStream, realOutStream,
          NULL, &compressHeader.UnpackSize, NULL);
      if (hres == S_OK)
        if (_zlibDecoder->GetOutputProcessedSize() == compressHeader.UnpackSize
            && _zlibDecoder->GetInputProcessedSize() == packSize)
          opRes = NExtract::NOperationResult::kOK;
      return hres;
    }
    {
      const UInt64 packSize64 = packSize;
      const HRESULT hres = _lzfseDecoder.Interface()->Code(bufInStream, realOutStream,
          &packSize64, &compressHeader.UnpackSize, NULL);
      if (hres == S_OK)
      {
        // in/out sizes were checked in Code()
        opRes = NExtract::NOperationResult::kOK;
      }
      return hres;
    }
  }

  HRESULT hres;
  if (compressHeader.Method == NHfs::kMethod_ZLIB_RSRC)
  {
    hres = ExtractResourceFork_ZLIB(
        inStreamFork, realOutStream,
        forkSize, compressHeader.UnpackSize,
        progressStart, extractCallback);
    // for debug:
    // hres = NCompress::CopyStream(inStreamFork, realOutStream, NULL);
  }
  else if (compressHeader.Method == NHfs::kMethod_LZVN_RSRC)
  {
    hres = ExtractResourceFork_LZFSE(
        inStreamFork, realOutStream,
        forkSize, compressHeader.UnpackSize,
        progressStart, extractCallback);
  }
  /*
  else if (compressHeader.Method == NHfs::kMethod_ZBM_RSRC)
  {
    hres = ExtractResourceFork_ZBM(
        inStreamFork, realOutStream,
        forkSize, compressHeader.UnpackSize,
        progressStart, extractCallback);
  }
  */
  else
  {
    opRes = NExtract::NOperationResult::kUnsupportedMethod;
    hres = S_FALSE;
  }

  if (hres == S_OK)
    opRes = NExtract::NOperationResult::kOK;
  return hres;
}


Z7_COM7F_IMF(CHandler::Extract(const UInt32 *indices, UInt32 numItems,
    Int32 testMode, IArchiveExtractCallback *extractCallback))
{
  COM_TRY_BEGIN
  const bool allFilesMode = (numItems == (UInt32)(Int32)-1);
  if (allFilesMode)
    numItems = Refs.Size();
  if (numItems == 0)
    return S_OK;
  UInt32 i;
  UInt64 totalSize = 0;
  for (i = 0; i < numItems; i++)
  {
    const CRef &ref = Refs[allFilesMode ? i : indices[i]];
    totalSize += Get_UnpackSize_of_Ref(ref);
  }
  RINOK(extractCallback->SetTotal(totalSize))

  UInt64 currentTotalSize = 0, currentItemSize = 0;
  
  const size_t kBufSize = kCompressionBlockSize;
  CByteBuffer buf(kBufSize + 0x10); // we need 1 additional bytes for uncompressed chunk header

  // there are hfs without adler in zlib.
  CDecoder decoder(true); // IsAdlerOptional

  for (i = 0;; i++, currentTotalSize += currentItemSize)
  {
    RINOK(extractCallback->SetCompleted(&currentTotalSize))
    if (i >= numItems)
      break;
    const UInt32 index = allFilesMode ? i : indices[i];
    const CRef &ref = Refs[index];
    const CItem &item = Items[ref.ItemIndex];
    currentItemSize = Get_UnpackSize_of_Ref(ref);

    int opRes;
   {
    CMyComPtr<ISequentialOutStream> realOutStream;
    const Int32 askMode = testMode ?
        NExtract::NAskMode::kTest :
        NExtract::NAskMode::kExtract;
    RINOK(extractCallback->GetStream(index, &realOutStream, askMode))

    if (ref.IsItem() && item.IsDir())
    {
      RINOK(extractCallback->PrepareOperation(askMode))
      RINOK(extractCallback->SetOperationResult(NExtract::NOperationResult::kOK))
      continue;
    }
    if (!testMode && !realOutStream)
      continue;
    
    RINOK(extractCallback->PrepareOperation(askMode))
    
    UInt64 pos = 0;
    opRes = NExtract::NOperationResult::kDataError;
    const CFork *fork = NULL;
    
    if (ref.AttrIndex >= 0)
    {
      const CAttr &attr = Attrs[ref.AttrIndex];
      if (attr.Fork_defined && attr.Data.Size() == 0)
        fork = &attr.Fork;
      else
      {
        opRes = NExtract::NOperationResult::kOK;
        if (realOutStream)
        {
          RINOK(WriteStream(realOutStream,
              // AttrBuf + attr.Pos, attr.Size
              attr.Data, attr.Data.Size()
              ))
        }
      }
    }
    else if (ref.IsResource())
      fork = &item.ResourceFork;
    else if (item.CompressHeader.IsSupported)
    {
      CMyComPtr<ISequentialInStream> inStreamFork;
      UInt64 forkSize = 0;
      const CByteBuffer *decmpfs_Data = NULL;

      if (item.CompressHeader.IsMethod_Resource())
      {
        const CFork &resourceFork = item.ResourceFork;
        forkSize = resourceFork.Size;
        GetForkStream(resourceFork, &inStreamFork);
      }
      else
      {
        const CAttr &attr = Attrs[item.decmpfs_AttrIndex];
        decmpfs_Data = &attr.Data;
      }

      if (inStreamFork || decmpfs_Data)
      {
        const HRESULT hres = decoder.Extract(
            inStreamFork, realOutStream,
            forkSize,
            item.CompressHeader,
            decmpfs_Data,
            currentTotalSize, extractCallback,
            opRes);
        if (hres != S_FALSE && hres != S_OK)
          return hres;
      }
    }
    else if (item.CompressHeader.IsCorrect)
      opRes = NExtract::NOperationResult::kUnsupportedMethod;
    else
      fork = &item.DataFork;
    
    if (fork)
    {
      if (fork->IsOk(Header.BlockSizeLog))
      {
        opRes = NExtract::NOperationResult::kOK;
        unsigned extentIndex;
        for (extentIndex = 0; extentIndex < fork->Extents.Size(); extentIndex++)
        {
          if (opRes != NExtract::NOperationResult::kOK)
            break;
          if (fork->Size == pos)
            break;
          const CExtent &e = fork->Extents[extentIndex];
          RINOK(InStream_SeekSet(_stream, SpecOffset + ((UInt64)e.Pos << Header.BlockSizeLog)))
          UInt64 extentRem = (UInt64)e.NumBlocks << Header.BlockSizeLog;
          while (extentRem != 0)
          {
            const UInt64 rem = fork->Size - pos;
            if (rem == 0)
            {
              // Here we check that there are no extra (empty) blocks in last extent.
              if (extentRem >= ((UInt64)1 << Header.BlockSizeLog))
                opRes = NExtract::NOperationResult::kDataError;
              break;
            }
            size_t cur = kBufSize;
            if (cur > rem)
              cur = (size_t)rem;
            if (cur > extentRem)
              cur = (size_t)extentRem;
            RINOK(ReadStream(_stream, buf, &cur))
            if (cur == 0)
            {
              opRes = NExtract::NOperationResult::kDataError;
              break;
            }
            if (realOutStream)
            {
              RINOK(WriteStream(realOutStream, buf, cur))
            }
            pos += cur;
            extentRem -= cur;
            const UInt64 processed = currentTotalSize + pos;
            RINOK(extractCallback->SetCompleted(&processed))
          }
        }
        if (extentIndex != fork->Extents.Size() || fork->Size != pos)
          opRes = NExtract::NOperationResult::kDataError;
      }
    }
   }
    RINOK(extractCallback->SetOperationResult(opRes))
  }
  return S_OK;
  COM_TRY_END
}

Z7_COM7F_IMF(CHandler::GetNumberOfItems(UInt32 *numItems))
{
  *numItems = Refs.Size();
  return S_OK;
}

HRESULT CHandler::GetForkStream(const CFork &fork, ISequentialInStream **stream)
{
  *stream = NULL;
  
  if (!fork.IsOk(Header.BlockSizeLog))
    return S_FALSE;

  CMyComPtr2<ISequentialInStream, CExtentsStream> extentStream;
  extentStream.Create_if_Empty();

  UInt64 rem = fork.Size;
  UInt64 virt = 0;

  FOR_VECTOR (i, fork.Extents)
  {
    const CExtent &e = fork.Extents[i];
    if (e.NumBlocks == 0)
      continue;
    UInt64 cur = ((UInt64)e.NumBlocks << Header.BlockSizeLog);
    if (cur > rem)
    {
      cur = rem;
      if (i != fork.Extents.Size() - 1)
        return S_FALSE;
    }
    CSeekExtent se;
    se.Phy = SpecOffset + ((UInt64)e.Pos << Header.BlockSizeLog);
    se.Virt = virt;
    virt += cur;
    rem -= cur;
    extentStream->Extents.Add(se);
  }
  
  if (rem != 0)
    return S_FALSE;
  
  CSeekExtent se;
  se.Phy = 0; // = SpecOffset ?
  se.Virt = virt;
  extentStream->Extents.Add(se);
  extentStream->Stream = _stream;
  extentStream->Init();
  *stream = extentStream.Detach();
  return S_OK;
}

Z7_COM7F_IMF(CHandler::GetStream(UInt32 index, ISequentialInStream **stream))
{
  *stream = NULL;

  const CRef &ref = Refs[index];
  const CFork *fork = NULL;
  if (ref.AttrIndex >= 0)
  {
    const CAttr &attr = Attrs[ref.AttrIndex];
    if (!attr.Fork_defined || attr.Data.Size() != 0)
      return S_FALSE;
    fork = &attr.Fork;
  }
  else
  {
    const CItem &item = Items[ref.ItemIndex];
    if (ref.IsResource())
      fork = &item.ResourceFork;
    else if (item.IsDir())
      return S_FALSE;
    else if (item.CompressHeader.IsCorrect)
      return S_FALSE;
    else
      fork = &item.DataFork;
  }
  return GetForkStream(*fork, stream);
}

static const Byte k_Signature[] = {
    2, 'B', 'D',
    4, 'H', '+', 0, 4,
    4, 'H', 'X', 0, 5 };

REGISTER_ARC_I(
  "HFS", "hfs hfsx", NULL, 0xE3,
  k_Signature,
  kHeaderPadSize,
  NArcInfoFlags::kMultiSignature,
  IsArc_HFS)

}}
