You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SCJMapper-V2/SC/p4kFile/p4kFileHeader.cs

325 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using ZstdNet;
namespace SCJMapper_V2.p4kFile
{
/// <summary>
/// Represents a Fileheader entry in the p4k File
/// seems to be a Zip64 based file and therefore using those headers
/// </summary>
internal class p4kFileHeader : IDisposable
{
//4.3.7 Local file header:
// local file header signature 4 bytes(0x04034b50)
// version needed to extract 2 bytes
// general purpose bit flag 2 bytes
// compression method 2 bytes
// last mod file time 2 bytes
// last mod file date 2 bytes
// crc-32 4 bytes
// compressed size 4 bytes
// uncompressed size 4 bytes
// file name length 2 bytes
// extra field length 2 bytes
// file name( variable size )
// extra field( variable size )
//4.3.8 File data
// Immediately following the local header for a file
// SHOULD be placed the compressed or stored data for the file.
// If the file is encrypted, the encryption header for the file
// SHOULD be placed after the local header and before the file
// data. The series of[local file header][encryption header]
// [file data][data descriptor] repeats for each file in the
// .ZIP archive.
// Zero-byte files, directories, and other file types that
// contain no content MUST not include file data.
// 4.5.3 -Zip64 Extended Information Extra Field(0x0001):
// The following is the layout of the zip64 extended
// information "extra" block.If one of the size or
// offset fields in the Local or Central directory
// record is too small to hold the required data,
// a Zip64 extended information record is created.
// The order of the fields in the zip64 extended
// information record is fixed, but the fields MUST
// only appear if the corresponding Local or Central
// directory record field is set to 0xFFFF or 0xFFFFFFFF.
// Note: all fields stored in Intel low - byte / high - byte order.
// Value Size Description
// ---- - ---------------
//(ZIP64)0x0001 2 bytes Tag for this "extra" block type
// Size 2 bytes Size of this "extra" block
// Original
// Size 8 bytes Original uncompressed file size
// Compressed
// Size 8 bytes Size of compressed data
// Relative Header
// Offset 8 bytes Offset of local header record
// Disk Start
// Number 4 bytes Number of the disk on which
// this file starts
// This entry in the Local header MUST include BOTH original
// and compressed file size fields.If encrypting the
// central directory and bit 13 of the general purpose bit
// flag is set indicating masking, the value stored in the
// Local Header for the original file size will be zero.
/// <summary>
/// ctor: Create class from data returned by the Reader
/// </summary>
/// <param name="reader">A binary data reader for this type of data</param>
public p4kFileHeader( p4kRecReader reader )
{
// sanity check only
System.Diagnostics.Trace.Assert( Marshal.SizeOf( typeof( MyRecord ) ) == RecordLength,
"Record size does not match!(" + Marshal.SizeOf( typeof( MyRecord ) ).ToString( ) + ")" );
System.Diagnostics.Trace.Assert( Marshal.SizeOf( typeof( MyZ64ExtraRecord ) ) == Z64ExtraRecordLength,
"Extra Record size does not match!(" + Marshal.SizeOf( typeof( MyZ64ExtraRecord ) ).ToString( ) + ")" );
if ( reader.IsOpen( ) ) {
try {
long cPos = reader.Position;
do {
// Fileheaders are Page aligned - scan to find one
reader.AdvancePage( ); // to next page
cPos = reader.Position;
string cPosS = cPos.ToString( "X" );
m_recordOffset = cPos;
m_item = p4kRecReader.ByteToType<MyRecord>( reader.TheReader );
m_itemValid = m_item.ID.SequenceEqual( p4kSignatures.LocalFileHeaderCry );
} while ( ( cPos < reader.Length ) && !m_itemValid );
// get some file attributes
if ( m_itemValid ) {
if ( m_item.FilenameLength > 0 ) {
ReadFilename( reader );
}
if ( m_item.ExtraFieldLength > 0 ) {
ReadExtradata( reader ); // Likely Zip64 extensions
}
m_fileDateTime = p4kFileTStamp.FromDos( m_item.LastModDate, m_item.LastModTime );
// check if standard fields or extension is used for size information
if ( m_item.CompressedSize < 0xffffffff ) {
m_fileSizeComp = m_item.CompressedSize;
m_fileSizeUnComp = m_item.UncompressedSize;
}
else {
m_fileSizeComp = (long)m_z64Item.CompressedSize;
m_fileSizeUnComp = (long)m_z64Item.UncompressedSize;
}
// now we would be able to read the file content
// but we skip it for now to process the next header
m_fileOffset = reader.TheReader.BaseStream.Position; // save position of this item
reader.TheReader.BaseStream.Seek( m_fileSizeComp, SeekOrigin.Current );
}
else {
// actually invalid but good manner ..
m_recordOffset = -1;
m_fileOffset = -1;
m_fileSizeComp = 0;
m_fileSizeUnComp = 0;
}
}
catch {
m_itemValid = false;
}
finally {
if ( !m_itemValid ) {
if ( m_item.ID.SequenceEqual( p4kSignatures.CentralDirRecord ) ) {
// read beyond the file entries
throw new OperationCanceledException( string.Format( "EOF - found Central Directory header {0}", m_item.ID.ToString( ) ) );
}
else {
// other error
throw new NotSupportedException( string.Format( "Cannot process fileheader ID {0}", m_item.ID.ToString( ) ) );
}
}
}
}
}
/// <summary>
/// Return the file related to this entry
/// </summary>
/// <param name="reader">An open p4k File reader</param>
/// <returns>The content of the file or an empty string</returns>
public byte[] GetFile( p4kRecReader reader )
{
if ( !m_itemValid ) return new byte[] { }; // ERROR cannot..
reader.Seek( m_fileOffset );
// ?? big files may have trouble here - may be we need to read and write chunks for this
// but for now we only want to get XMLs out and that is OK with byte alloc on the heap
byte[] fileBytes = new byte[m_fileSizeComp];
fileBytes = reader.ReadBytes( fileBytes.Length );
byte[] decompFile = null;
if ( m_item.CompressionMethod == 0x64 ) {
// this indicates p4k ZStd compression
using ( var decompressor = new Decompressor( ) ) {
try {
decompFile = decompressor.Unwrap( fileBytes );
return decompFile;
}
catch ( ZstdException e ) {
Console.WriteLine( "ZStd - Cannot decode file: " + m_filename );
Console.WriteLine( "Error: " + e.Message );
//Console.ReadLine();
return new byte[] { };
}
}
}
else {
// plain write - might be wrong if another compression was applied..
decompFile = fileBytes;
return decompFile;
}
}
public bool IsValid { get => m_itemValid; }
public string Filename { get => m_filename; }
public long RecordOffset { get => m_recordOffset; }
public long FileOffset { get => m_fileOffset; }
public long FileSizeComp { get => m_fileSizeComp; }
public long FileSizeUnComp { get => m_fileSizeUnComp; }
public DateTime FileModifyDate { get => m_fileDateTime; }
private void ReadFilename( p4kRecReader reader )
{
byte[] fileNameBytes = new byte[m_item.FilenameLength];
fileNameBytes = reader.ReadBytes( m_item.FilenameLength );
m_filename = Encoding.ASCII.GetString( fileNameBytes );
}
private void ReadExtradata( p4kRecReader reader )
{
// first the Zip64 extra record
m_z64Item = p4kRecReader.ByteToType<MyZ64ExtraRecord>( reader.TheReader );
// then the rest of the extra record (is another item with tag 0x0666 and the rest lenght (ignored)
m_extraBytes = new byte[m_item.ExtraFieldLength - Z64ExtraRecordLength];
m_extraBytes = reader.ReadBytes( m_extraBytes.Length );
m_extraBytes = null; // dump it ...
// now we would be able to read the file content
}
private MyRecord m_item;
private bool m_itemValid = false;
private MyZ64ExtraRecord m_z64Item;
private byte[] m_extraBytes = null;
private long m_recordOffset = -1; // offset from start (current position)
private long m_fileOffset = 0; // offset from start (current position)
// Entry attributes
private string m_filename = "";
private DateTime m_fileDateTime = new DateTime( 1970, 1, 1 );
private long m_fileSizeComp = 0;
private long m_fileSizeUnComp = 0;
private const int RecordLength = 30;
[StructLayout( LayoutKind.Sequential, Pack = 1 )] // , Size = RecordLength
private struct MyRecord
{
[MarshalAs( UnmanagedType.ByValArray, SizeConst = 4 )]
public byte[] ID; // local file header signature 4 bytes(0x14034b50) NOTE p4k here uses 0x14 and not 0x04
[MarshalAs( UnmanagedType.U2 )]
public UInt16 ExtractVersion; // version made by 2 bytes
[MarshalAs( UnmanagedType.U2 )]
public UInt16 BitFlags; // general purpose bit flag 2 bytes
[MarshalAs( UnmanagedType.U2 )]
public UInt16 CompressionMethod; // compression method 2 bytes
[MarshalAs( UnmanagedType.U2 )]
public UInt16 LastModTime; // last mod file time 2 bytes
[MarshalAs( UnmanagedType.U2 )]
public UInt16 LastModDate; // last mod file date 2 bytes
[MarshalAs( UnmanagedType.U4 )]
public UInt32 CRC32; // crc-32 4 bytes
[MarshalAs( UnmanagedType.U4 )]
public UInt32 CompressedSize; // compressed size 4 bytes
[MarshalAs( UnmanagedType.U4 )]
public UInt32 UncompressedSize; // uncompressed size 4 bytes
[MarshalAs( UnmanagedType.U2 )]
public UInt16 FilenameLength; // file name length 2 bytes
[MarshalAs( UnmanagedType.U2 )]
public UInt16 ExtraFieldLength; // extra field length 2 bytes
}
private const int Z64ExtraRecordLength = 32;
[StructLayout( LayoutKind.Sequential, Pack = 1 )] // , Size = RecordLength
private struct MyZ64ExtraRecord
{
[MarshalAs( UnmanagedType.U2 )]
public UInt16 ID; // (Zip64 ExtraHeader Signature)
[MarshalAs( UnmanagedType.U2 )]
public UInt16 Size; // Size 2 bytes Size of this "extra" block
[MarshalAs( UnmanagedType.U8 )]
public UInt64 UncompressedSize; // Original Size 8 bytes Original uncompressed file size
[MarshalAs( UnmanagedType.U8 )]
public UInt64 CompressedSize; // Compressed Size 8 bytes Size of compressed data
[MarshalAs( UnmanagedType.U8 )]
public UInt64 LocalHeaderOffset; // Relative Header Offset 8 bytes Offset of local header record
[MarshalAs( UnmanagedType.U4 )]
public UInt32 DiskStart; // Disk Start Number 4 bytes Number of the disk on which this file starts
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose( bool disposing )
{
if ( !disposedValue ) {
if ( disposing ) {
// TODO: dispose managed state (managed objects).
m_itemValid = false;
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
m_extraBytes = null;
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~p4kFileHeader() {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose( true );
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
}
}