// ArjHandler.cpp

#include "StdAfx.h"

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

#include "Windows/PropVariant.h"
#include "Windows/Time.h"

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

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

#include "../Compress/Arj/ArjDecoder1.h"
#include "../Compress/Arj/ArjDecoder2.h"
#include "../Compress/Copy/CopyCoder.h"

#include "IArchive.h"

#include "Common/ItemNameUtils.h"
#include "Common/OutStreamWithCRC.h"

using namespace NWindows;

#define Get16(p) GetUi16(p)
#define Get32(p) GetUi32(p)

namespace NArchive {
namespace NArj {

const int kBlockSizeMin = 30;
const int kBlockSizeMax = 2600;

namespace NSignature
{
  const Byte kSig0 = 0x60;
  const Byte kSig1 = 0xEA;
}

namespace NFileHeader
{
  namespace NCompressionMethod
  {
    enum
    {
      kStored = 0,
      kCompressed1a = 1,
      kCompressed1b = 2,
      kCompressed1c = 3,
      kCompressed2 = 4,
      kNoDataNoCRC = 8,
      kNoData = 9
    };
  }

  namespace NFileType
  {
    enum
    {
      kBinary = 0,
      k7BitText = 1,
      kArchiveHeader = 2,
      kDirectory = 3,
      kVolumeLablel = 4,
      kChapterLabel = 5
    };
  }
  
  namespace NFlags
  {
    const Byte kGarbled = 1;
    const Byte kVolume = 4;
    const Byte kExtFile = 8;
    const Byte kPathSym = 0x10;
    const Byte kBackup = 0x20;
  }

  namespace NHostOS
  {
    enum EEnum
    {
      kMSDOS = 0,  // filesystem used by MS-DOS, OS/2, Win32
          // pkarj 2.50 (FAT / VFAT / FAT32 file systems)
      kPRIMOS,
      kUnix,
      kAMIGA,
      kMac,
      kOS_2,
      kAPPLE_GS,
      kAtari_ST,
      kNext,
      kVAX_VMS,
      kWIN95
    };
  }
}

struct CArchiveHeader
{
  // Byte ArchiverVersion;
  // Byte ExtractVersion;
  Byte HostOS;
  // Byte Flags;
  // Byte SecuryVersion;
  // Byte FileType;
  // Byte Reserved;
  UInt32 CTime;
  UInt32 MTime;
  UInt32 ArchiveSize;
  // UInt32 SecurityEnvelopeFilePosition;
  // UInt16 FilespecPositionInFilename;
  // UInt16 LengthOfSecurityEnvelopeSata;
  // Byte EncryptionVersion;
  // Byte LastChapter;
  AString Name;
  AString Comment;
  
  HRESULT Parse(const Byte *p, unsigned size);
};

static HRESULT ReadString(const Byte *p, unsigned &size, AString &res)
{
  AString s;
  for (unsigned i = 0; i < size;)
  {
    char c = (char)p[i++];
    if (c == 0)
    {
      size = i;
      res = s;
      return S_OK;
    }
    s += c;
  }
  return S_FALSE;
}

HRESULT CArchiveHeader::Parse(const Byte *p, unsigned size)
{
  if (size < kBlockSizeMin)
    return S_FALSE;
  Byte firstHeaderSize = p[0];
  if (firstHeaderSize > size)
    return S_FALSE;
  // ArchiverVersion = p[1];
  // ExtractVersion = p[2];
  HostOS = p[3];
  // Flags = p[4];
  // SecuryVersion = p[5];
  if (p[6] != NFileHeader::NFileType::kArchiveHeader)
    return S_FALSE;
  // Reserved = p[7];
  CTime = Get32(p + 8);
  MTime = Get32(p + 12);
  ArchiveSize = Get32(p + 16);
  // SecurityEnvelopeFilePosition = Get32(p + 20);
  // UInt16 filespecPositionInFilename = Get16(p + 24);
  // LengthOfSecurityEnvelopeSata = Get16(p + 26);
  // EncryptionVersion = p[28];
  // LastChapter = p[29];
  unsigned pos = firstHeaderSize;
  unsigned size1 = size - pos;
  RINOK(ReadString(p + pos, size1, Name));
  pos += size1;
  size1 = size - pos;
  RINOK(ReadString(p + pos, size1, Comment));
  pos += size1;
  return S_OK;
}

struct CItem
{
  AString Name;
  AString Comment;

