/// <summary> /// Create a new ZipFileEntry from a stream and a filename /// </summary> /// <param name="zipstream">Stream representing the entry</param> /// <param name="filename">Internal filename to use</param> public ZipFileEntry(Stream zipstream, string filename, uint lastMod = Constants.TorrentZipFileDateTime) { _zip64 = false; _zipstream = zipstream; _generalPurposeBitFlag = GeneralPurposeBitFlag.DeflatingMaximumCompression; _compressionMethod = CompressionMethod.Deflated; _lastMod = lastMod; FileName = filename; }
/// <summary> /// Write the local file header entry to the included stream /// </summary> public void WriteHeader() { // Open the stream for writing BinaryWriter bw = new BinaryWriter(_zipstream); // Create an empty extra field to start out with List <byte> extraField = new List <byte>(); // Figure out if we're in Zip64 based on the size _zip64 = _uncompressedSize >= 0xffffffff; // Now check for a unicode filename and set the flag accordingly byte[] fileNameBytes; if (Utilities.IsUnicode(_fileName)) { _generalPurposeBitFlag |= GeneralPurposeBitFlag.LanguageEncodingFlag; fileNameBytes = Encoding.UTF8.GetBytes(_fileName); } else { fileNameBytes = Encoding.GetEncoding(858).GetBytes(_fileName); } // Set the version needed to extract according to if it's Zip64 ushort versionNeededToExtract = (ushort)(_zip64 ? ArchiveVersion.TorrentZip64 : ArchiveVersion.TorrentZip); // Now save the relative offset and write _relativeOffset = (ulong)_zipstream.Position; bw.Write(Constants.LocalFileHeaderSignature); bw.Write(versionNeededToExtract); bw.Write((ushort)_generalPurposeBitFlag); bw.Write((ushort)_compressionMethod); bw.Write(_lastMod); _crc32Location = (ulong)_zipstream.Position; // Now, write dummy bytes for crc, compressed size, and uncompressed size bw.Write(0xffffffff); bw.Write(0xffffffff); bw.Write(0xffffffff); // If we have Zip64, add the right things to the extra field if (_zip64) { for (int i = 0; i < 20; i++) { extraField.Add(0); } } // Write out the lengths and their associated fields ushort fileNameLength = (ushort)fileNameBytes.Length; bw.Write(fileNameLength); ushort extraFieldLength = (ushort)extraField.Count; bw.Write(extraFieldLength); bw.Write(fileNameBytes, 0, fileNameLength); _extraLocation = (ulong)_zipstream.Position; bw.Write(extraField.ToArray(), 0, extraFieldLength); }
/// <summary> /// Read the local file header from the input stream, assuming correctness /// </summary> /// <returns>Status of the underlying stream</returns> public ZipReturn ReadHeaderQuick() { try { // We assume that the file is torrentzip until proven otherwise _torrentZip = true; // Open the stream for reading BinaryReader br = new BinaryReader(_zipstream); // Set the position of the writer based on the entry information br.BaseStream.Seek((long)_relativeOffset, SeekOrigin.Begin); // If the first bytes aren't a local file header, log and return if (br.ReadUInt32() != Constants.LocalFileHeaderSignature) { return(ZipReturn.ZipLocalFileHeaderError); } // Now read in available information, ignoring unneeded _versionNeeded = (ArchiveVersion)br.ReadUInt16(); _generalPurposeBitFlag = (GeneralPurposeBitFlag)br.ReadUInt16(); // If the flag says there's no hash data, then we can't use quick mode if ((_generalPurposeBitFlag & GeneralPurposeBitFlag.ZeroedCRCAndSize) == GeneralPurposeBitFlag.ZeroedCRCAndSize) { return(ZipReturn.ZipCannotFastOpen); } _compressionMethod = (CompressionMethod)br.ReadUInt16(); _lastMod = br.ReadUInt32(); _crc = br.ReadUInt32(); _compressedSize = br.ReadUInt32(); _uncompressedSize = br.ReadUInt32(); ushort fileNameLength = br.ReadUInt16(); ushort extraFieldLength = br.ReadUInt16(); byte[] fileNameBytes = br.ReadBytes(fileNameLength); _fileName = ((_generalPurposeBitFlag & GeneralPurposeBitFlag.LanguageEncodingFlag) == 0 ? Encoding.GetEncoding(858).GetString(fileNameBytes) : Encoding.UTF8.GetString(fileNameBytes, 0, fileNameLength)); byte[] extraField = br.ReadBytes(extraFieldLength); /* * Full disclosure: this next section is in GordonJ's work but I honestly * have no idea everything that it does. It seems to do something to figure * out if it's Zip64, or possibly check for random things but it uses the * extra field for this, which I do not fully understand. It's copied in * its entirety below in the hope that it makes things better... */ _zip64 = false; int pos = 0; while (extraFieldLength > pos) { ushort type = BitConverter.ToUInt16(extraField, pos); pos += 2; ushort blockLength = BitConverter.ToUInt16(extraField, pos); pos += 2; switch (type) { case 0x0001: Zip64 = true; if (_uncompressedSize == 0xffffffff) { _uncompressedSize = BitConverter.ToUInt64(extraField, pos); pos += 8; } if (_compressedSize == 0xffffffff) { _compressedSize = BitConverter.ToUInt64(extraField, pos); pos += 8; } break; case 0x7075: pos += 1; uint nameCRC32 = BitConverter.ToUInt32(extraField, pos); pos += 4; CRC32 crcTest = new CRC32(); crcTest.SlurpBlock(fileNameBytes, 0, fileNameLength); uint fCRC = (uint)crcTest.Crc32Result; if (nameCRC32 != fCRC) { return(ZipReturn.ZipLocalFileHeaderError); } int charLen = blockLength - 5; FileName = Encoding.UTF8.GetString(extraField, pos, charLen); pos += charLen; break; default: pos += blockLength; break; } } // Set the position of the data _dataLocation = (ulong)_zipstream.Position; } catch { return(ZipReturn.ZipLocalFileHeaderError); } return(ZipReturn.ZipGood); }
/// <summary> /// Write the central directory entry from the included stream /// </summary> /// <param name="output">Write out the data from the internal stream to the output stream</param> public void WriteCentralDirectory(Stream output) { // Open the output stream for writing BinaryWriter bw = new BinaryWriter(output); // Create an empty extra field to start out with List <byte> extraField = new List <byte>(); // Now get the uncompressed size (for Zip64 compatibility) uint uncompressedSize32; if (_uncompressedSize >= 0xffffffff) { _zip64 = true; uncompressedSize32 = 0xffffffff; extraField.AddRange(BitConverter.GetBytes(_uncompressedSize)); } else { uncompressedSize32 = (uint)_uncompressedSize; } // Now get the compressed size (for Zip64 compatibility) uint compressedSize32; if (_compressedSize >= 0xffffffff) { _zip64 = true; compressedSize32 = 0xffffffff; extraField.AddRange(BitConverter.GetBytes(_compressedSize)); } else { compressedSize32 = (uint)_compressedSize; } // Now get the relative offset (for Zip64 compatibility) uint relativeOffset32; if (_relativeOffset >= 0xffffffff) { _zip64 = true; relativeOffset32 = 0xffffffff; extraField.AddRange(BitConverter.GetBytes(_relativeOffset)); } else { relativeOffset32 = (uint)_relativeOffset; } // If we wrote anything to the extra field, set the flag and size if (extraField.Count > 0) { ushort extraFieldLengthInternal = (ushort)extraField.Count; extraField.InsertRange(0, BitConverter.GetBytes((ushort)0x0001)); // id extraField.InsertRange(2, BitConverter.GetBytes(extraFieldLengthInternal)); // data length } ushort extraFieldLength = (ushort)extraField.Count; // Now check for a unicode filename and set the flag accordingly byte[] fileNameBytes; if (Utilities.IsUnicode(_fileName)) { _generalPurposeBitFlag |= GeneralPurposeBitFlag.LanguageEncodingFlag; fileNameBytes = Encoding.UTF8.GetBytes(_fileName); } else { fileNameBytes = Encoding.GetEncoding(858).GetBytes(_fileName); } ushort fileNameLength = (ushort)fileNameBytes.Length; // Set the version needed to extract according to if it's Zip64 ushort versionNeededToExtract = (ushort)(_zip64 ? ArchiveVersion.TorrentZip64 : ArchiveVersion.TorrentZip); // Now, write all of the data to the stream bw.Write(Constants.CentralDirectoryHeaderSignature); bw.Write((ushort)ArchiveVersion.MSDOSandOS2); bw.Write(versionNeededToExtract); bw.Write((ushort)_generalPurposeBitFlag); bw.Write((ushort)_compressionMethod); bw.Write(_lastMod); bw.Write(_crc); bw.Write(compressedSize32); bw.Write(uncompressedSize32); bw.Write(fileNameLength); bw.Write(extraFieldLength); bw.Write((ushort)0); // File comment length bw.Write((ushort)0); // Disk number start bw.Write((ushort)0); // Internal file attributes bw.Write((uint)0); // External file attributes bw.Write(relativeOffset32); bw.Write(fileNameBytes, 0, fileNameLength); // Only write first bytes if longer than allowed bw.Write(extraField.ToArray(), 0, extraFieldLength); // Only write the first bytes if longer than allowed // We have no file comment, so we don't have to write more }
/// <summary> /// Read the central directory entry from the input stream /// </summary> /// <returns>Status of the underlying stream</returns> public ZipReturn ReadCentralDirectory() { try { // Open the stream for reading BinaryReader br = new BinaryReader(_zipstream); // If the first bytes aren't a central directory header, log and return if (br.ReadUInt32() != Constants.CentralDirectoryHeaderSignature) { return(ZipReturn.ZipCentralDirError); } // Now read in available information, skipping the unnecessary _versionMadeBy = (ArchiveVersion)br.ReadUInt16(); _versionNeeded = (ArchiveVersion)br.ReadUInt16(); _generalPurposeBitFlag = (GeneralPurposeBitFlag)br.ReadUInt16(); _compressionMethod = (CompressionMethod)br.ReadUInt16(); // If we have an unsupported compression method, log and return if (_compressionMethod != CompressionMethod.Stored && _compressionMethod != CompressionMethod.Deflated) { return(ZipReturn.ZipCentralDirError); } // Keep reading available information, skipping the unnecessary _lastMod = br.ReadUInt32(); _crc = br.ReadUInt32(); _compressedSize = br.ReadUInt32(); _uncompressedSize = br.ReadUInt32(); // Now store some temp vars to find the filename, extra field, and comment ushort fileNameLength = br.ReadUInt16(); ushort extraFieldLength = br.ReadUInt16(); ushort fileCommentLength = br.ReadUInt16(); // Even more reading available information, skipping the unnecessary br.ReadUInt16(); // Disk number start _internalFileAttributes = (InternalFileAttributes)br.ReadUInt16(); _externalFileAttributes = br.ReadUInt32(); _relativeOffset = br.ReadUInt32(); byte[] fileNameBytes = br.ReadBytes(fileNameLength); _fileName = ((_generalPurposeBitFlag & GeneralPurposeBitFlag.LanguageEncodingFlag) == 0 ? Encoding.GetEncoding(858).GetString(fileNameBytes) : Encoding.UTF8.GetString(fileNameBytes, 0, fileNameLength)); _extraField = br.ReadBytes(extraFieldLength); _comment = br.ReadBytes(fileCommentLength); /* * Full disclosure: this next section is in GordonJ's work but I honestly * have no idea everything that it does. It seems to do something to figure * out if it's Zip64, or possibly check for random things but it uses the * extra field for this, which I do not fully understand. It's copied in * its entirety below in the hope that it makes things better... */ int pos = 0; while (extraFieldLength > pos) { ushort type = BitConverter.ToUInt16(_extraField, pos); pos += 2; ushort blockLength = BitConverter.ToUInt16(_extraField, pos); pos += 2; switch (type) { case 0x0001: Zip64 = true; if (UncompressedSize == 0xffffffff) { UncompressedSize = BitConverter.ToUInt64(_extraField, pos); pos += 8; } if (_compressedSize == 0xffffffff) { _compressedSize = BitConverter.ToUInt64(_extraField, pos); pos += 8; } if (_relativeOffset == 0xffffffff) { _relativeOffset = BitConverter.ToUInt64(_extraField, pos); pos += 8; } break; case 0x7075: //byte version = extraField[pos]; pos += 1; uint nameCRC32 = BitConverter.ToUInt32(_extraField, pos); pos += 4; CRC32 crcTest = new CRC32(); crcTest.SlurpBlock(fileNameBytes, 0, fileNameLength); uint fCRC = (uint)crcTest.Crc32Result; if (nameCRC32 != fCRC) { return(ZipReturn.ZipCentralDirError); } int charLen = blockLength - 5; _fileName = Encoding.UTF8.GetString(_extraField, pos, charLen); pos += charLen; break; default: pos += blockLength; break; } } } catch { return(ZipReturn.ZipCentralDirError); } return(ZipReturn.ZipGood); }