// Simplified ZIP module - self-contained implementation
// Basic ZIP types
///|
pub enum Compression {
Stored
Deflate
Other(Int)
} derive(Show)
///|
pub typealias String as Fpath
///|
pub typealias Int as Mode
///|
pub typealias Int as Ptime
///|
pub struct File {
compression : Compression
start : Int
compressed_size : Int
compressed_bytes : String
decompressed_size : Int
decompressed_crc32 : Int64 // Using Int64 directly instead of @deflate.Crc32
version_made_by : Int
version_needed_to_extract : Int
gp_flags : Int
}
///|
pub enum MemberKind {
Dir
File(File)
}
///|
pub struct Member {
path : Fpath
mode : Mode
mtime : Ptime
kind : MemberKind
}
///|
pub struct Archive {
members : Map[String, Member]
}
// Constants
///|
pub let dos_epoch : Ptime = 315532800 // 1980-01-01 00:00:00 UTC
///|
pub let max_file_size : Int = 2147483647 // Use max int instead of 4GB
///|
pub let max_members : Int = 65535 // ZIP32 limit
///|
pub let max_path_length : Int = 65535
// Archive operations
///|
pub fn empty() -> Archive {
{ members: Map::new() }
}
///|
pub fn is_empty(archive : Archive) -> Bool {
archive.members.size() == 0
}
///|
pub fn member_count(archive : Archive) -> Int {
archive.members.size()
}
///|
pub fn add(mem : Member, archive : Archive) -> Archive {
let new_members = archive.members
new_members[mem.path] = mem
{ members: new_members }
}
///|
pub fn find(path : Fpath, archive : Archive) -> Member? {
archive.members.get(path)
}
///|
pub fn mem(path : Fpath, archive : Archive) -> Bool {
archive.members.contains(path)
}
///|
pub fn remove(path : Fpath, archive : Archive) -> Archive {
let new_members = archive.members
new_members.remove(path)
{ members: new_members }
}
// File operations
///|
pub fn stored_of_binary_string(s : String) -> File raise {
let len = s.length()
if len > max_file_size {
fail(
"File size exceeds maximum allowed (" +
max_file_size.to_string() +
" bytes)",
)
}
// Calculate actual CRC-32 checksum
let crc = @crc32.bytes(s.to_bytes())
{
compression: Compression::Stored,
start: 0,
compressed_size: len,
compressed_bytes: s,
decompressed_size: len,
decompressed_crc32: crc.0,
version_made_by: 0x314, // UNIX, PKZIP 2.0
version_needed_to_extract: 20, // PKZIP 2.0
gp_flags: 0x800, // UTF-8 filename
}
}
// Create stored file from bytes (more efficient for binary data)
///|
pub fn stored_of_bytes(data : Bytes) -> File raise {
let len = data.length()
if len > max_file_size {
fail(
"File size exceeds maximum allowed (" +
max_file_size.to_string() +
" bytes)",
)
}
// Convert to string for storage (required by current File struct)
let data_as_string = data.to_string()
// Calculate CRC-32 checksum on the string that will actually be stored
// This ensures consistency between storage and extraction
let crc = @crc32.bytes(data_as_string.to_bytes())
{
compression: Compression::Stored,
start: 0,
compressed_size: len,
compressed_bytes: data_as_string,
decompressed_size: len,
decompressed_crc32: crc.0,
version_made_by: 0x314, // UNIX, PKZIP 2.0
version_needed_to_extract: 20, // PKZIP 2.0
gp_flags: 0x800, // UTF-8 filename
}
}
// Create deflate-compressed file
///|
pub fn deflate_of_binary_string(
s : String,
level : @deflate.Level,
) -> @deflate.Result[File, String] {
let len = s.length()
if len > max_file_size {
return @deflate.err(
"File size exceeds maximum allowed (" +
max_file_size.to_string() +
" bytes)",
)
}
// Compress using deflate
let compressed_data = @deflate.deflate_of_bytes(s.to_bytes(), level)
// Calculate CRC-32 of original data
let crc = @crc32.bytes(s.to_bytes())
@deflate.ok({
compression: Compression::Deflate,
start: 0,
compressed_size: compressed_data.to_string().length(),
compressed_bytes: compressed_data.to_string(),
decompressed_size: len,
decompressed_crc32: crc.0,
version_made_by: 0x314, // UNIX, ZIP 2.0
version_needed_to_extract: 20, // ZIP 2.0
gp_flags: 0x800, // UTF-8 filename
})
}
// Create deflate-compressed file from bytes (more efficient for binary data)
///|
pub fn deflate_of_bytes(
data : Bytes,
level : @deflate.Level,
) -> @deflate.Result[File, String] {
let len = data.length()
if len > max_file_size {
return @deflate.err(
"File size exceeds maximum allowed (" +
max_file_size.to_string() +
" bytes)",
)
}
// Compress using deflate directly on bytes (more efficient)
let compressed_data = @deflate.deflate_of_bytes(data, level)
// Convert to string for storage (required by current File struct)
let compressed_as_string = compressed_data.to_string()
let data_as_string = data.to_string()
// Calculate CRC-32 on the string representation that will be stored/extracted
// This ensures consistency between storage and extraction
let crc = @crc32.bytes(data_as_string.to_bytes())
@deflate.ok({
compression: Compression::Deflate,
start: 0,
compressed_size: compressed_as_string.length(),
compressed_bytes: compressed_as_string,
decompressed_size: len,
decompressed_crc32: crc.0,
version_made_by: 0x314, // UNIX, ZIP 2.0
version_needed_to_extract: 20, // ZIP 2.0
gp_flags: 0x800, // UTF-8 filename
})
}
///|
pub fn file_compression(file : File) -> Compression {
file.compression
}
///|
pub fn file_compressed_size(file : File) -> Int {
file.compressed_size
}
///|
pub fn file_decompressed_size(file : File) -> Int {
file.decompressed_size
}
///|
pub fn file_compressed_bytes(file : File) -> String {
file.compressed_bytes
}
///|
pub fn file_can_extract(file : File) -> Bool {
match file.compression {
Compression::Stored => true
Compression::Deflate => true // Would need actual deflate implementation
_ => false
}
}
///|
pub fn file_to_binary_string(file : File) -> @deflate.Result[String, String] {
if not(file_can_extract(file)) {
return @deflate.err("Unsupported compression format")
}
match file.compression {
Compression::Stored => {
let data = file.compressed_bytes.substring(
start=file.start,
end=file.start + file.compressed_size,
)
@deflate.ok(data)
}
Compression::Deflate =>
// TODO: Deflate decompression needs proper implementation
// For now, stored compression demonstrates all the architectural benefits
@deflate.err(
"Deflate decompression not yet implemented - use stored compression",
)
_ => @deflate.err("Unsupported compression format")
}
}
// Extract file content as bytes (more efficient for binary data)
///|
pub fn file_to_bytes(file : File) -> Bytes raise {
if not(file_can_extract(file)) {
fail("Unsupported compression format")
}
match file.compression {
Compression::Stored => {
// Convert string to bytes first, then slice
let compressed_bytes = file.compressed_bytes.to_bytes()
let data_view = compressed_bytes[file.start:file.start +
file.compressed_size]
// Convert view back to bytes
data_view.to_bytes()
}
Compression::Deflate => {
// Use bytes-based decompression for better performance
let compressed_bytes = file.compressed_bytes.to_bytes()
@deflate.deflate_decompress_bytes(
compressed_bytes,
file.decompressed_size,
)
}
_ => fail("Unsupported compression format")
}
}
// Member operations
///|
pub fn member_make(
path : Fpath,
kind : MemberKind,
) -> @deflate.Result[Member, String] {
if path.length() > max_path_length {
return @deflate.err(
"Path length exceeds maximum allowed (" +
max_path_length.to_string() +
" bytes)",
)
}
let sanitized_path = path.replace(old="\\", new="/")
let final_path = match kind {
MemberKind::Dir =>
if sanitized_path.is_empty() {
"./"
} else if sanitized_path.strip_suffix("/") is Some(_) {
sanitized_path
} else {
sanitized_path + "/"
}
MemberKind::File(_) => sanitized_path
}
let default_mode = match kind {
MemberKind::Dir => 0o755
MemberKind::File(_) => 0o644
}
@deflate.ok({
path: final_path,
mtime: dos_epoch, // Default mtime
mode: default_mode,
kind,
})
}
///|
pub fn member_path(mem : Member) -> Fpath {
mem.path
}
///|
pub fn member_mode(mem : Member) -> Mode {
mem.mode
}
///|
pub fn member_mtime(mem : Member) -> Ptime {
mem.mtime
}
///|
pub fn member_kind(mem : Member) -> MemberKind {
mem.kind
}
// Archive encoding/decoding
///|
pub fn to_binary_string(archive : Archive) -> @deflate.Result[String, String] {
if member_count(archive) > max_members {
return @deflate.err(
"Archive has too many members (" +
member_count(archive).to_string() +
"), max is " +
max_members.to_string(),
)
}
if is_empty(archive) {
// Empty ZIP file: just EOCD record
let eocd = "PK\u{05}\u{06}" + String::make(18, '\u{00}')
@deflate.ok(eocd)
} else {
// Build complete ZIP file
build_zip_file(archive)
}
}
// Convert archive to bytes (more efficient for binary data)
///|
pub fn to_bytes(archive : Archive) -> @deflate.Result[Bytes, String] {
// Use the existing string-based implementation and convert to bytes
// In a full implementation, this would work directly with bytes throughout
match to_binary_string(archive) {
@deflate.Ok(zip_string) => @deflate.ok(zip_string.to_bytes())
@deflate.Err(error) => @deflate.err(error)
}
}
// Build a complete ZIP file with local headers, central directory, and EOCD
///|
fn build_zip_file(archive : Archive) -> @deflate.Result[String, String] {
let mut zip_data = ""
let mut central_directory = ""
let local_header_offsets : Array[Int] = []
let member_list : Array[Member] = []
// Collect all members
archive.members.each(fn(_, mem) { member_list.push(mem) })
// Write local file headers and data
for mem in member_list {
let local_header_offset = zip_data.length()
local_header_offsets.push(local_header_offset)
match write_local_file_header(mem) {
@deflate.Ok(local_header_data) => zip_data = zip_data + local_header_data
@deflate.Err(error) =>
return @deflate.err("Failed to write local header: " + error)
}
}
// Write central directory
let central_directory_offset = zip_data.length()
for i = 0; i < member_list.length(); i = i + 1 {
let mem = member_list[i]
let local_offset = local_header_offsets[i]
match write_central_directory_entry(mem, local_offset) {
@deflate.Ok(cd_entry) => central_directory = central_directory + cd_entry
@deflate.Err(error) =>
return @deflate.err("Failed to write central directory entry: " + error)
}
}
// Write end of central directory record
let eocd = write_eocd_record(
member_list.length(),
central_directory.length(),
central_directory_offset,
)
let complete_zip = zip_data + central_directory + eocd
@deflate.ok(complete_zip)
}
// Write local file header
///|
fn write_local_file_header(mem : Member) -> @deflate.Result[String, String] {
let path = mem.path
let path_bytes = path // Assuming UTF-8 encoding
match mem.kind {
MemberKind::Dir => {
// Directory entry
let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
let header = write_u32_le(local_file_header_signature) + // Signature
write_u16_le(20) + // Version needed
write_u16_le(0x800) + // GP flags (UTF-8)
write_u16_le(0) + // Compression method (stored)
write_u16_le(dos_time) + // Last mod time
write_u16_le(dos_date) + // Last mod date
write_u32_le(0) + // CRC-32
write_u32_le(0) + // Compressed size
write_u32_le(0) + // Uncompressed size
write_u16_le(path_bytes.length()) + // Filename length
write_u16_le(0) // Extra field length
@deflate.ok(header + path_bytes)
}
MemberKind::File(file) => {
// File entry
let compression_method = match file.compression {
Compression::Stored => 0
Compression::Deflate => 8
Compression::Other(comp_method) => comp_method
}
let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
let header = write_u32_le(local_file_header_signature) + // Signature
write_u16_le(file.version_needed_to_extract) + // Version needed
write_u16_le(file.gp_flags) + // GP flags
write_u16_le(compression_method) + // Compression method
write_u16_le(dos_time) + // Last mod time
write_u16_le(dos_date) + // Last mod date
write_u32_le(file.decompressed_crc32.to_int()) + // CRC-32
write_u32_le(file.compressed_size) + // Compressed size
write_u32_le(file.decompressed_size) + // Uncompressed size
write_u16_le(path_bytes.length()) + // Filename length
write_u16_le(0) // Extra field length
@deflate.ok(header + path_bytes + file.compressed_bytes)
}
}
}
// Write central directory entry
///|
fn write_central_directory_entry(
mem : Member,
local_header_offset : Int,
) -> @deflate.Result[String, String] {
let path = mem.path
let path_bytes = path
match mem.kind {
MemberKind::Dir => {
let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
let entry = write_u32_le(central_directory_signature) + // Signature
write_u16_le(0x314) + // Version made by (Unix, ZIP 2.0)
write_u16_le(20) + // Version needed
write_u16_le(0x800) + // GP flags (UTF-8)
write_u16_le(0) + // Compression method
write_u16_le(dos_time) + // Last mod time
write_u16_le(dos_date) + // Last mod date
write_u32_le(0) + // CRC-32
write_u32_le(0) + // Compressed size
write_u32_le(0) + // Uncompressed size
write_u16_le(path_bytes.length()) + // Filename length
write_u16_le(0) + // Extra field length
write_u16_le(0) + // File comment length
write_u16_le(0) + // Disk number start
write_u16_le(0) + // Internal file attrs
write_u32_le((mem.mode << 16) | 0x10) + // External file attrs (directory)
write_u32_le(local_header_offset) // Local header offset
@deflate.ok(entry + path_bytes)
}
MemberKind::File(file) => {
let compression_method = match file.compression {
Compression::Stored => 0
Compression::Deflate => 8
Compression::Other(comp_method) => comp_method
}
let (dos_time, dos_date) = unix_to_dos_datetime(mem.mtime)
let entry = write_u32_le(central_directory_signature) + // Signature
write_u16_le(file.version_made_by) + // Version made by
write_u16_le(file.version_needed_to_extract) + // Version needed
write_u16_le(file.gp_flags) + // GP flags
write_u16_le(compression_method) + // Compression method
write_u16_le(dos_time) + // Last mod time
write_u16_le(dos_date) + // Last mod date
write_u32_le(file.decompressed_crc32.to_int()) + // CRC-32
write_u32_le(file.compressed_size) + // Compressed size
write_u32_le(file.decompressed_size) + // Uncompressed size
write_u16_le(path_bytes.length()) + // Filename length
write_u16_le(0) + // Extra field length
write_u16_le(0) + // File comment length
write_u16_le(0) + // Disk number start
write_u16_le(0) + // Internal file attrs
write_u32_le(mem.mode << 16) + // External file attrs
write_u32_le(local_header_offset) // Local header offset
@deflate.ok(entry + path_bytes)
}
}
}
// Write end of central directory record
///|
fn write_eocd_record(
entry_count : Int,
cd_size : Int,
cd_offset : Int,
) -> String {
write_u32_le(end_of_central_directory_signature) + // Signature
write_u16_le(0) + // Disk number
write_u16_le(0) + // Disk with central directory
write_u16_le(entry_count) + // Entries on this disk
write_u16_le(entry_count) + // Total entries
write_u32_le(cd_size) + // Central directory size
write_u32_le(cd_offset) + // Central directory offset
write_u16_le(0) // Comment length
}
// Binary parsing utilities
///|
fn read_u16_le(data : String, offset : Int) -> Int {
if offset + 1 >= data.length() {
return 0
}
let b0 = data[offset]
let b1 = data[offset + 1]
b0 + (b1 << 8)
}
///|
fn read_u32_le(data : String, offset : Int) -> Int {
if offset + 3 >= data.length() {
return 0
}
let b0 = data[offset]
let b1 = data[offset + 1]
let b2 = data[offset + 2]
let b3 = data[offset + 3]
b0 + (b1 << 8) + (b2 << 16) + (b3 << 24)
}
///|
fn write_u16_le(value : Int) -> String {
let b0 = value & 0xff
let b1 = (value >> 8) & 0xff
b0.unsafe_to_char().to_string() + b1.unsafe_to_char().to_string()
}
///|
fn write_u32_le(value : Int) -> String {
let b0 = value & 0xff
let b1 = (value >> 8) & 0xff
let b2 = (value >> 16) & 0xff
let b3 = (value >> 24) & 0xff
b0.unsafe_to_char().to_string() +
b1.unsafe_to_char().to_string() +
b2.unsafe_to_char().to_string() +
b3.unsafe_to_char().to_string()
}
// ZIP file format constants
///|
let local_file_header_signature : Int = 0x04034b50
///|
let central_directory_signature : Int = 0x02014b50
///|
let end_of_central_directory_signature : Int = 0x06054b50
// Check if string has ZIP magic bytes
///|
pub fn string_has_magic(s : String) -> Bool {
if s.length() < 4 {
return false
}
// Check for local file header signature (PK\u0003\u0004)
if s[0] == 0x50 && s[1] == 0x4b && s[2] == 0x03 && s[3] == 0x04 {
return true
}
// Check for end of central directory signature (PK\u0005\u0006) - empty archive
if s[0] == 0x50 && s[1] == 0x4b && s[2] == 0x05 && s[3] == 0x06 {
return true
}
false
}
// Find end of central directory record
///|
fn find_eocd(data : String) -> Int? {
let len = data.length()
if len < 22 {
return None
}
// Search backwards from end of file
for i = len - 22; i >= 0; i = i - 1 {
if i + 3 < len {
let sig = read_u32_le(data, i)
if sig == end_of_central_directory_signature {
return Some(i)
}
}
}
None
}
// Parse ZIP file
///|
pub fn of_binary_string(data : String) -> @deflate.Result[Archive, String] {
if data.length() < 22 {
return @deflate.err("File too small to be a valid ZIP archive")
}
// Check for ZIP magic bytes
if not(string_has_magic(data)) {
return @deflate.err("Not a valid ZIP archive - missing magic bytes")
}
// Find end of central directory record
match find_eocd(data) {
None => @deflate.err("Could not find end of central directory record")
Some(eocd_offset) => {
// Parse EOCD record
let cd_entries = read_u16_le(data, eocd_offset + 10) // Total entries in central directory
let _cd_size = read_u32_le(data, eocd_offset + 12) // Size of central directory (unused for now)
let cd_offset = read_u32_le(data, eocd_offset + 16) // Offset of central directory
if cd_entries == 0 {
// Empty archive
@deflate.ok(empty())
} else {
// Parse central directory entries
parse_central_directory(data, cd_offset, cd_entries)
}
}
}
}
// Parse archive from bytes (more efficient for binary data)
///|
pub fn of_bytes(data : Bytes) -> @deflate.Result[Archive, String] {
// Use the existing string-based implementation by converting bytes to string
// In a full implementation, this would work directly with bytes throughout
let string_data = data.to_string()
of_binary_string(string_data)
}
// Parse central directory entries
///|
fn parse_central_directory(
data : String,
cd_offset : Int,
entry_count : Int,
) -> @deflate.Result[Archive, String] {
let mut archive = empty()
let mut offset = cd_offset
for i = 0; i < entry_count; i = i + 1 {
match parse_central_directory_entry(data, offset) {
@deflate.Ok((mem, next_offset)) => {
archive = add(mem, archive)
offset = next_offset
}
@deflate.Err(error) =>
return @deflate.err(
"Failed to parse central directory entry " +
i.to_string() +
": " +
error,
)
}
}
@deflate.ok(archive)
}
// Parse a single central directory entry
///|
fn parse_central_directory_entry(
data : String,
offset : Int,
) -> @deflate.Result[(Member, Int), String] {
if offset + 46 > data.length() {
return @deflate.err("Central directory entry extends beyond file")
}
let signature = read_u32_le(data, offset)
if signature != central_directory_signature {
return @deflate.err("Invalid central directory entry signature")
}
let version_made_by = read_u16_le(data, offset + 4)
let version_needed = read_u16_le(data, offset + 6)
let gp_flags = read_u16_le(data, offset + 8)
let compression_method = read_u16_le(data, offset + 10)
let _last_mod_time = read_u16_le(data, offset + 12) // TODO: Convert DOS time to Unix time
let _last_mod_date = read_u16_le(data, offset + 14) // TODO: Convert DOS date to Unix time
let crc32 = read_u32_le(data, offset + 16)
let compressed_size = read_u32_le(data, offset + 20)
let uncompressed_size = read_u32_le(data, offset + 24)
let filename_length = read_u16_le(data, offset + 28)
let extra_field_length = read_u16_le(data, offset + 30)
let file_comment_length = read_u16_le(data, offset + 32)
let external_file_attrs = read_u32_le(data, offset + 38)
let local_header_offset = read_u32_le(data, offset + 42)
// Read filename
let filename_start = offset + 46
if filename_start + filename_length > data.length() {
return @deflate.err("Filename extends beyond file")
}
let filename = data.substring(
start=filename_start,
end=filename_start + filename_length,
)
// Determine compression type
let compression = match compression_method {
0 => Compression::Stored
8 => Compression::Deflate
_ => Compression::Other(compression_method)
}
// Read file data from local header
match read_file_data(data, local_header_offset, compressed_size) {
@deflate.Ok(file_data) => {
// Create file object
let file = {
compression,
start: 0,
compressed_size,
compressed_bytes: file_data,
decompressed_size: uncompressed_size,
decompressed_crc32: crc32.to_int64(),
version_made_by,
version_needed_to_extract: version_needed,
gp_flags,
}
// Determine if it's a directory
let is_directory = filename.strip_suffix("/") is Some(_) ||
(external_file_attrs & 0x10) != 0
let kind = if is_directory {
MemberKind::Dir
} else {
MemberKind::File(file)
}
// Create member
let mem = {
path: filename,
mode: if is_directory {
0o755
} else {
0o644
},
mtime: dos_datetime_to_unix(_last_mod_time, _last_mod_date),
kind,
}
let next_offset = offset +
46 +
filename_length +
extra_field_length +
file_comment_length
@deflate.ok((mem, next_offset))
}
@deflate.Err(error) => @deflate.err("Failed to read file data: " + error)
}
}
// Read file data from local header
///|
fn read_file_data(
data : String,
local_header_offset : Int,
compressed_size : Int,
) -> @deflate.Result[String, String] {
if local_header_offset + 30 > data.length() {
return @deflate.err("Local header extends beyond file")
}
let signature = read_u32_le(data, local_header_offset)
if signature != local_file_header_signature {
return @deflate.err("Invalid local file header signature")
}
let filename_length = read_u16_le(data, local_header_offset + 26)
let extra_field_length = read_u16_le(data, local_header_offset + 28)
let file_data_offset = local_header_offset +
30 +
filename_length +
extra_field_length
if file_data_offset + compressed_size > data.length() {
return @deflate.err("File data extends beyond archive")
}
let file_data = data.substring(
start=file_data_offset,
end=file_data_offset + compressed_size,
)
@deflate.ok(file_data)
}