mirror of
https://github.com/c3lang/c3c.git
synced 2026-02-27 20:11:17 +00:00
* stdlib: implement `std::compression::zip` and `std::compression::deflate` - C3 implementation of DEFLATE (RFC 1951) and ZIP archive handling. - Support for reading and writing archives using STORE and DEFLATE methods. - Decompression supports both fixed and dynamic Huffman blocks. - Compression using greedy LZ77 matching. - Zero dependencies on libc. - Stream-based entry reading and writing. - Full unit test coverage. NOTE: This is an initial implementation. Future improvements could be: - Optimization of the LZ77 matching (lazy matching). - Support for dynamic Huffman blocks in compression. - ZIP64 support for large files/archives. - Support for encryption and additional compression methods. * optimizations+refactoring deflate: - replace linear search with hash-based match finding. - implement support for dynamic Huffman blocks using the Package-Merge algorithm. - add streaming decompression. - add buffered StreamBitReader. zip: - add ZIP64 support. - add CP437 and UTF-8 filename encoding detection. - add DOS date/time conversion and timestamp preservation. - add ZipEntryReader for streaming entry reads. - implement ZipArchive.extract and ZipArchive.recover helpers. other: - Add `set_modified_time` to std::io; - Add benchmarks and a few more unit tests. * zip: add archive comment support add tests * forgot to rename the benchmark :( * detect utf8 names on weird zips fix method not passed to open_writer * another edge case where directory doesn't end with / * testing utilities - detect encrypted zip - `ZipArchive.open_writer` default to DEFLATE * fix zip64 creation, add tests * fix ZIP header endianness for big-endian compatibility Update ZipLFH, ZipCDH, ZipEOCD, Zip64EOCD, and Zip64Locator structs to use little-endian bitstruct types from std::core::bitorder * fix ZipEntryReader position tracking and seek logic ZIP_METHOD_STORE added a test to track this * add package-merge algorithm attribution Thanks @konimarti * standalone deflate_benchmark.c3 against `miniz` * fix integer overflows, leaks and improve safety * a few safety for 32-bit systems and tests * deflate compress optimization * improve match finding, hash updates, and buffer usage * use ulong for zip offsets * style changes (#18) * style changes * update tests * style changes in `deflate.c3` * fix typo * Allocator first. Some changes to deflate to use `copy_to` * Fix missing conversion on 32 bits. * Fix deflate stream. Formatting. Prefer switch over if-elseif * - Stream functions now use long/ulong rather than isz/usz for seek/available. - `instream.seek` is replaced by `set_cursor` and `cursor`. - `instream.available`, `cursor` etc are long/ulong rather than isz/usz to be correct on 32-bit. * Update to constdef * Fix test --------- Co-authored-by: Book-reader <thevoid@outlook.co.nz> Co-authored-by: Christoffer Lerno <christoffer@aegik.com>
1216 lines
30 KiB
Plaintext
1216 lines
30 KiB
Plaintext
<*
|
|
ZIP archive module (STORE and DEFLATE).
|
|
|
|
API:
|
|
- fn ZipArchive? open(Allocator allocator, String path, String mode = "r")
|
|
- fn ZipArchive? recover(Allocator allocator, String path, )
|
|
- fn void? ZipArchive.extract(&self, String output_dir)
|
|
- fn ZipEntry? ZipArchive.stat(&self, String filename)
|
|
- fn char[]? ZipArchive.read_file_all(&self, Allocator allocator, String filename)
|
|
- fn void? ZipArchive.close(&self)
|
|
*>
|
|
module std::compression::zip;
|
|
import std::io, std::collections::list, std::hash::crc32, std::time, std::math;
|
|
import std::encoding::codepage, std::compression::deflate;
|
|
import libc;
|
|
|
|
faultdef
|
|
INVALID_ARGUMENT,
|
|
IO_ERROR,
|
|
CORRUPTED_DATA,
|
|
ENTRY_NOT_FOUND,
|
|
ENCRYPTED_FILE;
|
|
|
|
<*
|
|
Describes a single entry within a ZIP archive.
|
|
*>
|
|
struct ZipEntry
|
|
{
|
|
String name;
|
|
ulong uncompressed_size;
|
|
ulong compressed_size;
|
|
bool is_directory;
|
|
bool is_encrypted;
|
|
uint crc32;
|
|
ulong offset;
|
|
ZipMethod method;
|
|
ushort last_mod_time;
|
|
ushort last_mod_date;
|
|
}
|
|
|
|
fn Time ZipEntry.time(&self) => dos_date_time_to_time(self.last_mod_date, self.last_mod_time);
|
|
|
|
alias ZipEntryList = List{ZipEntry};
|
|
|
|
struct ZipArchive
|
|
{
|
|
File* file;
|
|
Allocator allocator;
|
|
ZipEntryList entries;
|
|
String mode;
|
|
String path;
|
|
String comment;
|
|
}
|
|
|
|
constdef ZipMethod : UShortLE
|
|
{
|
|
STORE = {0},
|
|
DEFLATE = {8},
|
|
}
|
|
|
|
<*
|
|
Opens a ZIP archive.
|
|
@param allocator : `The allocator to use.`
|
|
@param path : `The path to the ZIP file.`
|
|
@param mode : `Opening mode ("r", "w", "w+").`
|
|
@return `The opened archive.`
|
|
@require mode == "r" || mode == "w" || mode == "w+"
|
|
*>
|
|
fn ZipArchive? open(Allocator allocator, String path, String mode = "r")
|
|
{
|
|
if (mode == "w" || mode == "w+")
|
|
{
|
|
File f = file::open(path, mode == "w+" ? "w+" : "w+b")!;
|
|
|
|
ZipArchive archive;
|
|
archive.allocator = allocator;
|
|
archive.file = allocator::new(allocator, File, f);
|
|
archive.entries.init(allocator);
|
|
archive.path = path.copy(allocator);
|
|
archive.mode = mode.copy(allocator);
|
|
return archive;
|
|
}
|
|
if (mode != "r") return INVALID_ARGUMENT~;
|
|
|
|
File f = file::open(path, "rb")!;
|
|
defer (catch err) (void)f.close();
|
|
|
|
ulong file_size = f.size()!!;
|
|
|
|
if (file_size < ZipEOCD.sizeof) return CORRUPTED_DATA~;
|
|
|
|
ulong search_start = file_size > (ulong)(ZipEOCD.sizeof + 65535)
|
|
? file_size - (ulong)(ZipEOCD.sizeof + 65535)
|
|
: 0;
|
|
|
|
ZipEOCD eocd;
|
|
bool found = false;
|
|
|
|
for (ulong pos = file_size - (ulong)ZipEOCD.sizeof; pos >= search_start; pos--)
|
|
{
|
|
if (pos > (ulong)isz.max) return io::OVERFLOW~;
|
|
f.set_cursor(pos)!;
|
|
UIntLE sig;
|
|
if (io::read_any(&f, &sig)! != 4) break;
|
|
|
|
if (sig.val == ZIP_EOCD_SIG)
|
|
{
|
|
f.set_cursor(pos)!;
|
|
if (io::read_any(&f, &eocd)! == ZipEOCD.sizeof)
|
|
{
|
|
ulong expected_end = pos + ZipEOCD.sizeof + eocd.comment_len.val;
|
|
if (expected_end == file_size)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (pos == 0) break;
|
|
}
|
|
|
|
if (!found) return CORRUPTED_DATA~;
|
|
|
|
ZipArchive archive;
|
|
archive.allocator = allocator;
|
|
archive.file = allocator::new(allocator, File, f);
|
|
archive.entries.init(allocator);
|
|
archive.path = path.copy(allocator);
|
|
archive.mode = mode.copy(allocator);
|
|
|
|
if (eocd.comment_len.val > 0)
|
|
{
|
|
char[] comment_data = allocator::alloc_array(allocator, char, (usz)eocd.comment_len.val);
|
|
defer allocator::free(allocator, comment_data);
|
|
if (archive.file.read(comment_data)! == (usz)eocd.comment_len.val)
|
|
{
|
|
archive.comment = codepage::decode(allocator, comment_data, CP437)!!;
|
|
}
|
|
}
|
|
|
|
defer catch (void)archive.close();
|
|
|
|
if (eocd.cd_offset.val > (uint)isz.max) return io::OVERFLOW~;
|
|
archive.file.set_cursor(eocd.cd_offset.val)!;
|
|
|
|
usz num_entries = eocd.num_entries.val;
|
|
|
|
|
|
// ZIP64 check
|
|
if (eocd.num_entries.val == 0xFFFF || eocd.cd_offset.val == 0xFFFFFFFF)
|
|
{
|
|
isz locator_pos = (isz)file_size - ZipEOCD.sizeof - Zip64Locator.sizeof;
|
|
if (locator_pos >= 0)
|
|
{
|
|
archive.file.set_cursor(locator_pos)!;
|
|
Zip64Locator locator;
|
|
if (try n = archive.file.read(((char*)&locator)[:Zip64Locator.sizeof]))
|
|
{
|
|
if (n == Zip64Locator.sizeof && locator.signature.val == ZIP64_LOCATOR_SIG)
|
|
{
|
|
if (locator.offset_eocd.val > (ulong)isz.max) return io::OVERFLOW~;
|
|
archive.file.set_cursor(locator.offset_eocd.val)!;
|
|
Zip64EOCD eocd64;
|
|
io::read_any(archive.file, &eocd64)!;
|
|
if (eocd64.signature.val == ZIP64_EOCD_SIG)
|
|
{
|
|
if (eocd64.offset_cd.val > (ulong)isz.max) return io::OVERFLOW~;
|
|
archive.file.set_cursor(eocd64.offset_cd.val)!!;
|
|
num_entries = (usz)eocd64.count_total.val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (usz i = 0; i < num_entries; i++)
|
|
{
|
|
ZipCDH cdh;
|
|
if (io::read_any(archive.file, &cdh)! != ZipCDH.sizeof) break;
|
|
if (cdh.signature.val != ZIP_CDH_SIG) break;
|
|
|
|
char[] raw_name = allocator::alloc_array(allocator, char, cdh.filename_len.val);
|
|
if (archive.file.read(raw_name)! != (usz)cdh.filename_len.val)
|
|
{
|
|
allocator::free(allocator, raw_name);
|
|
break;
|
|
}
|
|
|
|
String name;
|
|
bool is_utf8 = (cdh.flags.val & 0x0800) != 0;
|
|
if (is_utf8 || is_valid_utf8(raw_name))
|
|
{
|
|
name = (String)raw_name;
|
|
}
|
|
else
|
|
{
|
|
name = (String)codepage::decode(allocator, raw_name, CP437)!!;
|
|
allocator::free(allocator, raw_name.ptr);
|
|
}
|
|
|
|
char[] extra_field;
|
|
if (cdh.extra_field_len.val > 0)
|
|
{
|
|
extra_field = allocator::alloc_array(allocator, char, cdh.extra_field_len.val);
|
|
archive.file.read(extra_field)!;
|
|
}
|
|
|
|
archive.file.set_cursor(cdh.comment_len.val, FROM_CURSOR)!;
|
|
|
|
ulong uncompressed_size = cdh.uncompressed_size.val;
|
|
ulong compressed_size = cdh.compressed_size.val;
|
|
ulong offset = cdh.relative_offset.val;
|
|
|
|
if (cdh.uncompressed_size.val == 0xFFFFFFFF || cdh.compressed_size.val == 0xFFFFFFFF || cdh.relative_offset.val == 0xFFFFFFFF)
|
|
{
|
|
ByteReader reader = { .bytes = extra_field };
|
|
while (reader.available()! >= 4)
|
|
{
|
|
ushort id = io::read_le_ushort(&reader)!;
|
|
ushort size = io::read_le_ushort(&reader)!;
|
|
|
|
if (id == ZIP64_EXTRA_ID)
|
|
{
|
|
int remaining = size;
|
|
if (cdh.uncompressed_size.val == 0xFFFFFFFF && remaining >= 8)
|
|
{
|
|
uncompressed_size = io::read_le_ulong(&reader)!;
|
|
remaining -= 8;
|
|
}
|
|
if (cdh.compressed_size.val == 0xFFFFFFFF && remaining >= 8)
|
|
{
|
|
compressed_size = io::read_le_ulong(&reader)!;
|
|
remaining -= 8;
|
|
}
|
|
if (cdh.relative_offset.val == 0xFFFFFFFF && remaining >= 8)
|
|
{
|
|
offset = io::read_le_ulong(&reader)!;
|
|
remaining -= 8;
|
|
}
|
|
break;
|
|
}
|
|
reader.set_cursor(size, FROM_CURSOR)!;
|
|
}
|
|
}
|
|
if (extra_field.len > 0) allocator::free(allocator, extra_field);
|
|
|
|
bool is_directory = name.ends_with("/") || name.ends_with("\\");
|
|
if (!is_directory)
|
|
{
|
|
ushort host_system = cdh.version_made_by.val >> 8;
|
|
if (host_system == 0 || host_system == 10) // MS-DOS or NTFS
|
|
{
|
|
if ((cdh.external_attr.val & 0x10) != 0) is_directory = true;
|
|
}
|
|
else if (host_system == 3) // Unix
|
|
{
|
|
if (((cdh.external_attr.val >> 16) & 0x4000) != 0) is_directory = true;
|
|
}
|
|
}
|
|
|
|
ZipEntry entry = {
|
|
.name = name,
|
|
.uncompressed_size = uncompressed_size,
|
|
.compressed_size = compressed_size,
|
|
.crc32 = cdh.crc32.val,
|
|
.offset = offset,
|
|
.method = cdh.method,
|
|
.last_mod_time = cdh.last_mod_time.val,
|
|
.last_mod_date = cdh.last_mod_date.val,
|
|
.is_directory = is_directory,
|
|
.is_encrypted = (cdh.flags.val & 1) != 0
|
|
};
|
|
|
|
archive.entries.push(entry);
|
|
}
|
|
|
|
return archive;
|
|
}
|
|
|
|
<*
|
|
Recovers a ZIP archive by scanning for Local File Headers
|
|
when the Central Directory is missing or corrupted.
|
|
@param path : `The path to the broken ZIP file.`
|
|
@param allocator : `The allocator to use.`
|
|
@return `The recovered archive.`
|
|
*>
|
|
fn ZipArchive? recover(Allocator allocator, String path)
|
|
{
|
|
File f = file::open(path, "rb")!;
|
|
defer (catch err) (void)f.close();
|
|
|
|
ZipArchive archive = {
|
|
.allocator = allocator,
|
|
.file = allocator::new(allocator, File, f),
|
|
.path = path.copy(allocator),
|
|
.mode = "r".copy(allocator)
|
|
};
|
|
archive.entries.init(allocator);
|
|
defer (catch err) (void)archive.close();
|
|
|
|
char[4] sig_buf;
|
|
while (true)
|
|
{
|
|
long offset = archive.file.cursor()!!;
|
|
usz n = archive.file.read(sig_buf[..])!;
|
|
if (n < 4) break;
|
|
|
|
if (bitorder::read(sig_buf, UIntLE) != ZIP_LFH_SIG)
|
|
{
|
|
archive.file.set_cursor(-3, FROM_CURSOR)!!;
|
|
continue;
|
|
}
|
|
|
|
ZipLFH lfh;
|
|
if (archive.file.read(((char*)&lfh.version_needed)[:ZipLFH.sizeof - 4])! != ZipLFH.sizeof - 4) break;
|
|
lfh.signature.val = ZIP_LFH_SIG;
|
|
|
|
char[] raw_name = allocator::alloc_array(allocator, char, lfh.filename_len.val);
|
|
if (archive.file.read(raw_name)! != (usz)lfh.filename_len.val)
|
|
{
|
|
allocator::free(allocator, raw_name);
|
|
break;
|
|
}
|
|
|
|
String name;
|
|
if ((lfh.flags.val & 0x0800) != 0 || is_valid_utf8(raw_name))
|
|
{
|
|
name = (String)raw_name;
|
|
}
|
|
else
|
|
{
|
|
name = (String)codepage::decode(allocator, raw_name, CP437)!!;
|
|
allocator::free(allocator, raw_name.ptr);
|
|
}
|
|
|
|
archive.file.set_cursor(lfh.extra_field_len.val, FROM_CURSOR)!!;
|
|
|
|
ZipEntry entry = {
|
|
.name = name,
|
|
.compressed_size = lfh.compressed_size.val,
|
|
.uncompressed_size = lfh.uncompressed_size.val,
|
|
.crc32 = lfh.crc32.val,
|
|
.offset = offset,
|
|
.method = lfh.method,
|
|
.last_mod_time = lfh.last_mod_time.val,
|
|
.last_mod_date = lfh.last_mod_date.val,
|
|
.is_directory = name.ends_with("/") || name.ends_with("\\"),
|
|
.is_encrypted = (lfh.flags.val & 1) != 0
|
|
};
|
|
|
|
archive.entries.push(entry);
|
|
|
|
if (lfh.compressed_size.val > 0 && (ulong)lfh.compressed_size.val > (ulong)isz.max) return io::OVERFLOW~;
|
|
archive.file.set_cursor(lfh.compressed_size.val, FROM_CURSOR)!!;
|
|
}
|
|
|
|
if (archive.entries.len() == 0) return CORRUPTED_DATA~;
|
|
return archive;
|
|
}
|
|
|
|
<*
|
|
Closes the ZIP archive, writing the central directory if in write mode.
|
|
*>
|
|
fn void? ZipArchive.close(&self)
|
|
{
|
|
defer {
|
|
(void)self.file.close();
|
|
|
|
foreach (&entry : self.entries)
|
|
{
|
|
allocator::free(self.allocator, entry.name);
|
|
}
|
|
self.entries.free();
|
|
allocator::free(self.allocator, self.mode);
|
|
allocator::free(self.allocator, self.path);
|
|
if (self.comment.len > 0) allocator::free(self.allocator, self.comment);
|
|
if (self.file) allocator::free(self.allocator, self.file);
|
|
}
|
|
|
|
if (self.mode.starts_with("w"))
|
|
{
|
|
self.file.flush()!;
|
|
ulong cd_offset = self.file.cursor()!;
|
|
ulong cd_size = 0;
|
|
|
|
for (usz i = 0; i < self.entries.len(); i++)
|
|
{
|
|
ZipEntry* entry = self.entries.get_ref(i);
|
|
ZipCDH cdh = {
|
|
.signature.val = ZIP_CDH_SIG,
|
|
.version_made_by.val = 45, // 4.5 for ZIP64
|
|
.version_needed.val = 45,
|
|
.method = entry.method,
|
|
.last_mod_time.val = entry.last_mod_time,
|
|
.last_mod_date.val = entry.last_mod_date,
|
|
.crc32.val = entry.crc32,
|
|
.filename_len.val = (ushort)entry.name.len,
|
|
};
|
|
|
|
bool is_zip64 = entry.uncompressed_size >= 0xFFFFFFFF || entry.compressed_size >= 0xFFFFFFFF || entry.offset >= 0xFFFFFFFF;
|
|
|
|
char[] extra_data;
|
|
if (is_zip64)
|
|
{
|
|
cdh.compressed_size.val = 0xFFFFFFFF;
|
|
cdh.uncompressed_size.val = 0xFFFFFFFF;
|
|
cdh.relative_offset.val = 0xFFFFFFFF;
|
|
|
|
// Header(4) + Uncomp(8) + Comp(8) + Offset(8)
|
|
ushort extra_size = 28;
|
|
extra_data = allocator::alloc_array(self.allocator, char, extra_size);
|
|
bitorder::write(ZIP64_EXTRA_ID, extra_data[:2], UShortLE);
|
|
bitorder::write((ushort)(extra_size - 4), extra_data[2:2], UShortLE);
|
|
bitorder::write(entry.uncompressed_size, extra_data[4:8], ULongLE);
|
|
bitorder::write(entry.compressed_size, extra_data[12:8], ULongLE);
|
|
bitorder::write(entry.offset, extra_data[20:8], ULongLE);
|
|
|
|
cdh.extra_field_len.val = extra_size;
|
|
}
|
|
else
|
|
{
|
|
cdh.compressed_size.val = (uint)entry.compressed_size;
|
|
cdh.uncompressed_size.val = (uint)entry.uncompressed_size;
|
|
cdh.relative_offset.val = (uint)entry.offset;
|
|
}
|
|
|
|
// Set external attributes (MS-DOS compatibility).
|
|
// 0x10 is the DOS directory attribute.
|
|
cdh.external_attr.val = (uint)(entry.is_directory ? 0x10 : 0);
|
|
|
|
io::write_any(self.file, &cdh)!;
|
|
self.file.write(entry.name)!;
|
|
if (is_zip64)
|
|
{
|
|
self.file.write(extra_data)!;
|
|
allocator::free(self.allocator, extra_data);
|
|
}
|
|
|
|
ulong entry_record_size = (ulong)(ZipCDH.sizeof + entry.name.len + cdh.extra_field_len.val);
|
|
if (cd_size > (ulong.max - entry_record_size)) return io::OVERFLOW~;
|
|
cd_size += entry_record_size;
|
|
}
|
|
|
|
bool cd_zip64 = self.entries.len() >= 0xFFFF || cd_size >= 0xFFFFFFFF || cd_offset >= 0xFFFFFFFF;
|
|
|
|
if (cd_zip64)
|
|
{
|
|
ulong eocd64_offset = self.file.cursor()!;
|
|
|
|
Zip64EOCD eocd64 = {
|
|
.signature.val = ZIP64_EOCD_SIG,
|
|
.size.val = (ulong)(Zip64EOCD.sizeof - 12),
|
|
.version_made.val = 45,
|
|
.version_needed.val = 45,
|
|
.count_this_disk.val = (ulong)self.entries.len(),
|
|
.count_total.val = (ulong)self.entries.len(),
|
|
.size_cd.val = cd_size,
|
|
.offset_cd.val = cd_offset,
|
|
};
|
|
|
|
io::write_any(self.file, &eocd64)!;
|
|
|
|
Zip64Locator locator = {
|
|
.signature.val = ZIP64_LOCATOR_SIG,
|
|
.disk_start.val = 0,
|
|
.offset_eocd.val = eocd64_offset,
|
|
.total_disks.val = 1,
|
|
};
|
|
|
|
io::write_any(self.file, &locator)!;
|
|
}
|
|
|
|
char[] encoded_comment;
|
|
if (self.comment.len > 0)
|
|
{
|
|
char[]? res = codepage::encode(self.allocator, self.comment, CodePage.CP437);
|
|
if (try res)
|
|
{
|
|
encoded_comment = res;
|
|
if (encoded_comment.len > 0xFFFF)
|
|
{
|
|
allocator::free(self.allocator, encoded_comment.ptr);
|
|
return INVALID_ARGUMENT~;
|
|
}
|
|
}
|
|
}
|
|
defer if (encoded_comment.ptr) allocator::free(self.allocator, encoded_comment);
|
|
|
|
ZipEOCD eocd = {
|
|
.signature.val = ZIP_EOCD_SIG,
|
|
.num_entries_this_disk.val = (ushort)(self.entries.len() >= 0xFFFF ? 0xFFFF : (ushort)self.entries.len()),
|
|
.num_entries.val = (ushort)(self.entries.len() >= 0xFFFF ? 0xFFFF : (ushort)self.entries.len()),
|
|
.cd_size.val = (uint)(cd_size >= 0xFFFFFFFF ? 0xFFFFFFFF : (uint)cd_size),
|
|
.cd_offset.val = (uint)(cd_offset >= 0xFFFFFFFF ? 0xFFFFFFFF : (uint)cd_offset),
|
|
.comment_len.val = (ushort)encoded_comment.len,
|
|
};
|
|
|
|
io::write_any(self.file, &eocd)!;
|
|
|
|
if (encoded_comment.len > 0)
|
|
{
|
|
self.file.write(encoded_comment)!;
|
|
}
|
|
}
|
|
}
|
|
|
|
<*
|
|
Extracts the entire archive to the specified directory.
|
|
@param output_dir : `The directory to extract to.`
|
|
*>
|
|
fn void? ZipArchive.extract(&self, String output_dir) => @pool()
|
|
{
|
|
for (usz i = 0; i < self.count(); i++)
|
|
{
|
|
ZipEntry entry = self.stat_at(i) ?? ENTRY_NOT_FOUND~!;
|
|
String out_path_str;
|
|
if (try tmp = path::temp(output_dir))
|
|
{
|
|
if (try combined = tmp.tappend(entry.name))
|
|
{
|
|
out_path_str = combined.str_view();
|
|
}
|
|
else
|
|
{
|
|
return IO_ERROR~;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return IO_ERROR~;
|
|
}
|
|
|
|
if (entry.is_directory)
|
|
{
|
|
(void)path::mkdir(out_path_str, true);
|
|
}
|
|
else
|
|
{
|
|
if (try tmp = path::temp(out_path_str))
|
|
{
|
|
if (try parent = tmp.parent())
|
|
{
|
|
(void)path::mkdir(parent.str_view(), true);
|
|
}
|
|
}
|
|
|
|
ZipEntryReader reader = self.open_reader(entry.name)!;
|
|
defer (void)reader.close();
|
|
|
|
File f = file::open(out_path_str, "wb")!;
|
|
defer (void)f.close();
|
|
char[65536] buf;
|
|
while (true)
|
|
{
|
|
usz? res = reader.read(&buf);
|
|
if (catch excuse = res)
|
|
{
|
|
if (excuse == io::EOF) break;
|
|
return excuse~;
|
|
}
|
|
usz n = res;
|
|
if (n == 0) break;
|
|
f.write(buf[:n])!;
|
|
}
|
|
f.close()!;
|
|
|
|
file::set_modified_time(out_path_str, (Time_t)entry.time().to_seconds())!;
|
|
}
|
|
}
|
|
|
|
// Set directory timestamps (reverse order for subdirectories)
|
|
for (usz i = self.count(); i > 0; i--)
|
|
{
|
|
ZipEntry entry;
|
|
if (try res = self.stat_at(i - 1))
|
|
{
|
|
entry = res;
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
if (!entry.is_directory) continue;
|
|
|
|
if (try tmp = path::temp(output_dir))
|
|
{
|
|
if (try combined = tmp.tappend(entry.name))
|
|
{
|
|
String out_path_str = (String)combined.str_view();
|
|
file::set_modified_time(out_path_str, (Time_t)entry.time().to_seconds())!;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn usz ZipArchive.count(&self) => self.entries.len();
|
|
|
|
<*
|
|
Returns metadata for the entry at the given index.
|
|
@require index < self.count()
|
|
*>
|
|
fn ZipEntry? ZipArchive.stat_at(&self, usz index)
|
|
{
|
|
if (index >= self.entries.len()) return ENTRY_NOT_FOUND~;
|
|
return self.entries.get(index);
|
|
}
|
|
|
|
<*
|
|
Returns metadata for the entry with the given filename.
|
|
*>
|
|
fn ZipEntry? ZipArchive.stat(&self, String filename)
|
|
{
|
|
for (usz i = 0; i < self.entries.len(); i++)
|
|
{
|
|
ZipEntry entry = self.entries.get(i);
|
|
if (entry.name == filename) return entry;
|
|
}
|
|
return ENTRY_NOT_FOUND~;
|
|
}
|
|
|
|
|
|
<*
|
|
Reads an entire file from the archive.
|
|
@param allocator : `The allocator to use.`
|
|
@param filename : `The name of the file to read.`
|
|
@return `The uncompressed file data.`
|
|
*>
|
|
fn char[]? ZipArchive.read_file_all(&self, Allocator allocator, String filename, )
|
|
{
|
|
ZipEntryReader reader = self.open_reader(filename)!;
|
|
defer (void)reader.close();
|
|
ZipEntry entry = self.stat(filename)!;
|
|
|
|
char[] data;
|
|
defer catch if (data.ptr) allocator::free(allocator, data);
|
|
|
|
if (reader.method == STORE)
|
|
{
|
|
if (reader.size > (ulong)usz.max) return io::OVERFLOW~;
|
|
data = allocator::alloc_array(allocator, char, (usz)reader.size);
|
|
reader.read(data)!;
|
|
}
|
|
else
|
|
{
|
|
if (reader.adapter.start_offset > (ulong)isz.max) return io::OVERFLOW~;
|
|
self.file.set_cursor(reader.adapter.start_offset)!;
|
|
if (entry.compressed_size > (ulong)usz.max) return io::OVERFLOW~;
|
|
char[] compressed = allocator::alloc_array(allocator, char, (usz)entry.compressed_size);
|
|
defer allocator::free(allocator, compressed);
|
|
self.file.read(compressed)!;
|
|
data = deflate::decompress(allocator, compressed)!;
|
|
}
|
|
|
|
Crc32 crc;
|
|
crc.init();
|
|
crc.update(data);
|
|
if (~crc.result != entry.crc32) return CORRUPTED_DATA~;
|
|
|
|
return data;
|
|
}
|
|
|
|
|
|
<*
|
|
Adds a directory entry to the archive.
|
|
*>
|
|
fn void? ZipArchive.add_directory(&self, String dirname)
|
|
{
|
|
String dir_name = dirname;
|
|
if (!dirname.ends_with("/"))
|
|
{
|
|
dir_name = string::tformat("%s/", dirname);
|
|
}
|
|
|
|
ZipEntryWriter? writer_opt = self.open_writer(dir_name, STORE);
|
|
if (catch err = writer_opt) return err~;
|
|
|
|
ZipEntryWriter writer = writer_opt;
|
|
writer.entry.is_directory = true;
|
|
writer.close()!;
|
|
}
|
|
|
|
<*
|
|
Writes an entire file to the archive.
|
|
@param filename : `The name of the file to create.`
|
|
@param data : `The data to write.`
|
|
@param method : `Compression method.`
|
|
*>
|
|
fn void? ZipArchive.write_file(&self, String filename, char[] data, ZipMethod method = DEFLATE)
|
|
{
|
|
ZipEntryWriter writer = self.open_writer(filename, method)!;
|
|
|
|
writer.write(data)!;
|
|
writer.close()!;
|
|
}
|
|
|
|
struct ZipEntryReader (InStream)
|
|
{
|
|
ulong size; // Uncompressed size
|
|
ulong pos; // Uncompressed position
|
|
ZipMethod method;
|
|
// For DEFLATE
|
|
Inflater* inflater;
|
|
ArchiveStreamAdapter adapter;
|
|
char* bit_buf;
|
|
}
|
|
|
|
fn usz? ZipEntryReader.read(&self, char[] buffer) @dynamic
|
|
{
|
|
if (self.method == STORE)
|
|
{
|
|
usz n = self.adapter.read(buffer)!;
|
|
if (n == 0) return io::EOF~;
|
|
self.pos += n;
|
|
return n;
|
|
}
|
|
else if (self.method == DEFLATE)
|
|
{
|
|
if (self.inflater == null)
|
|
{
|
|
self.inflater = allocator::new(self.adapter.archive.allocator, Inflater);
|
|
self.bit_buf = allocator::alloc_array(self.adapter.archive.allocator, char, 8192);
|
|
self.inflater.init(&self.adapter, self.bit_buf[:8192]);
|
|
}
|
|
|
|
usz n = self.inflater.read(buffer)!;
|
|
if (n == 0) return io::EOF~;
|
|
self.pos += n;
|
|
return n;
|
|
}
|
|
|
|
return io::UNSUPPORTED_OPERATION~;
|
|
}
|
|
|
|
fn void? ZipEntryReader.close(&self) @dynamic
|
|
{
|
|
if (self.method == DEFLATE && self.inflater != null)
|
|
{
|
|
allocator::free(self.adapter.archive.allocator, self.bit_buf);
|
|
allocator::free(self.adapter.archive.allocator, self.inflater);
|
|
self.inflater = null;
|
|
self.bit_buf = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
fn char? ZipEntryReader.read_byte(&self) @dynamic
|
|
{
|
|
char[1] b;
|
|
usz n = self.read(&b)!;
|
|
if (n == 0) return io::EOF~;
|
|
return b[0];
|
|
}
|
|
|
|
fn usz ZipEntryReader.len(&self) @dynamic
|
|
{
|
|
if (self.size > (ulong)usz.max) return usz.max;
|
|
return (usz)self.size;
|
|
}
|
|
|
|
fn ulong? ZipEntryReader.available(&self) @dynamic
|
|
{
|
|
return self.size - self.pos;
|
|
}
|
|
|
|
fn usz? ZipEntryReader.seek(&self, isz offset, Seek seek) @dynamic
|
|
{
|
|
self.set_cursor((long)offset, (SeekOrigin)seek.ordinal)!;
|
|
long size = self.cursor()!;
|
|
if (size > (ulong)usz.max) return io::OVERFLOW~;
|
|
return (usz)size;
|
|
}
|
|
|
|
fn long? ZipEntryReader.cursor(&self) @dynamic
|
|
{
|
|
return self.pos;
|
|
}
|
|
|
|
fn void? ZipEntryReader.set_cursor(&self, long offset, SeekOrigin seek) @dynamic
|
|
{
|
|
if (self.method == DEFLATE) return io::UNSUPPORTED_OPERATION~;
|
|
|
|
ulong new_pos = self.pos;
|
|
switch (seek)
|
|
{
|
|
case FROM_START:
|
|
if (offset < 0) return io::INVALID_ARGUMENT~;
|
|
new_pos = offset;
|
|
case FROM_CURSOR:
|
|
new_pos = new_pos + offset;
|
|
case FROM_END:
|
|
new_pos = self.size + offset;
|
|
}
|
|
if (new_pos > self.size) new_pos = self.size;
|
|
self.pos = new_pos;
|
|
self.adapter.pos = new_pos;
|
|
}
|
|
|
|
<*
|
|
Opens a reader for an entry.
|
|
@param filename : `The name of the file to read.`
|
|
@return `A reader for the entry's data.`
|
|
*>
|
|
fn ZipEntryReader? ZipArchive.open_reader(&self, String filename)
|
|
{
|
|
ZipEntry? entry = self.stat(filename);
|
|
if (catch entry) return ENTRY_NOT_FOUND~;
|
|
|
|
if (entry.is_encrypted) return ENCRYPTED_FILE~;
|
|
|
|
self.file.flush()!;
|
|
if (entry.offset > (ulong)isz.max) return io::OVERFLOW~;
|
|
self.file.set_cursor(entry.offset)!;
|
|
ZipLFH lfh;
|
|
io::read_any(self.file, &lfh)!;
|
|
if (lfh.signature.val != ZIP_LFH_SIG) return CORRUPTED_DATA~;
|
|
|
|
ZipEntryReader reader;
|
|
|
|
reader.adapter.archive = self;
|
|
ulong start_offset = entry.offset + ZipLFH.sizeof + lfh.filename_len.val + lfh.extra_field_len.val;
|
|
if (start_offset > (ulong)isz.max) return io::OVERFLOW~;
|
|
reader.adapter.start_offset = start_offset;
|
|
// For STORE: adapter.size is uncompressed size.
|
|
// For DEFLATE: adapter.size is compressed size.
|
|
reader.adapter.size = entry.method == STORE
|
|
? entry.uncompressed_size
|
|
: entry.compressed_size;
|
|
reader.size = entry.uncompressed_size;
|
|
reader.method = entry.method;
|
|
|
|
return reader;
|
|
}
|
|
|
|
struct ZipEntryWriter (OutStream)
|
|
{
|
|
ZipArchive* archive;
|
|
ZipEntry entry;
|
|
Crc32 crc;
|
|
ZipLFH lfh;
|
|
ulong lfh_offset;
|
|
char[] buffer;
|
|
usz capacity;
|
|
}
|
|
|
|
fn usz? ZipEntryWriter.write(&self, char[] bytes) @dynamic
|
|
{
|
|
if (bytes.len == 0) return 0;
|
|
|
|
self.crc.update(bytes);
|
|
self.entry.uncompressed_size += (ulong)bytes.len;
|
|
|
|
if (self.entry.method == STORE)
|
|
{
|
|
usz n = self.archive.file.write(bytes)!;
|
|
self.entry.compressed_size += n;
|
|
return n;
|
|
}
|
|
else
|
|
{
|
|
usz new_len = self.buffer.len + bytes.len;
|
|
if (new_len > self.capacity)
|
|
{
|
|
usz new_cap = self.capacity;
|
|
if (new_cap < 4096) new_cap = 4096;
|
|
while (new_cap < new_len)
|
|
{
|
|
if (new_cap > usz.max / 2) { new_cap = new_len; break; }
|
|
new_cap *= 2;
|
|
}
|
|
|
|
char* p = allocator::realloc_array(self.archive.allocator, self.buffer.ptr, char, new_cap);
|
|
self.buffer = p[:self.buffer.len]; // Keep length as used size
|
|
self.capacity = new_cap;
|
|
}
|
|
|
|
mem::copy(self.buffer.ptr + self.buffer.len, bytes.ptr, bytes.len);
|
|
// Update slice length
|
|
self.buffer = self.buffer.ptr[:new_len];
|
|
return bytes.len;
|
|
}
|
|
}
|
|
|
|
fn void? ZipEntryWriter.write_byte(&self, char c) @dynamic
|
|
{
|
|
char[1] b = { c };
|
|
self.write(&b)!;
|
|
}
|
|
|
|
fn void? ZipEntryWriter.close(&self) @dynamic
|
|
{
|
|
if (self.entry.method == DEFLATE)
|
|
{
|
|
if (self.buffer.len > 0)
|
|
{
|
|
char[]? data = deflate::compress(self.archive.allocator, self.buffer);
|
|
if (catch data) return IO_ERROR~;
|
|
self.archive.file.write(data)!;
|
|
self.entry.compressed_size = data.len;
|
|
|
|
allocator::free(self.archive.allocator, data);
|
|
allocator::free(self.archive.allocator, self.buffer);
|
|
self.buffer = {};
|
|
}
|
|
}
|
|
|
|
self.entry.crc32 = ~self.crc.result;
|
|
|
|
self.lfh.crc32.val = self.entry.crc32;
|
|
|
|
// ZIP64 sentinel: readers typically fallback to Central Directory for actual sizes.
|
|
self.lfh.compressed_size.val = (uint)math::min(self.entry.compressed_size, (ulong)0xFFFFFFFF);
|
|
self.lfh.uncompressed_size.val = (uint)math::min(self.entry.uncompressed_size, (ulong)0xFFFFFFFF);
|
|
|
|
long end_pos = self.archive.file.cursor()!;
|
|
|
|
if (self.lfh_offset > (long)isz.max) return io::OVERFLOW~;
|
|
self.archive.file.set_cursor(self.lfh_offset)!;
|
|
io::write_any(self.archive.file, &self.lfh)!;
|
|
|
|
self.archive.file.set_cursor(end_pos)!;
|
|
|
|
self.archive.entries.push(self.entry);
|
|
}
|
|
|
|
<*
|
|
Opens a writer for a new entry.
|
|
@param filename : `The name of the file to create in the archive.`
|
|
@param method : `Compression method.`
|
|
@return `A writer for the new entry.`
|
|
*>
|
|
fn ZipEntryWriter? ZipArchive.open_writer(&self, String filename, ZipMethod method = DEFLATE)
|
|
{
|
|
if (!self.mode.starts_with("w")) return IO_ERROR~;
|
|
|
|
ZipEntryWriter writer;
|
|
writer.archive = self;
|
|
writer.entry.name = filename.copy(self.allocator);
|
|
defer catch allocator::free(self.allocator, writer.entry.name);
|
|
|
|
writer.entry.method = method;
|
|
writer.entry.offset = self.file.cursor()!;
|
|
writer.crc.init();
|
|
writer.lfh_offset = writer.entry.offset;
|
|
|
|
writer.lfh = {
|
|
.signature.val = ZIP_LFH_SIG,
|
|
.version_needed.val = 20,
|
|
.method = method,
|
|
.flags.val = 0x0800, // UTF-8 flag
|
|
.filename_len.val = (ushort)filename.len,
|
|
};
|
|
|
|
Time now = time::now();
|
|
writer.entry.last_mod_time = time_to_dos_time(now);
|
|
writer.entry.last_mod_date = time_to_dos_date(now);
|
|
writer.lfh.last_mod_time.val = writer.entry.last_mod_time;
|
|
writer.lfh.last_mod_date.val = writer.entry.last_mod_date;
|
|
|
|
io::write_any(self.file, &writer.lfh)!;
|
|
self.file.write(filename)!;
|
|
|
|
return writer;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// PRIVATE IMPLEMENTATION
|
|
// -----------------------------------------------------------------------------
|
|
|
|
struct ArchiveStreamAdapter (InStream) @private
|
|
{
|
|
ZipArchive* archive;
|
|
ulong start_offset;
|
|
ulong size;
|
|
ulong pos;
|
|
}
|
|
|
|
fn usz? ArchiveStreamAdapter.read(&self, char[] buffer) @dynamic
|
|
{
|
|
if (self.pos >= self.size) return 0;
|
|
|
|
usz to_read = (usz)math::min((ulong)buffer.len, self.size - self.pos);
|
|
if (to_read == 0) return 0;
|
|
|
|
ulong abs_pos = self.start_offset + self.pos;
|
|
if (abs_pos > (ulong)isz.max) return io::OVERFLOW~;
|
|
|
|
// Note: ZipArchive shared file handle access is not thread-safe.
|
|
self.archive.file.set_cursor(abs_pos)!;
|
|
usz n = self.archive.file.read(buffer[:to_read])!;
|
|
self.pos += n;
|
|
return n;
|
|
}
|
|
|
|
fn char? ArchiveStreamAdapter.read_byte(&self) @dynamic
|
|
{
|
|
char[1] b;
|
|
if (try n = self.read(b[..]))
|
|
{
|
|
if (n == 0) return io::EOF~;
|
|
return b[0];
|
|
}
|
|
return io::EOF~;
|
|
}
|
|
|
|
struct ZipLFH @packed @private
|
|
{
|
|
UIntLE signature;
|
|
UShortLE version_needed;
|
|
UShortLE flags;
|
|
ZipMethod method;
|
|
UShortLE last_mod_time;
|
|
UShortLE last_mod_date;
|
|
UIntLE crc32;
|
|
UIntLE compressed_size;
|
|
UIntLE uncompressed_size;
|
|
UShortLE filename_len;
|
|
UShortLE extra_field_len;
|
|
}
|
|
|
|
struct ZipCDH @packed @private
|
|
{
|
|
UIntLE signature;
|
|
UShortLE version_made_by;
|
|
UShortLE version_needed;
|
|
UShortLE flags;
|
|
ZipMethod method;
|
|
UShortLE last_mod_time;
|
|
UShortLE last_mod_date;
|
|
UIntLE crc32;
|
|
UIntLE compressed_size;
|
|
UIntLE uncompressed_size;
|
|
UShortLE filename_len;
|
|
UShortLE extra_field_len;
|
|
UShortLE comment_len;
|
|
UShortLE disk_number_start;
|
|
UShortLE internal_attr;
|
|
UIntLE external_attr;
|
|
UIntLE relative_offset;
|
|
}
|
|
|
|
struct ZipEOCD @packed @private
|
|
{
|
|
UIntLE signature;
|
|
UShortLE disk_number;
|
|
UShortLE cd_disk_number;
|
|
UShortLE num_entries_this_disk;
|
|
UShortLE num_entries;
|
|
UIntLE cd_size;
|
|
UIntLE cd_offset;
|
|
UShortLE comment_len;
|
|
}
|
|
|
|
const uint ZIP_LFH_SIG @private = 0x04034B50;
|
|
const uint ZIP_CDH_SIG @private = 0x02014B50;
|
|
const uint ZIP_EOCD_SIG @private = 0x06054B50;
|
|
const uint ZIP64_EOCD_SIG @private = 0x06064B50;
|
|
const uint ZIP64_LOCATOR_SIG @private = 0x07064B50;
|
|
|
|
const ushort ZIP64_EXTRA_ID @private = 0x0001;
|
|
|
|
struct Zip64EOCD @packed @private
|
|
{
|
|
UIntLE signature;
|
|
ULongLE size; // Size of remaining record
|
|
UShortLE version_made;
|
|
UShortLE version_needed;
|
|
UIntLE disk_num;
|
|
UIntLE disk_start;
|
|
ULongLE count_this_disk;
|
|
ULongLE count_total;
|
|
ULongLE size_cd;
|
|
ULongLE offset_cd;
|
|
// char[] custom_data;
|
|
}
|
|
|
|
struct Zip64Locator @packed @private
|
|
{
|
|
UIntLE signature;
|
|
UIntLE disk_start;
|
|
ULongLE offset_eocd;
|
|
UIntLE total_disks;
|
|
}
|
|
|
|
struct Zip64ExtraField @private
|
|
{
|
|
ushort header_id;
|
|
ushort size;
|
|
ulong uncompressed_size;
|
|
ulong compressed_size;
|
|
ulong offset;
|
|
uint disk_start;
|
|
}
|
|
|
|
fn ushort time_to_dos_time(Time t) @private
|
|
{
|
|
long seconds = (long)t / 1_000_000;
|
|
int s = (int)(seconds % 60);
|
|
int m = (int)((seconds / 60) % 60);
|
|
int h = (int)((seconds / 3600) % 24);
|
|
return (ushort)(((h & 0x1F) << 11) | ((m & 0x3F) << 5) | ((s / 2) & 0x1F));
|
|
}
|
|
|
|
fn ushort time_to_dos_date(Time t) @private
|
|
{
|
|
long seconds = (long)t / 1_000_000;
|
|
long days = seconds / 86400;
|
|
|
|
if (days < 3652) return (ushort)((0 << 9) | (1 << 5) | 1); // 1980-01-01
|
|
|
|
days -= 3652;
|
|
int year = 0;
|
|
while (true)
|
|
{
|
|
int y = 1980 + year;
|
|
bool is_leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
|
|
int year_days = is_leap ? 366 : 365;
|
|
if (days < (long)year_days) break;
|
|
days -= (long)year_days;
|
|
year++;
|
|
}
|
|
|
|
int[12] month_days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
int y = 1980 + year;
|
|
if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) month_days[1] = 29;
|
|
|
|
int month = 0;
|
|
while (days >= (long)month_days[month])
|
|
{
|
|
days -= (long)month_days[month];
|
|
month++;
|
|
}
|
|
|
|
int day = (int)days + 1;
|
|
month++;
|
|
return (ushort)(((year & 0x7F) << 9) | ((month & 0xF) << 5) | (day & 0x1F));
|
|
}
|
|
|
|
fn Time dos_date_time_to_time(ushort dos_date, ushort dos_time) @private
|
|
{
|
|
int sec = (int)((dos_time & 0x1F) * 2);
|
|
int min = (int)((dos_time >> 5) & 0x3F);
|
|
int hour = (int)((dos_time >> 11) & 0x1F);
|
|
|
|
int day = (int)(dos_date & 0x1F);
|
|
int month = (int)((dos_date >> 5) & 0xF);
|
|
int year = (int)((dos_date >> 9) & 0x7F) + 1980;
|
|
|
|
if (day == 0) day = 1;
|
|
if (month == 0) month = 1;
|
|
if (month > 12) month = 12;
|
|
|
|
// 1970 to 1980 is 3652 days
|
|
long total_days = 3652;
|
|
|
|
for (int y = 1980; y < year; y++)
|
|
{
|
|
bool is_leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
|
|
total_days += is_leap ? 366 : 365;
|
|
}
|
|
|
|
bool is_leap_year = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
|
int[12] mdays = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
if (is_leap_year) mdays[1] = 29;
|
|
|
|
for (int i = 0; i < month - 1; i++)
|
|
{
|
|
total_days += mdays[i];
|
|
}
|
|
|
|
total_days += (long)(day - 1);
|
|
|
|
long total_seconds = total_days * 86400;
|
|
total_seconds += (long)hour * 3600;
|
|
total_seconds += (long)min * 60;
|
|
total_seconds += (long)sec;
|
|
|
|
return (Time)(total_seconds * 1_000_000);
|
|
}
|
|
|
|
fn bool is_valid_utf8(char[] bytes) @private
|
|
{
|
|
usz i = 0;
|
|
while (i < bytes.len)
|
|
{
|
|
char lead = bytes[i];
|
|
switch (lead)
|
|
{
|
|
case 0x00..0x7F:
|
|
i++;
|
|
case 0xC2..0xDF:
|
|
if (i + 1 >= bytes.len || (bytes[i + 1] & 0xC0) != 0x80) return false;
|
|
i += 2;
|
|
case 0xE0..0xEF:
|
|
if (i + 2 >= bytes.len) return false;
|
|
char b1 = bytes[i + 1];
|
|
char b2 = bytes[i + 2];
|
|
if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80) return false;
|
|
if (lead == 0xE0 && b1 < 0xA0) return false;
|
|
if (lead == 0xED && b1 >= 0xA0) return false;
|
|
i += 3;
|
|
case 0xF0..0xF4:
|
|
if (i + 3 >= bytes.len) return false;
|
|
char b1 = bytes[i + 1];
|
|
char b2 = bytes[i + 2];
|
|
char b3 = bytes[i + 3];
|
|
if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80) return false;
|
|
if (lead == 0xF0 && b1 < 0x90) return false;
|
|
if (lead == 0xF4 && b1 >= 0x90) return false;
|
|
i += 4;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|