  UInt32 MTime;
  UInt32 PackSize;
  UInt32 Size;
  UInt32 FileCRC;

  Byte Version;
  Byte ExtractVersion;
  Byte HostOS;
  Byte Flags;
  Byte Method;
  Byte FileType;

  // UInt16 FilespecPositionInFilename;
  UInt16 FileAccessMode;
  // Byte FirstChapter;
  // Byte LastChapter;
  
  UInt64 DataPosition;
  
  bool IsEncrypted() const { return (Flags & NFileHeader::NFlags::kGarbled) != 0; }
  bool IsDir() const { return (FileType == NFileHeader::NFileType::kDirectory); }
  UInt32 GetWinAttributes() const
  {
    UInt32 winAtrributes;
    switch(HostOS)
    {
      case NFileHeader::NHostOS::kMSDOS:
      case NFileHeader::NHostOS::kWIN95:
        winAtrributes = FileAccessMode;
        break;
      default:
        winAtrributes = 0;
    }
    if (IsDir())
      winAtrributes |= FILE_ATTRIBUTE_DIRECTORY;
    return winAtrributes;
  }

  HRESULT Parse(const Byte *p, unsigned size);
};

HRESULT CItem::Parse(const Byte *p, unsigned size)
{
  if (size < kBlockSizeMin)
    return S_FALSE;

  Byte firstHeaderSize = p[0];

  Version = p[1];
  ExtractVersion = p[2];
  HostOS = p[3];
  Flags = p[4];
  Method = p[5];
  FileType = p[6];
  // Reserved = p[7];
  MTime = Get32(p + 8);
  PackSize = Get32(p + 12);
  Size = Get32(p + 16);
  FileCRC = Get32(p + 20);
  // FilespecPositionInFilename = Get16(p + 24);
  FileAccessMode = Get16(p + 26);
  // FirstChapter = p[28];
  // FirstChapter = p[29];

  unsigned pos = firstHeaderSize;
  unsigned size1 = size - pos;
  RINOK(ReadString(p + pos, size1, Name));
  pos += size1;
  size1 = size - pos;
  RINOK(ReadString(p + pos, size1, Comment));
  pos += size1;

  return S_OK;
}

struct CInArchiveException
{
  enum CCauseType
  {
    kUnexpectedEndOfArchive = 0,
    kCRCError,
    kIncorrectArchive,
  }
  Cause;
  CInArchiveException(CCauseType cause): Cause(cause) {};
};

class CInArchive
{
  UInt32 _blockSize;
  Byte _block[kBlockSizeMax + 4];
  
  HRESULT ReadBlock(bool &filled);
  HRESULT ReadSignatureAndBlock(bool &filled);
  HRESULT SkeepExtendedHeaders();

  HRESULT SafeReadBytes(void *data, UInt32 size);
    
public:
  CArchiveHeader Header;

  IInStream *Stream;
  IArchiveOpenCallback *Callback;
  UInt64 NumFiles;
  UInt64 NumBytes;

