/*
# zPak and zPakr
Copyright 2025 [XWolfOverride](https://github.com/XWolfOverride)
## Smaol permission notice:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- Any modifications of the software will maintain the copyright notice of the previous modifiers and optionally add a new copyright notice below oredred by date.
- The above copyright notice chain and this permission notice shall be accessible by final user, on software start or in a designed screen, dialog or option dedicated to license information.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**
*/
using System.Text;
namespace XWolf.Z
{
public class zPak
{
private const string ZPAK_HEADER = "zPak!\u001a";
private const string ZPAK_FOOTER = "/zPak";
private class zPackEntry
{
public string Name { get; set; } = "";
public long EntryPosition { get; set; } = 0;
public long Position { get; set; } = 0;
public long Length { get; set; } = 0;
}
private readonly List<zPackEntry> entries = [];
private string? fileName;
private Stream? stream;
private long offset = -1;
private bool writting = false;
#region Constructors
public zPak()
{
}
public zPak(string name, bool autocreate = true)
{
Open(name, autocreate);
}
public zPak(Stream stream, bool autocreate = true)
{
Open(stream, autocreate);
}
#endregion
#region Stream / File access
public void Open(string name, bool autocreate = true)
{
fileName = name;
if (File.Exists(fileName))
OpenForRead(autocreate);
else
if (autocreate)
OpenForWrite();
else
throw new IOException("zPak does not exists.");
}
private void OpenForRead(bool autocreate)
{
if (fileName == null)
throw new Exception("Can't reopen stream");
Close();
writting = false;
var file = new FileStream(fileName, FileMode.Open, FileAccess.Read);
OpenStream(file, autocreate);
}
private void OpenForWrite()
{
if (fileName == null)
throw new Exception("Can't reopen stream");
Close();
writting = true;
var file = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OpenStream(file, true);
}
public void Open(Stream stream, bool autocreate = true)
{
fileName = null;
OpenStream(stream, autocreate);
}
private void OpenStream(Stream stream, bool autocreate)
{
this.stream = stream;
if (offset < 0)
{
ReadOffset(autocreate);
ReadDirectory();
}
}
public void Close()
{
if (stream == null)
return;
if (writting)
{
GetStream().Position = GetStream().Length;
WriteLong(offset);
WritePlainString(ZPAK_FOOTER);
}
if (stream != null)
{
stream.Close();
stream = null;
}
}
private Stream GetStream(bool forWrite = false)
{
if (stream == null)
throw new IOException("Pak not opened.");
if (forWrite && !stream.CanWrite)
OpenForWrite();
return stream;
}
#endregion
#region Stream I/O
private byte[] ReadBytes(int ctt)
{
byte[] buf = new byte[ctt];
if (GetStream().Read(buf, 0, ctt) != ctt)
throw new IOException("Unexpected end of file.");
return buf;
}
private int ReadInt()
{
return BitConverter.ToInt32(ReadBytes(4), 0);
}
private long ReadLong()
{
return BitConverter.ToInt64(ReadBytes(8), 0);
}
private string ReadPlainString(int len)
{
var strbuf = ReadBytes(len);
return Encoding.UTF8.GetString(strbuf, 0, strbuf.Length);
}
private string ReadString()
{
return ReadPlainString(ReadInt());
}
private void WriteBytes(byte[] bytes)
{
GetStream(true).Write(bytes);
}
private void WriteInt(int i)
{
WriteBytes(BitConverter.GetBytes(i));
}
private void WriteLong(long l)
{
WriteBytes(BitConverter.GetBytes(l));
}
private void WritePlainString(string str)
{
WriteBytes(Encoding.UTF8.GetBytes(str));
}
private void WriteString(string str)
{
WriteInt(str.Length);
WritePlainString(str);
}
#endregion
#region Pak header handling
private void ReadOffset(bool autocreate)
{
if (GetStream().Length > 0)
{
if (GetStream().Length > ZPAK_HEADER.Length + ZPAK_FOOTER.Length + 8)
{
GetStream().Position = GetStream().Length - ZPAK_FOOTER.Length;
if (ReadPlainString(8) == ZPAK_FOOTER)
{
GetStream().Position = GetStream().Length - (ZPAK_FOOTER.Length + 8);
offset = ReadLong();
}
else
PreparePakStub(autocreate);
}
else
PreparePakStub(autocreate);
}
else
PreparePakStub(autocreate);
}
private void PreparePakStub(bool autocreate)
{
if (!autocreate)
throw new IOException("Not a zPak.");
offset = GetStream().Position = GetStream().Length;
WritePlainString(ZPAK_HEADER);
}
private zPackEntry ReadEntry()
{
var entry = new zPackEntry
{
EntryPosition = ZPos,
Name = ReadString(),
Position = ZPos,
Length = ReadLong()
};
return entry;
}
private void ReadDirectory()
{
entries.Clear();
ZPos = 0;
if (ReadPlainString(ZPAK_HEADER.Length) != ZPAK_HEADER)
throw new Exception("Not a correct zPak file or corrupted");
var end = GetStream().Length - (ZPAK_FOOTER.Length + 8);
while (GetStream().Position < end)
{
var ent = ReadEntry();
GetStream().Seek(ent.Length, SeekOrigin.Current);
entries.Add(ent);
}
}
private long ZPos
{
get { return GetStream().Position - offset; }
set { GetStream().Position = value + offset; }
}
#endregion
}
}