From 5d3d32ded672b67471d9d7c85ebbe691129cc51c Mon Sep 17 00:00:00 2001 From: Igor Tolmachev Date: Mon, 1 Jul 2024 19:12:40 +0900 Subject: Add compression support (lzma and xz are broken) --- src/zip/driver.rs | 88 ++++++++++++++------------ src/zip/error.rs | 2 + src/zip/file/info.rs | 161 ++++++++++++++++++++++++++++++++++++++++++++++++ src/zip/file/mod.rs | 7 +++ src/zip/file/read.rs | 125 +++++++++++++++++++++++++++++++++++++ src/zip/file/write.rs | 30 +++++++++ src/zip/file_driver.rs | 98 ----------------------------- src/zip/file_info.rs | 163 ------------------------------------------------- src/zip/mod.rs | 6 +- src/zip/tests.rs | 2 +- 10 files changed, 378 insertions(+), 304 deletions(-) create mode 100644 src/zip/file/info.rs create mode 100644 src/zip/file/mod.rs create mode 100644 src/zip/file/read.rs create mode 100644 src/zip/file/write.rs delete mode 100644 src/zip/file_driver.rs delete mode 100644 src/zip/file_info.rs (limited to 'src/zip') diff --git a/src/zip/driver.rs b/src/zip/driver.rs index a280dc7..e276844 100644 --- a/src/zip/driver.rs +++ b/src/zip/driver.rs @@ -1,13 +1,17 @@ use crate::driver::{ArchiveRead, ArchiveWrite, Driver}; use crate::zip::structs::{deserialize, EOCDR64Locator, ExtraHeader, CDR, EOCDR, EOCDR64}; -use crate::zip::{BitFlag, CompressionMethod, ZipError, ZipFile, ZipFileInfo, ZipResult}; +use crate::zip::{ + BitFlag, CompressionMethod, ZipError, ZipFileInfo, ZipFileReader, ZipFileWriter, ZipResult, +}; use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime}; +use std::collections::HashMap as Map; +use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; -pub struct Zip { +pub struct Zip { io: Io, - files: Vec, + files: Map, comment: String, } @@ -15,11 +19,12 @@ impl Driver for Zip { type Error = ZipError; type Io = Io; - type FileDriver<'d> = ZipFile<'d, Self::Io> where Self::Io: 'd; type FileInfo = ZipFileInfo; } impl ArchiveRead for Zip { + type FileReader<'d> = ZipFileReader<'d, Io> where Io: 'd; + fn read(mut io: Self::Io) -> ZipResult { // Search eocdr let limit = 65557.min(io.seek(SeekFrom::End(0))?) as i64; @@ -79,7 +84,7 @@ impl ArchiveRead for Zip { }; // Read cd records - let mut files = Vec::with_capacity(cd_records as usize); + let mut files = Map::with_capacity(cd_records as usize); io.seek(SeekFrom::Start(cd_pointer))?; let buf = { let mut buf = vec![0; cd_size as usize]; @@ -134,49 +139,54 @@ impl ArchiveRead for Zip { } } - files.push(ZipFileInfo::new( - CompressionMethod::from_struct_id(cdr.compression_method)?, - BitFlag::new(cdr.bit_flag), - NaiveDateTime::new( - NaiveDate::from_ymd_opt( - (cdr.dos_date as i32 >> 9 & 0x7F) + 1980, - cdr.dos_date as u32 >> 5 & 0xF, - cdr.dos_date as u32 & 0x1F, - ) - .ok_or(ZipError::InvalidDate)?, - NaiveTime::from_hms_opt( - (cdr.dos_time as u32 >> 11) & 0x1F, - (cdr.dos_time as u32 >> 5) & 0x3F, - (cdr.dos_time as u32 & 0x1F) * 2, + files.insert( + name.clone(), + ZipFileInfo::new( + CompressionMethod::from_struct_id(cdr.compression_method)?, + BitFlag::new(cdr.bit_flag), + NaiveDateTime::new( + NaiveDate::from_ymd_opt( + (cdr.dos_date as i32 >> 9 & 0x7F) + 1980, + cdr.dos_date as u32 >> 5 & 0xF, + cdr.dos_date as u32 & 0x1F, + ) + .ok_or(ZipError::InvalidDate)?, + NaiveTime::from_hms_opt( + (cdr.dos_time as u32 >> 11) & 0x1F, + (cdr.dos_time as u32 >> 5) & 0x3F, + (cdr.dos_time as u32 & 0x1F) * 2, + ) + .ok_or(ZipError::InvalidTime)?, ) - .ok_or(ZipError::InvalidTime)?, - ) - .and_local_timezone(Local) - .unwrap(), - cdr.crc, - compressed_size, - size, - header_pointer, - name, - comment, - )); + .and_local_timezone(Local) + .unwrap(), + cdr.crc, + compressed_size, + size, + header_pointer, + name, + comment, + ), + ); } Ok(Self { io, files, comment }) } - fn files(&self) -> &Vec { - &self.files + fn files(&self) -> Vec<&Self::FileInfo> { + let mut files: Vec<&Self::FileInfo> = self.files.values().collect(); + files.sort_by_key(|f| &f.name); + files } - fn get_file_info(&self, index: usize) -> ZipResult<&Self::FileInfo> { - self.files.get(index).ok_or(ZipError::FileNotFound.into()) + fn get_file_info(&self, name: &str) -> ZipResult<&Self::FileInfo> { + self.files.get(name).ok_or(ZipError::FileNotFound.into()) } - fn get_file_reader<'d>(&'d mut self, index: usize) -> ZipResult> { - Ok(ZipFile::new( + fn get_file_reader<'d>(&'d mut self, name: &str) -> ZipResult> { + Ok(ZipFileReader::new( &mut self.io, - self.files.get(index).ok_or(ZipError::FileNotFound)?, + self.files.get(name).ok_or(ZipError::FileNotFound)?, )?) } } @@ -187,6 +197,8 @@ impl Zip { } } -impl ArchiveWrite for Zip {} +impl ArchiveWrite for Zip { + type FileWriter<'d> = ZipFileWriter<'d, Io> where Io: 'd; +} impl Zip {} diff --git a/src/zip/error.rs b/src/zip/error.rs index 3acbf8c..5659f93 100644 --- a/src/zip/error.rs +++ b/src/zip/error.rs @@ -21,6 +21,7 @@ pub enum ZipError { NegativeFileOffset, FileNotFound, + CompressionDataIsUnseekable, } impl From for ArchiveError { @@ -59,6 +60,7 @@ impl Display for ZipError { Self::NegativeFileOffset => write!(f, "Negative file offset"), Self::FileNotFound => write!(f, "File not found"), + Self::CompressionDataIsUnseekable => write!(f, "Compression data is unseekable"), } } } diff --git a/src/zip/file/info.rs b/src/zip/file/info.rs new file mode 100644 index 0000000..2891b59 --- /dev/null +++ b/src/zip/file/info.rs @@ -0,0 +1,161 @@ +use crate::driver::ArchiveFileInfo; +use crate::zip::{ZipError, ZipResult}; +use chrono::{DateTime, Local}; + +#[derive(Debug, Clone)] +pub enum CompressionMethod { + Store, + Deflate, + BZip2, + LZMA, + XZ, +} + +impl CompressionMethod { + pub(crate) fn from_struct_id(id: u16) -> ZipResult { + match id { + 0 => Ok(Self::Store), + 8 => Ok(Self::Deflate), + 12 => Ok(Self::BZip2), + 14 => Ok(Self::LZMA), + 95 => Ok(Self::XZ), + 1..=7 | 9..=11 | 13 | 15..=20 | 93..=94 | 96..=99 => { + Err(ZipError::UnsupportedCompressionMethod.into()) + } + 21..=92 | 100.. => Err(ZipError::InvalidCompressionMethod.into()), + } + } +} + +#[derive(Debug, Clone)] +pub struct BitFlag { + flag: u16, +} + +pub mod bit { + #[derive(Debug, PartialEq, Eq)] + pub enum DeflateMode { + Normal, + Maximum, + Fast, + SuperFast, + } +} + +macro_rules! get_set_bit_flag { + {$($get:ident $set:ident $bit:expr)+} => { + $( + pub fn $get(&self) -> bool { + self.get_bit($bit) + } + + pub fn $set(&mut self, enable: bool) { + self.set_bit($bit, enable); + } + )* + }; +} + +impl BitFlag { + pub fn new(flag: u16) -> Self { + Self { flag } + } + + #[inline] + fn get_bit(&self, bit: u32) -> bool { + (self.flag & 2u16.pow(bit)) > 0 + } + + #[inline] + fn set_bit(&mut self, bit: u32, enable: bool) { + if enable { + self.flag |= 2u16.pow(bit); + } else { + self.flag &= !2u16.pow(bit); + } + } + + pub fn deflate_mode(&self) -> bit::DeflateMode { + match self.flag & 6 { + 0 => bit::DeflateMode::Normal, + 2 => bit::DeflateMode::Maximum, + 4 => bit::DeflateMode::Fast, + 6 => bit::DeflateMode::SuperFast, + _ => panic!("impossible"), + } + } + + pub fn set_deflate_mode(&mut self, mode: bit::DeflateMode) { + match mode { + bit::DeflateMode::Normal => { + self.set_bit(1, false); + self.set_bit(2, false); + } + bit::DeflateMode::Maximum => { + self.set_bit(1, true); + self.set_bit(2, false); + } + bit::DeflateMode::Fast => { + self.set_bit(1, false); + self.set_bit(2, true); + } + bit::DeflateMode::SuperFast => { + self.set_bit(1, true); + self.set_bit(2, true); + } + } + } + + get_set_bit_flag! { + is_encrypted set_encrypted 0 + is_imploding_8k set_imploding_8k 1 + is_imploding_3sf_trees set_imploding_3sf_trees 2 + is_lzma_has_eos_marker set_lzma_has_eos_marker 1 + is_has_data_descriptor set_has_data_descriptor 3 + is_patched_data set_patched_data 5 + is_strong_encryption set_strong_encryption 6 + is_utf8 set_utf8 11 + is_cd_encryption set_cd_encryption 13 + } +} + +#[derive(Debug, Clone)] +pub struct ZipFileInfo { + pub compression_method: CompressionMethod, + pub bit_flag: BitFlag, + pub datetime: DateTime, + pub crc: u32, + pub compressed_size: u64, + pub size: u64, + pub header_pointer: u64, + pub name: String, + pub comment: String, +} + +impl ZipFileInfo { + pub fn new( + compression_method: CompressionMethod, + bit_flag: BitFlag, + datetime: DateTime, + crc: u32, + compressed_size: u64, + size: u64, + header_pointer: u64, + name: String, + comment: String, + ) -> Self { + Self { + compression_method, + bit_flag, + datetime, + crc, + compressed_size, + size, + header_pointer, + name, + comment, + } + } +} + +impl ArchiveFileInfo for ZipFileInfo {} diff --git a/src/zip/file/mod.rs b/src/zip/file/mod.rs new file mode 100644 index 0000000..43ccc04 --- /dev/null +++ b/src/zip/file/mod.rs @@ -0,0 +1,7 @@ +mod info; +mod read; +mod write; + +pub use info::{bit, BitFlag, CompressionMethod, ZipFileInfo}; +pub use read::ZipFileReader; +pub use write::ZipFileWriter; diff --git a/src/zip/file/read.rs b/src/zip/file/read.rs new file mode 100644 index 0000000..56f42da --- /dev/null +++ b/src/zip/file/read.rs @@ -0,0 +1,125 @@ +use crate::driver::FileDriver; +use crate::zip::{CompressionMethod, ZipError, ZipFileInfo, ZipResult}; +use bzip2::read::BzDecoder; +use flate2::read::DeflateDecoder; +use std::io::{ + Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, +}; +use xz2::read::XzDecoder; + +enum IoProxy { + Store(Io), + Deflate(DeflateDecoder), + BZip2(BzDecoder), + XZ(XzDecoder), +} + +pub struct ZipFileReader<'d, Io: Read> { + io: IoProxy<&'d mut Io>, + info: &'d ZipFileInfo, + + bounds: (u64, u64), + cursor: u64, +} + +impl<'d, Io: Read> FileDriver for ZipFileReader<'d, Io> { + type Io = Io; + type FileInfo = ZipFileInfo; + + fn info(&self) -> &Self::FileInfo { + self.info + } +} + +impl<'d, Io: Read + Seek> ZipFileReader<'d, Io> { + pub fn new(io: &'d mut Io, info: &'d ZipFileInfo) -> ZipResult { + io.seek(SeekFrom::Start(info.header_pointer))?; + let buf = { + let mut buf = [0; 30]; + io.read(&mut buf)?; + buf + }; + if u32::from_le_bytes(buf[..4].try_into().unwrap()) != 0x04034b50 { + return Err(ZipError::InvalidFileHeaderSignature.into()); + } + let data_pointer = info.header_pointer + + 30 + + u16::from_le_bytes(buf[26..28].try_into().unwrap()) as u64 + + u16::from_le_bytes(buf[28..30].try_into().unwrap()) as u64; + io.seek(SeekFrom::Start(data_pointer))?; + + Ok(Self { + io: match info.compression_method { + CompressionMethod::Store => IoProxy::Store(io), + CompressionMethod::Deflate => IoProxy::Deflate(DeflateDecoder::new(io)), + CompressionMethod::BZip2 => IoProxy::BZip2(BzDecoder::new(io)), + CompressionMethod::LZMA => IoProxy::XZ(XzDecoder::new(io)), + CompressionMethod::XZ => IoProxy::XZ(XzDecoder::new(io)), + }, + info, + + bounds: (data_pointer, data_pointer + info.compressed_size), + cursor: data_pointer, + }) + } + + pub fn seekable(&self) -> bool { + match self.io { + IoProxy::Store(..) => true, + _ => false, + } + } +} + +impl<'d, Io: Read> Read for ZipFileReader<'d, Io> { + fn read(&mut self, buf: &mut [u8]) -> IoResult { + let upper = buf.len().min((self.bounds.1 - self.cursor) as usize); + let bytes = match &mut self.io { + IoProxy::Store(io) => io.read(&mut buf[..upper]), + IoProxy::Deflate(io) => io.read(&mut buf[..upper]), + IoProxy::BZip2(io) => io.read(&mut buf[..upper]), + IoProxy::XZ(io) => io.read(&mut buf[..upper]), + }?; + self.cursor += upper as u64; + Ok(bytes) + } +} + +impl<'d, Io: Read + Seek> Seek for ZipFileReader<'d, Io> { + fn seek(&mut self, pos: SeekFrom) -> IoResult { + match &mut self.io { + IoProxy::Store(io) => { + self.cursor = match pos { + SeekFrom::Start(offset) => self.bounds.0 + offset, + SeekFrom::End(offset) => { + let cursor = self.bounds.1.saturating_add_signed(offset); + if cursor < self.bounds.0 { + return Err(IoError::new( + IoErrorKind::InvalidInput, + ZipError::NegativeFileOffset, + )); + } + cursor + } + SeekFrom::Current(offset) => { + let cursor = self.cursor.saturating_add_signed(offset); + if cursor < self.bounds.0 { + return Err(IoError::new( + IoErrorKind::InvalidInput, + ZipError::NegativeFileOffset, + )); + } + cursor + } + } + .min(self.bounds.1); + + Ok(io.seek(SeekFrom::Start(self.cursor))? - self.bounds.0) + } + _ => Err(IoError::new( + IoErrorKind::Unsupported, + ZipError::CompressionDataIsUnseekable, + )), + } + } +} diff --git a/src/zip/file/write.rs b/src/zip/file/write.rs new file mode 100644 index 0000000..627db6d --- /dev/null +++ b/src/zip/file/write.rs @@ -0,0 +1,30 @@ +use crate::driver::FileDriver; +use crate::zip::ZipFileInfo; +use bzip2::write::BzEncoder; +use flate2::write::DeflateEncoder; +use std::io::Write; +use xz2::write::XzEncoder; + +enum IoProxy { + Store(Io), + Deflate(DeflateEncoder), + BZip2(BzEncoder), + XZ(XzEncoder), +} + +pub struct ZipFileWriter<'d, Io: Write> { + io: IoProxy<&'d mut Io>, + info: &'d ZipFileInfo, + + bounds: (u64, u64), + cursor: u64, +} + +impl<'d, Io: Write> FileDriver for ZipFileWriter<'d, Io> { + type Io = Io; + type FileInfo = ZipFileInfo; + + fn info(&self) -> &Self::FileInfo { + self.info + } +} diff --git a/src/zip/file_driver.rs b/src/zip/file_driver.rs deleted file mode 100644 index 51075e4..0000000 --- a/src/zip/file_driver.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::driver::FileDriver; -use crate::zip::{ZipError, ZipFileInfo, ZipResult}; -use std::io::{ - Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, Write, -}; - -pub struct ZipFile<'d, Io> { - io: &'d mut Io, - info: &'d ZipFileInfo, - - bounds: (u64, u64), - cursor: u64, -} - -impl<'d, Io> FileDriver for ZipFile<'d, Io> { - type Io = Io; - type FileInfo = ZipFileInfo; - - fn info(&self) -> &Self::FileInfo { - self.info - } -} - -impl<'d, Io: Read + Seek> ZipFile<'d, Io> { - pub fn new(io: &'d mut Io, info: &'d ZipFileInfo) -> ZipResult { - io.seek(SeekFrom::Start(info.header_pointer))?; - let buf = { - let mut buf = [0; 30]; - io.read(&mut buf)?; - buf - }; - if u32::from_le_bytes(buf[..4].try_into().unwrap()) != 0x04034b50 { - return Err(ZipError::InvalidFileHeaderSignature.into()); - } - let data_pointer = info.header_pointer - + 30 - + u16::from_le_bytes(buf[26..28].try_into().unwrap()) as u64 - + u16::from_le_bytes(buf[28..30].try_into().unwrap()) as u64; - io.seek(SeekFrom::Start(data_pointer))?; - - Ok(Self { - io, - info, - - bounds: (data_pointer, data_pointer + info.compressed_size), - cursor: data_pointer, - }) - } -} - -impl<'d, Io: Read> Read for ZipFile<'d, Io> { - fn read(&mut self, buf: &mut [u8]) -> IoResult { - let upper = buf.len().min((self.bounds.1 - self.cursor) as usize); - self.cursor += upper as u64; - self.io.read(&mut buf[..upper]) - } -} - -impl<'d, Io: Write> Write for ZipFile<'d, Io> { - fn write(&mut self, buf: &[u8]) -> IoResult { - todo!() - } - - fn flush(&mut self) -> IoResult<()> { - todo!() - } -} - -impl<'d, Io: Seek> Seek for ZipFile<'d, Io> { - fn seek(&mut self, pos: SeekFrom) -> IoResult { - self.cursor = match pos { - SeekFrom::Start(offset) => self.bounds.0 + offset, - SeekFrom::End(offset) => { - let cursor = self.bounds.1.saturating_add_signed(offset); - if cursor < self.bounds.0 { - return Err(IoError::new( - IoErrorKind::InvalidInput, - ZipError::NegativeFileOffset, - )); - } - cursor - } - SeekFrom::Current(offset) => { - let cursor = self.cursor.saturating_add_signed(offset); - if cursor < self.bounds.0 { - return Err(IoError::new( - IoErrorKind::InvalidInput, - ZipError::NegativeFileOffset, - )); - } - cursor - } - } - .min(self.bounds.1); - - Ok(self.io.seek(SeekFrom::Start(self.cursor))? - self.bounds.0) - } -} diff --git a/src/zip/file_info.rs b/src/zip/file_info.rs deleted file mode 100644 index 92d52f7..0000000 --- a/src/zip/file_info.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::driver::ArchiveFileInfo; -use crate::zip::{ZipError, ZipResult}; -use chrono::{DateTime, Local}; - -#[derive(Debug, Clone)] -pub enum CompressionMethod { - Store, - Deflate, - BZIP2, - LZMA, - ZStd, - XZ, -} - -impl CompressionMethod { - pub(crate) fn from_struct_id(id: u16) -> ZipResult { - match id { - 0 => Ok(Self::Store), - 8 => Ok(Self::Deflate), - 12 => Ok(Self::BZIP2), - 14 => Ok(Self::LZMA), - 93 => Ok(Self::ZStd), - 95 => Ok(Self::XZ), - 1..=7 | 9..=11 | 13 | 15..=20 | 94 | 96..=99 => { - Err(ZipError::UnsupportedCompressionMethod.into()) - } - 21..=92 | 100.. => Err(ZipError::InvalidCompressionMethod.into()), - } - } -} - -#[derive(Debug, Clone)] -pub struct BitFlag { - flag: u16, -} - -pub mod bit { - #[derive(Debug, PartialEq, Eq)] - pub enum DeflateMode { - Normal, - Maximum, - Fast, - SuperFast, - } -} - -macro_rules! get_set_bit_flag { - {$($get:ident $set:ident $bit:expr)+} => { - $( - pub fn $get(&self) -> bool { - self.get_bit($bit) - } - - pub fn $set(&mut self, enable: bool) { - self.set_bit($bit, enable); - } - )* - }; -} - -impl BitFlag { - pub fn new(flag: u16) -> Self { - Self { flag } - } - - #[inline] - fn get_bit(&self, bit: u32) -> bool { - (self.flag & 2u16.pow(bit)) > 0 - } - - #[inline] - fn set_bit(&mut self, bit: u32, enable: bool) { - if enable { - self.flag |= 2u16.pow(bit); - } else { - self.flag &= !2u16.pow(bit); - } - } - - pub fn deflate_mode(&self) -> bit::DeflateMode { - match self.flag & 6 { - 0 => bit::DeflateMode::Normal, - 2 => bit::DeflateMode::Maximum, - 4 => bit::DeflateMode::Fast, - 6 => bit::DeflateMode::SuperFast, - _ => panic!("impossible"), - } - } - - pub fn set_deflate_mode(&mut self, mode: bit::DeflateMode) { - match mode { - bit::DeflateMode::Normal => { - self.set_bit(1, false); - self.set_bit(2, false); - } - bit::DeflateMode::Maximum => { - self.set_bit(1, true); - self.set_bit(2, false); - } - bit::DeflateMode::Fast => { - self.set_bit(1, false); - self.set_bit(2, true); - } - bit::DeflateMode::SuperFast => { - self.set_bit(1, true); - self.set_bit(2, true); - } - } - } - - get_set_bit_flag! { - is_encrypted set_encrypted 0 - is_imploding_8k set_imploding_8k 1 - is_imploding_3sf_trees set_imploding_3sf_trees 2 - is_lzma_has_eos_marker set_lzma_has_eos_marker 1 - is_has_data_descriptor set_has_data_descriptor 3 - is_patched_data set_patched_data 5 - is_strong_encryption set_strong_encryption 6 - is_utf8 set_utf8 11 - is_cd_encryption set_cd_encryption 13 - } -} - -#[derive(Debug, Clone)] -pub struct ZipFileInfo { - pub compression_method: CompressionMethod, - pub bit_flag: BitFlag, - pub datetime: DateTime, - pub crc: u32, - pub compressed_size: u64, - pub size: u64, - pub header_pointer: u64, - pub name: String, - pub comment: String, -} - -impl ZipFileInfo { - pub fn new( - compression_method: CompressionMethod, - bit_flag: BitFlag, - datetime: DateTime, - crc: u32, - compressed_size: u64, - size: u64, - header_pointer: u64, - name: String, - comment: String, - ) -> Self { - Self { - compression_method, - bit_flag, - datetime, - crc, - compressed_size, - size, - header_pointer, - name, - comment, - } - } -} - -impl ArchiveFileInfo for ZipFileInfo {} diff --git a/src/zip/mod.rs b/src/zip/mod.rs index 3fe8384..0f19824 100644 --- a/src/zip/mod.rs +++ b/src/zip/mod.rs @@ -1,14 +1,12 @@ mod archive; mod driver; mod error; -mod file_driver; -mod file_info; +mod file; mod structs; pub use driver::Zip; pub use error::{ZipError, ZipResult}; -pub use file_driver::ZipFile; -pub use file_info::{bit, BitFlag, CompressionMethod, ZipFileInfo}; +pub use file::{bit, BitFlag, CompressionMethod, ZipFileInfo, ZipFileReader, ZipFileWriter}; #[cfg(test)] mod tests; diff --git a/src/zip/tests.rs b/src/zip/tests.rs index 05e076d..92a9c3f 100644 --- a/src/zip/tests.rs +++ b/src/zip/tests.rs @@ -1,4 +1,4 @@ -use crate::zip::file_info::{bit::DeflateMode, BitFlag}; +use crate::zip::{bit::DeflateMode, BitFlag}; #[test] fn test_bit_flag() { -- cgit v1.2.3