  HRESULT Open(const UInt64 *searchHeaderSizeLimit);
  HRESULT GetNextItem(bool &filled, CItem &item);
};

static inline bool TestMarkerCandidate(const Byte *p, unsigned maxSize)
{
  if (p[0] != NSignature::kSig0 || p[1] != NSignature::kSig1)
    return false;
  UInt32 blockSize = Get16(p + 2);
  p += 4;
  if (p[6] != NFileHeader::NFileType::kArchiveHeader ||
      p[0] > blockSize ||
      maxSize < 2 + 2 + blockSize + 4 ||
      blockSize < kBlockSizeMin || blockSize > kBlockSizeMax ||
      p[28] > 8) // EncryptionVersion
    return false;
  // return (Get32(p + blockSize) == CrcCalc(p, blockSize));
  return true;
}

static HRESULT FindAndReadMarker(ISequentialInStream *stream, const UInt64 *searchHeaderSizeLimit, UInt64 &position)
{
  position = 0;

  const int kMarkerSizeMin = 2 + 2 + kBlockSizeMin + 4;
  const int kMarkerSizeMax = 2 + 2 + kBlockSizeMax + 4;

  CByteBuffer byteBuffer;
  const UInt32 kBufSize = 1 << 16;
  byteBuffer.SetCapacity(kBufSize);
  Byte *buf = byteBuffer;

  size_t processedSize = kMarkerSizeMax;
  RINOK(ReadStream(stream, buf, &processedSize));
  if (processedSize < kMarkerSizeMin)
    return S_FALSE;
  if (TestMarkerCandidate(buf, (unsigned)processedSize))
    return S_OK;

  UInt32 numBytesPrev = (UInt32)processedSize - 1;
  memmove(buf, buf + 1, numBytesPrev);
  UInt64 curTestPos = 1;
  for (;;)
  {
    if (searchHeaderSizeLimit != NULL)
      if (curTestPos > *searchHeaderSizeLimit)
        return S_FALSE;
    processedSize = kBufSize - numBytesPrev;
    RINOK(ReadStream(stream, buf + numBytesPrev, &processedSize));
    UInt32 numBytesInBuffer = numBytesPrev + (UInt32)processedSize;
    if (numBytesInBuffer < kMarkerSizeMin)
      return S_FALSE;
    UInt32 numTests = numBytesInBuffer - kMarkerSizeMin + 1;
    UInt32 pos;
    for (pos = 0; pos < numTests; pos++)
    {
      for (; buf[pos] != NSignature::kSig0 && pos < numTests; pos++);
      if (pos == numTests)
        break;
      if (TestMarkerCandidate(buf + pos, numBytesInBuffer - pos))
      {
        position = curTestPos + pos;
        return S_OK;
      }
    }
    curTestPos += pos;
    numBytesPrev = numBytesInBuffer - numTests;
    memmove(buf, buf + numTests, numBytesPrev);
  }
}

HRESULT CInArchive::SafeReadBytes(void *data, UInt32 size)
{
  size_t processed = size;
  RINOK(ReadStream(Stream, data, &processed));
  if (processed != size)
    throw CInArchiveException(CInArchiveException::kUnexpectedEndOfArchive);
  return S_OK;
}

HRESULT CInArchive::ReadBlock(bool &filled)
{
  filled = false;
  Byte buf[2];
  RINOK(SafeReadBytes(buf, 2));
  _blockSize = Get16(buf);
  if (_blockSize == 0)
    return S_OK;
  if (_blockSize > kBlockSizeMax)
    throw CInArchiveException(CInArchiveException::kIncorrectArchive);
  RINOK(SafeReadBytes(_block, _blockSize + 4));
  NumBytes += _blockSize + 6;
  if (Get32(_block + _blockSize) != CrcCalc(_block, _blockSize))
    throw CInArchiveException(CInArchiveException::kCRCError);
  filled = true;
  return S_OK;
}

HRESULT CInArchive::ReadSignatureAndBlock(bool &filled)
{
  Byte id[2];
  RINOK(SafeReadBytes(id, 2));
  if (id[0] != NSignature::kSig0 || id[1] != NSignature::kSig1)
    throw CInArchiveException(CInArchiveException::kIncorrectArchive);
  return ReadBlock(filled);
}

HRESULT CInArchive::SkeepExtendedHeaders()
{
  for (UInt32 i = 0;; i++)
  {
    bool filled;
    RINOK(ReadBlock(filled));
    if (!filled)
      return S_OK;
    if (Callback && (i & 0xFF) == 0)
      RINOK(Callback->SetCompleted(&NumFiles, &NumBytes));
  }
}

HRESULT CInArchive::Open(const UInt64 *searchHeaderSizeLimit)
{
  UInt64 position = 0;
  RINOK(FindAndReadMarker(Stream, searchHeaderSizeLimit, position));
  RINOK(Stream->Seek(position, STREAM_SEEK_SET, NULL));
  bool filled;
  RINOK(ReadSignatureAndBlock(filled));
  if (!filled)
    return S_FALSE;
  RINOK(Header.Parse(_block, _blockSize));
  return SkeepExtendedHeaders();
}

HRESULT CInArchive::GetNextItem(bool &filled, CItem &item)
{
  RINOK(ReadSignatureAndBlock(filled));
  if (!filled)
    return S_OK;
  filled = false;
  RINOK(item.Parse(_block, _blockSize));
  /*
  UInt32 extraData;
  if ((header.Flags & NFileHeader::NFlags::kExtFile) != 0)
    extraData = GetUInt32FromMemLE(_block + pos);
  */

  RINOK(SkeepExtendedHeaders());
  filled = true;
  return S_OK;
}

class CHandler:
  public IInArchive,
  public CMyUnknownImp
{
public:
  MY_UNKNOWN_IMP1(IInArchive)

  INTERFACE_IInArchive(;)

  HRESULT Open2(IInStream *inStream, const UInt64 *maxCheckStartPosition,
      IArchiveOpenCallback *callback);
private:
  CInArchive _archive;
  CObjectVector<CItem> _items;
  CMyComPtr<IInStream> _stream;
};

const wchar_t *kHostOS[] =
{
  L"MSDOS",
  L"PRIMOS",
  L"UNIX",
  L"AMIGA",
  L"MAC",
  L"OS/2",
  L"APPLE GS",
  L"ATARI ST",
  L"NEXT",
  L"VAX VMS",
  L"WIN95"
};

const wchar_t *kUnknownOS = L"Unknown";

const int kNumHostOSes = sizeof(kHostOS) / sizeof(kHostOS[0]);

STATPROPSTG kArcProps[] =
{
  { NULL, kpidName, VT_BSTR},
  { NULL, kpidCTime, VT_BSTR},
  { NULL, kpidMTime, VT_BSTR},
  { NULL, kpidHostOS, VT_BSTR},
  { NULL, kpidComment, VT_BSTR}
};

STATPROPSTG kProps[] =
{
  { NULL, kpidPath, VT_BSTR},
  { NULL, kpidIsDir, VT_BOOL},
  { NULL, kpidSize, VT_UI8},
  { NULL, kpidPackSize, VT_UI8},
  { NULL, kpidMTime, VT_FILETIME},
  { NULL, kpidAttrib, VT_UI4},
  { NULL, kpidEncrypted, VT_BOOL},
  { NULL, kpidCRC, VT_UI4},
  { NULL, kpidMethod, VT_UI1},
  { NULL, kpidHostOS, VT_BSTR},
  { NULL, kpidComment, VT_BSTR}
};

IMP_IInArchive_Props
IMP_IInArchive_ArcProps

static void SetTime(UInt32 dosTime, NWindows::NCOM::CPropVariant &prop)
{
  if (dosTime == 0)
    return;
  FILETIME localFileTime, utc;
  if (NTime::DosTimeToFileTime(dosTime, localFileTime))
  {
    if (!LocalFileTimeToFileTime(&localFileTime, &utc))
      utc.dwHighDateTime = utc.dwLowDateTime = 0;
  }
  else
    utc.dwHighDateTime = utc.dwLowDateTime = 0;
  prop = utc;
}

static void SetHostOS(Byte hostOS, NWindows::NCOM::CPropVariant &prop)
{
  prop = hostOS < kNumHostOSes ? kHostOS[hostOS] : kUnknownOS;
}

static void SetUnicodeString(const AString &s, NWindows::NCOM::CPropVariant &prop)
{
  if (!s.IsEmpty())
    prop = MultiByteToUnicodeString(s, CP_OEMCP);
}
 
STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NWindows::NCOM::CPropVariant prop;
  switch(propID)
  {
    case kpidName:  SetUnicodeString(_archive.Header.Name, prop); break;
    case kpidCTime:  SetTime(_archive.Header.CTime, prop); break;
    case kpidMTime:  SetTime(_archive.Header.MTime, prop); break;
    case kpidHostOS:  SetHostOS(_archive.Header.HostOS, prop); break;
    case kpidComment:  SetUnicodeString(_archive.Header.Comment, prop); break;
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

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

STDMETHODIMP CHandler::GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NWindows::NCOM::CPropVariant prop;
  const CItem &item = _items[index];
  switch(propID)
  {
    case kpidPath:  prop = NItemName::GetOSName(MultiByteToUnicodeString(item.Name, CP_OEMCP)); break;
    case kpidIsDir:  prop = item.IsDir(); break;
    case kpidSize:  prop = item.Size; break;
    case kpidPackSize:  prop = item.PackSize; break;
    case kpidAttrib:  prop = item.GetWinAttributes(); break;
    case kpidEncrypted:  prop = item.IsEncrypted(); break;
    case kpidCRC:  prop = item.FileCRC; break;
    case kpidMethod:  prop = item.Method; break;
    case kpidHostOS:  SetHostOS(item.HostOS, prop); break;
    case kpidMTime:  SetTime(item.MTime, prop); break;
    case kpidComment:  SetUnicodeString(item.Comment, prop); break;
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

HRESULT CHandler::Open2(IInStream *inStream, const UInt64 *maxCheckStartPosition,
      IArchiveOpenCallback *callback)
{
  Close();
  
  UInt64 endPos = 0;
  if (callback != NULL)
  {
    RINOK(inStream->Seek(0, STREAM_SEEK_END, &endPos));
    RINOK(inStream->Seek(0, STREAM_SEEK_SET, NULL));
  }
  
  _archive.Stream = inStream;
  _archive.Callback = callback;
  _archive.NumFiles = _archive.NumBytes = 0;

  RINOK(_archive.Open(maxCheckStartPosition));
  if (callback != NULL)
    RINOK(callback->SetTotal(NULL, &endPos));
  for (;;)
  {
    CItem item;
    bool filled;


    RINOK(_archive.GetNextItem(filled, item));
    
    RINOK(inStream->Seek(0, STREAM_SEEK_CUR, &item.DataPosition));
    
    if (!filled)
      break;
    _items.Add(item);
    
    if (inStream->Seek(item.PackSize, STREAM_SEEK_CUR, NULL) != S_OK)
      throw CInArchiveException(CInArchiveException::kUnexpectedEndOfArchive);

    _archive.NumFiles = _items.Size();
    _archive.NumBytes = item.DataPosition;
    
    if (callback != NULL && _items.Size() % 100 == 0)
    {
      RINOK(callback->SetCompleted(&_archive.NumFiles, &_archive.NumBytes));
    }
  }
  return S_OK;
}

STDMETHODIMP CHandler::Open(IInStream *inStream,
    const UInt64 *maxCheckStartPosition, IArchiveOpenCallback *callback)
{
  COM_TRY_BEGIN
  HRESULT res;
  try
  {
    res = Open2(inStream, maxCheckStartPosition, callback);
    if (res == S_OK)
    {
      _stream = inStream;
      return S_OK;
    }
  }
  catch(const CInArchiveException &) { res = S_FALSE; }
  Close();
  return res;
  COM_TRY_END
}

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

STDMETHODIMP CHandler::Extract(const UInt32* indices, UInt32 numItems,
    Int32 testModeSpec, IArchiveExtractCallback *extractCallback)
{
  COM_TRY_BEGIN
  bool testMode = (testModeSpec != 0);
  UInt64 totalUnpacked = 0, totalPacked = 0;
  bool allFilesMode = (numItems == UInt32(-1));
  if (allFilesMode)
    numItems = _items.Size();
  if (numItems == 0)
    return S_OK;
  UInt32 i;
  for (i = 0; i < numItems; i++)
  {
    const CItem &item = _items[allFilesMode ? i : indices[i]];
    totalUnpacked += item.Size;
    totalPacked += item.PackSize;
  }
  extractCallback->SetTotal(totalUnpacked);

  totalUnpacked = totalPacked = 0;
  UInt64 curUnpacked, curPacked;
  
  CMyComPtr<ICompressCoder> arj1Decoder;
  CMyComPtr<ICompressCoder> arj2Decoder;
  NCompress::CCopyCoder *copyCoderSpec = new NCompress::CCopyCoder();
  CMyComPtr<ICompressCoder> copyCoder = copyCoderSpec;

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

  CLimitedSequentialInStream *inStreamSpec = new CLimitedSequentialInStream;
  CMyComPtr<ISequentialInStream> inStream(inStreamSpec);
  inStreamSpec->SetStream(_stream);

  for (i = 0; i < numItems; i++, totalUnpacked += curUnpacked, totalPacked += curPacked)
  {
    lps->InSize = totalPacked;
    lps->OutSize = totalUnpacked;
    RINOK(lps->SetCur());

    curUnpacked = curPacked = 0;

    CMyComPtr<ISequentialOutStream> realOutStream;
    Int32 askMode = testMode ?
        NExtract::NAskMode::kTest :
        NExtract::NAskMode::kExtract;
    Int32 index = allFilesMode ? i : indices[i];
    const CItem &item = _items[index];
    RINOK(extractCallback->GetStream(index, &realOutStream, askMode));

    if (item.IsDir())
    {
      // if (!testMode)
      {
        RINOK(extractCallback->PrepareOperation(askMode));
        RINOK(extractCallback->SetOperationResult(NExtract::NOperationResult::kOK));
      }
      continue;
    }

    if (!testMode && (!realOutStream))
      continue;

    RINOK(extractCallback->PrepareOperation(askMode));
    curUnpacked = item.Size;
    curPacked = item.PackSize;

    {
      COutStreamWithCRC *outStreamSpec = new COutStreamWithCRC;
      CMyComPtr<ISequentialOutStream> outStream(outStreamSpec);
      outStreamSpec->SetStream(realOutStream);
      realOutStream.Release();
      outStreamSpec->Init();
  
      inStreamSpec->Init(item.PackSize);
      
      UInt64 pos;
      _stream->Seek(item.DataPosition, STREAM_SEEK_SET, &pos);

      HRESULT result = S_OK;
      Int32 opRes = NExtract::NOperationResult::kOK;

      if (item.IsEncrypted())
        opRes = NExtract::NOperationResult::kUnSupportedMethod;
      else
      {
        switch(item.Method)
        {
          case NFileHeader::NCompressionMethod::kStored:
          {
            result = copyCoder->Code(inStream, outStream, NULL, NULL, progress);
            if (result == S_OK && copyCoderSpec->TotalSize != item.PackSize)
              result = S_FALSE;
            break;
          }
          case NFileHeader::NCompressionMethod::kCompressed1a:
          case NFileHeader::NCompressionMethod::kCompressed1b:
          case NFileHeader::NCompressionMethod::kCompressed1c:
          {
            if (!arj1Decoder)
              arj1Decoder = new NCompress::NArj::NDecoder1::CCoder;
            result = arj1Decoder->Code(inStream, outStream, NULL, &curUnpacked, progress);
            break;
          }
          case NFileHeader::NCompressionMethod::kCompressed2:
          {
            if (!arj2Decoder)
              arj2Decoder = new NCompress::NArj::NDecoder2::CCoder;
            result = arj2Decoder->Code(inStream, outStream, NULL, &curUnpacked, progress);
            break;
          }
          default:
            opRes = NExtract::NOperationResult::kUnSupportedMethod;
        }
      }
      if (opRes == NExtract::NOperationResult::kOK)
      {
        if (result == S_FALSE)
          opRes = NExtract::NOperationResult::kDataError;
        else
        {
          RINOK(result);
          opRes = (outStreamSpec->GetCRC() == item.FileCRC) ?
              NExtract::NOperationResult::kOK:
              NExtract::NOperationResult::kCRCError;
        }
      }
      outStream.Release();
      RINOK(extractCallback->SetOperationResult(opRes));
    }
  }
  return S_OK;
  COM_TRY_END
}

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

static CArcInfo g_ArcInfo =
  { L"Arj", L"arj", 0, 4, { 0x60, 0xEA }, 2, false, CreateArc, 0 };

REGISTER_ARC(Arj)

}}
