// ArjHandler.cpp #include "StdAfx.h" #include "../../../C/CpuArch.h" #include "../../Common/ComTry.h" #include "../../Common/IntToString.h" #include "../../Common/StringConvert.h" #include "../../Windows/PropVariant.h" #include "../../Windows/TimeUtils.h" #include "../Common/LimitedStreams.h" #include "../Common/ProgressUtils.h" #include "../Common/RegisterArc.h" #include "../Common/StreamObjects.h" #include "../Common/StreamUtils.h" #include "../Compress/CopyCoder.h" #include "../Compress/LzhDecoder.h" #include "Common/ItemNameUtils.h" #include "Common/OutStreamWithCRC.h" namespace NCompress { namespace NArj { namespace NDecoder { static const unsigned kMatchMinLen = 3; static const UInt32 kWindowSize = 1 << 15; // must be >= (1 << 14) class CCoder: public ICompressCoder, public CMyUnknownImp { CLzOutWindow _outWindow; NBitm::CDecoder _inBitStream; class CCoderReleaser { CCoder *_coder; public: CCoderReleaser(CCoder *coder): _coder(coder) {} void Disable() { _coder = NULL; } ~CCoderReleaser() { if (_coder) _coder->_outWindow.Flush(); } }; friend class CCoderReleaser; HRESULT CodeReal(UInt64 outSize, ICompressProgressInfo *progress); public: MY_UNKNOWN_IMP bool FinishMode; CCoder(): FinishMode(false) {} STDMETHOD(Code)(ISequentialInStream *inStream, ISequentialOutStream *outStream, const UInt64 *inSize, const UInt64 *outSize, ICompressProgressInfo *progress); UInt64 GetInputProcessedSize() const { return _inBitStream.GetProcessedSize(); } }; HRESULT CCoder::CodeReal(UInt64 rem, ICompressProgressInfo *progress) { const UInt32 kStep = 1 << 20; UInt64 next = 0; if (rem > kStep && progress) next = rem - kStep; while (rem != 0) { if (rem <= next) { if (_inBitStream.ExtraBitsWereRead()) return S_FALSE; UInt64 packSize = _inBitStream.GetProcessedSize(); UInt64 pos = _outWindow.GetProcessedSize(); RINOK(progress->SetRatioInfo(&packSize, &pos)); next = 0; if (rem > kStep) next = rem - kStep; } UInt32 len; { const unsigned kNumBits = 7 + 7; UInt32 val = _inBitStream.GetValue(kNumBits); if ((val & (1 << (kNumBits - 1))) == 0) { _outWindow.PutByte((Byte)(val >> 5)); _inBitStream.MovePos(1 + 8); rem--; continue; } UInt32 mask = 1 << (kNumBits - 2); unsigned w; for (w = 1; w < 7; w++, mask >>= 1) if ((val & mask) == 0) break; unsigned readBits = (w != 7 ? 1 : 0); readBits += w + w; len = (1 << w) - 1 + kMatchMinLen - 1 + (((val >> (kNumBits - readBits)) & ((1 << w) - 1))); _inBitStream.MovePos(readBits); } { const unsigned kNumBits = 4 + 13; UInt32 val = _inBitStream.GetValue(kNumBits); unsigned readBits = 1; unsigned w; if ((val & ((UInt32)1 << 16)) == 0) w = 9; else if ((val & ((UInt32)1 << 15)) == 0) w = 10; else if ((val & ((UInt32)1 << 14)) == 0) w = 11; else if ((val & ((UInt32)1 << 13)) == 0) w = 12; else { w = 13; readBits = 0; } readBits += w + w - 9; UInt32 dist = ((UInt32)1 << w) - (1 << 9) + (((val >> (kNumBits - readBits)) & ((1 << w) - 1))); _inBitStream.MovePos(readBits); if (len > rem) len = (UInt32)rem; if (!_outWindow.CopyBlock(dist, len)) return S_FALSE; rem -= len; } } if (FinishMode) { if (_inBitStream.ReadAlignBits() != 0) return S_FALSE; } if (_inBitStream.ExtraBitsWereRead()) return S_FALSE; return S_OK; } STDMETHODIMP CCoder::Code(ISequentialInStream *inStream, ISequentialOutStream *outStream, const UInt64 * /* inSize */, const UInt64 *outSize, ICompressProgressInfo *progress) { try { if (!outSize) return E_INVALIDARG; if (!_outWindow.Create(kWindowSize)) return E_OUTOFMEMORY; if (!_inBitStream.Create(1 << 17)) return E_OUTOFMEMORY; _outWindow.SetStream(outStream); _outWindow.Init(false); _inBitStream.SetStream(inStream); _inBitStream.Init(); CCoderReleaser coderReleaser(this); HRESULT res; { res = CodeReal(*outSize, progress); if (res != S_OK) return res; } coderReleaser.Disable(); return _outWindow.Flush(); } catch(const CInBufferException &e) { return e.ErrorCode; } catch(const CLzOutWindowException &e) { return e.ErrorCode; } catch(...) { return S_FALSE; } } }}} using namespace NWindows; #define Get16(p) GetUi16(p) #define Get32(p) GetUi32(p) namespace NArchive { namespace NArj { static const unsigned kBlockSizeMin = 30; static const unsigned kBlockSizeMax = 2600; static const Byte kSig0 = 0x60; static const Byte kSig1 = 0xEA; namespace NCompressionMethod { enum { kStored = 0, kCompressed1a = 1, kCompressed1b = 2, kCompressed1c = 3, kCompressed2 = 4, kNoDataNoCRC = 8, kNoData = 9 }; } namespace NFileType { enum { kBinary = 0, k7BitText, kArchiveHeader, kDirectory, kVolumeLablel, kChapterLabel }; } namespace NFlags { const Byte kGarbled = 1 << 0; const Byte kAnsiPage = 1 << 1; // or (OLD_SECURED_FLAG) obsolete const Byte kVolume = 1 << 2; const Byte kExtFile = 1 << 3; const Byte kPathSym = 1 << 4; const Byte kBackup = 1 << 5; // obsolete const Byte kSecured = 1 << 6; const Byte kDualName = 1 << 7; } namespace NHostOS { enum EEnum { kMSDOS = 0, // MS-DOS, OS/2, Win32, pkarj 2.50 (FAT / VFAT / FAT32) kPRIMOS, kUnix, kAMIGA, kMac, kOS_2, kAPPLE_GS, kAtari_ST, kNext, kVAX_VMS, kWIN95 }; } static const char * const kHostOS[] = { "MSDOS" , "PRIMOS" , "UNIX" , "AMIGA" , "MAC" , "OS/2" , "APPLE GS" , "ATARI ST" , "NEXT" , "VAX VMS" , "WIN95" }; struct CArcHeader { // Byte ArchiverVersion; // Byte ExtractVersion; Byte HostOS; // Byte Flags; // Byte SecuryVersion; // Byte FileType; // Byte Reserved; UInt32 CTime; UInt32 MTime; UInt32 ArchiveSize; // UInt32 SecurPos; // UInt16 FilespecPosInFilename; UInt16 SecurSize; // Byte EncryptionVersion; // Byte LastChapter; AString Name; AString Comment; HRESULT Parse(const Byte *p, unsigned size); }; API_FUNC_static_IsArc IsArc_Arj(const Byte *p, size_t size) { if (size < kBlockSizeMin + 4) return k_IsArc_Res_NEED_MORE; if (p[0] != kSig0 || p[1] != kSig1) return k_IsArc_Res_NO; UInt32 blockSize = Get16(p + 2); if (blockSize < kBlockSizeMin || blockSize > kBlockSizeMax) return k_IsArc_Res_NO; p += 4; size -= 4; Byte headerSize = p[0]; if (headerSize < kBlockSizeMin || headerSize > blockSize || p[6] != NFileType::kArchiveHeader || p[28] > 8) // EncryptionVersion return k_IsArc_Res_NO; if (blockSize + 4 <= size) if (Get32(p + blockSize) != CrcCalc(p, blockSize)) return k_IsArc_Res_NO; return k_IsArc_Res_YES; } } static HRESULT ReadString(const Byte *p, unsigned &size, AString &res) { unsigned num = size; for (unsigned i = 0; i < num;) { if (p[i++] == 0) { size = i; res = (const char *)p; return S_OK; } } return S_FALSE; } HRESULT CArcHeader::Parse(const Byte *p, unsigned size) { Byte headerSize = p[0]; if (headerSize < kBlockSizeMin || headerSize > size) return S_FALSE; // ArchiverVersion = p[1]; // ExtractVersion = p[2]; HostOS = p[3]; // Flags = p[4]; // SecuryVersion = p[5]; if (p[6] != NFileType::kArchiveHeader) return S_FALSE; // Reserved = p[7]; CTime = Get32(p + 8); MTime = Get32(p + 12); ArchiveSize = Get32(p + 16); // it can be zero. (currently used only for secured archives) // SecurPos = Get32(p + 20); // UInt16 filespecPositionInFilename = Get16(p + 24); SecurSize = Get16(p + 26); // EncryptionVersion = p[28]; // LastChapter = p[29]; unsigned pos = headerSize; 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; UInt32 SplitPos; Byte Version; Byte ExtractVersion; Byte HostOS; Byte Flags; Byte Method; Byte FileType; // UInt16 FilespecPosInFilename; UInt16 FileAccessMode; // Byte FirstChapter; // Byte LastChapter; UInt64 DataPosition; bool IsEncrypted() const { return (Flags & NFlags::kGarbled) != 0; } bool IsDir() const { return (FileType == NFileType::kDirectory); } bool IsSplitAfter() const { return (Flags & NFlags::kVolume) != 0; } bool IsSplitBefore() const { return (Flags & NFlags::kExtFile) != 0; } UInt32 GetWinAttrib() const { UInt32 atrrib = 0; switch (HostOS) { case NHostOS::kMSDOS: case NHostOS::kWIN95: atrrib = FileAccessMode; break; } if (IsDir()) atrrib |= FILE_ATTRIBUTE_DIRECTORY; return atrrib; } HRESULT Parse(const Byte *p, unsigned size); }; HRESULT CItem::Parse(const Byte *p, unsigned size) { Byte headerSize = p[0]; if (headerSize < kBlockSizeMin || headerSize > size) return S_FALSE; 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); // FilespecPosInFilename = Get16(p + 24); FileAccessMode = Get16(p + 26); // FirstChapter = p[28]; // FirstChapter = p[29]; SplitPos = 0; if (IsSplitBefore() && headerSize >= 34) SplitPos = Get32(p + 30); unsigned pos = headerSize; 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; } enum EErrorType { k_ErrorType_OK, k_ErrorType_Corrupted, k_ErrorType_UnexpectedEnd, }; class CArc { public: UInt64 Processed; EErrorType Error; bool IsArc; IInStream *Stream; IArchiveOpenCallback *Callback; UInt64 NumFiles; CArcHeader Header; HRESULT Open(); HRESULT GetNextItem(CItem &item, bool &filled); void Close() { IsArc = false; Error = k_ErrorType_OK; } private: UInt32 _blockSize; Byte _block[kBlockSizeMax + 4]; HRESULT ReadBlock(bool &filled, bool readSignature); HRESULT SkipExtendedHeaders(); HRESULT Read(void *data, size_t *size); }; HRESULT CArc::Read(void *data, size_t *size) { HRESULT res = ReadStream(Stream, data, size); Processed += *size; return res; } #define READ_STREAM(_dest_, _size_) \ { size_t _processed_ = (_size_); RINOK(Read(_dest_, &_processed_)); \ if (_processed_ != (_size_)) { Error = k_ErrorType_UnexpectedEnd; return S_OK; } } HRESULT CArc::ReadBlock(bool &filled, bool readSignature) { Error = k_ErrorType_OK; filled = false; Byte buf[4]; unsigned signSize = readSignature ? 2 : 0; READ_STREAM(buf, signSize + 2) if (readSignature) if (buf[0] != kSig0 || buf[1] != kSig1) { Error = k_ErrorType_Corrupted; return S_OK; } _blockSize = Get16(buf + signSize); if (_blockSize == 0) // end of archive return S_OK; if (_blockSize < kBlockSizeMin || _blockSize > kBlockSizeMax) { Error = k_ErrorType_Corrupted; return S_OK; } READ_STREAM(_block, _blockSize + 4); if (Get32(_block + _blockSize) != CrcCalc(_block, _blockSize)) { Error = k_ErrorType_Corrupted; return S_OK; } filled = true; return S_OK; } HRESULT CArc::SkipExtendedHeaders() { for (UInt32 i = 0;; i++) { bool filled; RINOK(ReadBlock(filled, false)); if (!filled) return S_OK; if (Callback && (i & 0xFF) == 0) RINOK(Callback->SetCompleted(&NumFiles, &Processed)); } } HRESULT CArc::Open() { bool filled; RINOK(ReadBlock(filled, true)); if (!filled) return S_FALSE; RINOK(Header.Parse(_block, _blockSize)); IsArc = true; return SkipExtendedHeaders(); } HRESULT CArc::GetNextItem(CItem &item, bool &filled) { RINOK(ReadBlock(filled, true)); if (!filled) return S_OK; filled = false; if (item.Parse(_block, _blockSize) != S_OK) { Error = k_ErrorType_Corrupted; return S_OK; } /* UInt32 extraData; if ((header.Flags & NFlags::kExtFile) != 0) extraData = GetUi32(_block + pos); */ RINOK(SkipExtendedHeaders()); filled = true; return S_OK; } class CHandler: public IInArchive, public CMyUnknownImp { CObjectVector _items; CMyComPtr _stream; UInt64 _phySize; CArc _arc; public: MY_UNKNOWN_IMP1(IInArchive) INTERFACE_IInArchive(;) HRESULT Open2(IInStream *inStream, IArchiveOpenCallback *callback); }; static const Byte kArcProps[] = { kpidName, kpidCTime, kpidMTime, kpidHostOS, kpidComment }; static const Byte kProps[] = { kpidPath, kpidIsDir, kpidSize, kpidPosition, kpidPackSize, kpidMTime, kpidAttrib, kpidEncrypted, kpidCRC, kpidMethod, kpidHostOS, kpidComment }; IMP_IInArchive_Props IMP_IInArchive_ArcProps static void SetTime(UInt32 dosTime, 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, NCOM::CPropVariant &prop) { char temp[16]; const char *s = NULL; if (hostOS < ARRAY_SIZE(kHostOS)) s = kHostOS[hostOS]; else { ConvertUInt32ToString(hostOS, temp); s = temp; } prop = s; } static void SetUnicodeString(const AString &s, NCOM::CPropVariant &prop) { if (!s.IsEmpty()) prop = MultiByteToUnicodeString(s, CP_OEMCP); } STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value) { COM_TRY_BEGIN NCOM::CPropVariant prop; switch (propID) { case kpidPhySize: prop = _phySize; break; case kpidName: SetUnicodeString(_arc.Header.Name, prop); break; case kpidCTime: SetTime(_arc.Header.CTime, prop); break; case kpidMTime: SetTime(_arc.Header.MTime, prop); break; case kpidHostOS: SetHostOS(_arc.Header.HostOS, prop); break; case kpidComment: SetUnicodeString(_arc.Header.Comment, prop); break; case kpidErrorFlags: { UInt32 v = 0; if (!_arc.IsArc) v |= kpv_ErrorFlags_IsNotArc; switch (_arc.Error) { case k_ErrorType_UnexpectedEnd: v |= kpv_ErrorFlags_UnexpectedEnd; break; case k_ErrorType_Corrupted: v |= kpv_ErrorFlags_HeadersError; break; } prop = v; 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 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 kpidPosition: if (item.IsSplitBefore() || item.IsSplitAfter()) prop = (UInt64)item.SplitPos; break; case kpidAttrib: prop = item.GetWinAttrib(); 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, IArchiveOpenCallback *callback) { Close(); UInt64 endPos = 0; RINOK(inStream->Seek(0, STREAM_SEEK_END, &endPos)); RINOK(inStream->Seek(0, STREAM_SEEK_SET, NULL)); _arc.Stream = inStream; _arc.Callback = callback; _arc.NumFiles = 0; _arc.Processed = 0; RINOK(_arc.Open()); _phySize = _arc.Processed; if (_arc.Header.ArchiveSize != 0) _phySize = (UInt64)_arc.Header.ArchiveSize + _arc.Header.SecurSize; for (;;) { CItem item; bool filled; _arc.Error = k_ErrorType_OK; RINOK(_arc.GetNextItem(item, filled)); if (_arc.Error != k_ErrorType_OK) break; if (!filled) { if (_arc.Error == k_ErrorType_OK) if (_arc.Header.ArchiveSize == 0) _phySize = _arc.Processed; break; } item.DataPosition = _arc.Processed; _items.Add(item); UInt64 pos = item.DataPosition + item.PackSize; if (_arc.Header.ArchiveSize == 0) _phySize = pos; if (pos > endPos) { _arc.Error = k_ErrorType_UnexpectedEnd; break; } RINOK(inStream->Seek(pos, STREAM_SEEK_SET, NULL)); _arc.NumFiles = _items.Size(); _arc.Processed = pos; if (callback && (_items.Size() & 0xFF) == 0) { RINOK(callback->SetCompleted(&_arc.NumFiles, &_arc.Processed)); } } return S_OK; } STDMETHODIMP CHandler::Open(IInStream *inStream, const UInt64 * /* maxCheckStartPosition */, IArchiveOpenCallback *callback) { COM_TRY_BEGIN HRESULT res; { res = Open2(inStream, callback); if (res == S_OK) { _stream = inStream; return S_OK; } } return res; COM_TRY_END } STDMETHODIMP CHandler::Close() { _arc.Close(); _phySize = 0; _items.Clear(); _stream.Release(); return S_OK; } STDMETHODIMP CHandler::Extract(const UInt32 *indices, UInt32 numItems, Int32 testMode, IArchiveExtractCallback *extractCallback) { COM_TRY_BEGIN UInt64 totalUnpacked = 0, totalPacked = 0; bool allFilesMode = (numItems == (UInt32)(Int32)-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; NCompress::NLzh::NDecoder::CCoder *lzhDecoderSpec = NULL; CMyComPtr lzhDecoder; NCompress::NArj::NDecoder::CCoder *arjDecoderSpec = NULL; CMyComPtr arjDecoder; NCompress::CCopyCoder *copyCoderSpec = new NCompress::CCopyCoder(); CMyComPtr copyCoder = copyCoderSpec; CLocalProgress *lps = new CLocalProgress; CMyComPtr progress = lps; lps->Init(extractCallback, false); CLimitedSequentialInStream *inStreamSpec = new CLimitedSequentialInStream; CMyComPtr 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 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 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 NCompressionMethod::kStored: { result = copyCoder->Code(inStream, outStream, NULL, NULL, progress); if (result == S_OK && copyCoderSpec->TotalSize != item.PackSize) result = S_FALSE; break; } case NCompressionMethod::kCompressed1a: case NCompressionMethod::kCompressed1b: case NCompressionMethod::kCompressed1c: { if (!lzhDecoder) { lzhDecoderSpec = new NCompress::NLzh::NDecoder::CCoder; lzhDecoder = lzhDecoderSpec; } lzhDecoderSpec->FinishMode = true; const UInt32 kHistorySize = 26624; lzhDecoderSpec->SetDictSize(kHistorySize); result = lzhDecoder->Code(inStream, outStream, NULL, &curUnpacked, progress); if (result == S_OK && lzhDecoderSpec->GetInputProcessedSize() != item.PackSize) result = S_FALSE; break; } case NCompressionMethod::kCompressed2: { if (!arjDecoder) { arjDecoderSpec = new NCompress::NArj::NDecoder::CCoder; arjDecoder = arjDecoderSpec; } arjDecoderSpec->FinishMode = true; result = arjDecoder->Code(inStream, outStream, NULL, &curUnpacked, progress); if (result == S_OK && arjDecoderSpec->GetInputProcessedSize() != item.PackSize) result = S_FALSE; 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 const Byte k_Signature[] = { kSig0, kSig1 }; REGISTER_ARC_I( "Arj", "arj", 0, 4, k_Signature, 0, 0, IsArc_Arj) }}