chore: update dependencies and optimise archive operations

This commit is contained in:
LynithDev 2026-09-29 15:04:51 +02:00
parent 00237d5281
commit 95eba5503f
22 changed files with 948 additions and 1032 deletions

1528
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -40,7 +40,7 @@ discord-rich-presence = {version = "=1.1.0"}
interfrost = {version = "=1.4.0"}
# logging
sentry = {version = "=0.48.5", default-features = false, features = [
sentry = {version = "=0.49.3", default-features = false, features = [
"backtrace",
"contexts",
"debug-images",
@ -49,7 +49,7 @@ sentry = {version = "=0.48.5", default-features = false, features = [
"reqwest",
"rustls",
]}
sentry-tracing = {version = "=0.48.5"}
sentry-tracing = {version = "=0.49.3"}
tracing = {version = "=0.1.44"}
tracing-futures = {version = "=0.2.5"}
tracing-subscriber = {version = "=0.3.23", features = ["env-filter", "fmt", "ansi", "json", "parking_lot", "registry"]}
@ -68,15 +68,15 @@ sqlx = {version = "=0.9.0", default-features = false, features = [
]}
# async
astral-tokio-tar = {version = "=0.6.2"}
async-compression = {version = "=0.4.42", features = ["tokio", "gzip"]}
astral-tokio-tar = {version = "=0.7.0"}
async-compression = {version = "=0.4.48", features = ["tokio", "gzip"]}
async-stream = {version = "=0.3.6"}
async-tempfile = {version = "=0.7.0", features = ["uuid"]}
async-trait = {version = "=0.1.89"}
async_zip = {version = "=0.0.18", features = ["chrono", "tokio", "tokio-fs", "deflate", "deflate64"]}
async-tempfile = {version = "=0.8.0", features = ["uuid"]}
async-trait = {version = "=0.1.92"}
async_zip = {version = "=0.0.19", features = ["tracing", "chrono", "tokio", "tokio-fs", "deflate", "deflate64"]}
futures-lite = {version = "=2.6.1"}
futures-util = {version = "=0.3.32", features = ["tokio-io"]}
reqwest = {version = "=0.13.3", default-features = false, features = [
futures-util = {version = "=0.3.34", features = ["tokio-io"]}
reqwest = {version = "=0.13.5", default-features = false, features = [
"json",
"stream",
"multipart",
@ -91,63 +91,63 @@ reqwest = {version = "=0.13.3", default-features = false, features = [
"system-proxy",
]}
reqwest-websocket = {version = "=0.6.0"}
tokio = {version = "=1.52.3", features = ["full"]}
tokio-stream = {version = "=0.1.18", features = ["full"]}
tokio-util = {version = "=0.7.18", features = ["full"]}
tokio = {version = "=1.53.1", features = ["full"]}
tokio-stream = {version = "=0.1.19", features = ["full"]}
tokio-util = {version = "=0.7.19", features = ["full"]}
# crypto (content hashing)
byteorder = {version = "=1.5.0"}
digest = {version = "=0.10.7", features = ["std"]}
md-5 = {version = "=0.10.6", default-features = false}
p256 = {version = "=0.13.2", features = ["ecdsa", "pkcs8"]}
sha1 = {version = "=0.10.6", default-features = false}
sha2 = {version = "=0.10.9", default-features = false}
digest = {version = "=0.11.3"}
md-5 = {version = "=0.11.0", default-features = false}
p256 = {version = "=0.14.0", features = ["ecdsa", "pkcs8"]}
sha1 = {version = "=0.11.0", default-features = false}
sha2 = {version = "=0.11.0", default-features = false}
# util
arboard = {version = "=3.6.1", features = ["wayland-data-control"]}
arc-swap = {version = "=1.7.1"}
base64 = {version = "=0.22.1"}
bytes = {version = "=1.11.1"}
chrono = {version = "=0.4.44", features = ["serde"]}
arc-swap = {version = "=1.9.2"}
base64 = {version = "=0.23.1"}
bytes = {version = "=1.12.1"}
chrono = {version = "=0.4.45", features = ["serde"]}
directories = {version = "=6.0.0"}
dunce = {version = "=1.0.5"}
htmd = {version = "=0.5.4"}
indicatif = {version = "=0.18.4", features = ["tokio"]}
junction = {version = "=2.0.0"}
htmd = {version = "=0.5.5"}
indicatif = {version = "=0.18.6", features = ["tokio"]}
junction = {version = "=2.1.0"}
libc = {version = "=0.2.189"}
notify = {version = "=8.2.0"}
open = {version = "=5.4.4"}
parking_lot = {version = "=0.12.5"}
pulldown-cmark = {version = "=0.13.4", default-features = false}
rand = {version = "=0.10.1"}
regex = {version = "=1.12.3"}
rand = {version = "=0.10.3"}
regex = {version = "=1.13.1"}
rfd = {version = "=0.17.2"}
rust-embed = {version = "=8.12.0", features = ["include-exclude", "compression", "debug-embed"]}
sanitize-filename = {version = "=0.6.0"}
serde = {version = "=1.0.228"}
serde_json = {version = "=1.0.150"}
serde_repr = {version = "=0.1.20"}
sysinfo = {version = "=0.39.5"}
toml = {version = "=1.1.4", default-features = false, features = ["parse", "serde"]}
trash = {version = "=5.2.6"}
serde = {version = "=1.0.229"}
serde_json = {version = "=1.0.151"}
serde_repr = {version = "=0.1.21"}
sysinfo = {version = "=0.39.6"}
toml = {version = "=1.1.6", default-features = false, features = ["parse", "serde"]}
trash = {version = "=5.2.9"}
percent-encoding = {version = "=2.3.2"}
url = {version = "=2.5.8", features = ["serde"]}
uuid = {version = "=1.23.1", features = ["serde", "v4"]}
uuid = {version = "=1.26.1", features = ["serde", "v4"]}
windows-sys = {version = "=0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"]}
# util (codegen/macro)
anyhow = {version = "=1.0.102"}
proc-macro2 = {version = "=1.0.106"}
quote = {version = "=1.0.45"}
anyhow = {version = "=1.0.104"}
proc-macro2 = {version = "=1.0.107"}
quote = {version = "=1.0.47"}
strum = {version = "=0.28.0", features = ["derive"]}
syn = {version = "=2.0.117"}
thiserror = {version = "=2.0.18"}
syn = {version = "=3.0.6"}
thiserror = {version = "=2.0.21"}
# ui
freya = {git = "https://github.com/marc2332/freya", tag = "v0.5.0-rc.7", features = ["engine", "winit", "router", "remote-asset", "sdk-tokio", "query", "radio", "borderless", "titlebar", "tray", "metrics"]}
freya-core = {git = "https://github.com/marc2332/freya", tag = "v0.5.0-rc.7"}
freya-testing = {git = "https://github.com/marc2332/freya", tag = "v0.5.0-rc.7"}
freya-skia-safe = "=0.100.0" # required for the SKSL player renderer
freya = {git = "https://github.com/marc2332/freya", tag = "v0.5.0-rc.8", features = ["engine", "winit", "router", "remote-asset", "sdk-tokio", "query", "radio", "borderless", "titlebar", "tray", "metrics"]}
freya-core = {git = "https://github.com/marc2332/freya", tag = "v0.5.0-rc.8"}
freya-testing = {git = "https://github.com/marc2332/freya", tag = "v0.5.0-rc.8"}
freya-skia-safe = "=0.100.0-beta.1" # required for the SKSL player renderer
image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]}
[profile.release]

View file

@ -91,7 +91,7 @@ impl Component for AccountPanel {
.opacity(progress)
.margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.))
.background(colors::page_elevated().with_a(220))
.blur(12.)
.backdrop_blur(12.)
.corner_radius(CornerRadius::new_all(12.))
.border(
Border::new()

View file

@ -346,7 +346,7 @@ impl Component for Button {
if enabled() {
rect = rect
.on_pointer_down(move |_| pressing.set(true))
.on_global_pointer_press(move |_| pressing.set(false))
.on_global_pointer_up(move |_| pressing.set(false))
.on_pointer_over(move |_| hovering.set(true))
.on_pointer_out(move |_| {
hovering.set(false);

View file

@ -57,7 +57,7 @@ impl Component for ControlPanel {
.opacity(progress)
.margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.))
.background(colors::page_elevated().with_a(220))
.blur(12.)
.backdrop_blur(12.)
.corner_radius(CornerRadius::new_all(14.))
.border(
Border::new()

View file

@ -121,7 +121,7 @@ impl Component for Dropdown {
.map(|b| Size::px(b.width()))
.unwrap_or_else(|| self.width.clone());
let on_global_pointer_press = move |_: Event<PointerEventData>| {
let on_global_pointer_up = move |_: Event<PointerEventData>| {
open.set_if_modified(false);
};
@ -154,7 +154,7 @@ impl Component for Dropdown {
})
.on_pointer_enter(move |_| hovering.set(true))
.on_pointer_leave(move |_| hovering.set(false))
.on_global_pointer_press(on_global_pointer_press)
.on_global_pointer_up(on_global_pointer_up)
.on_sized(move |e: Event<SizedEventData>| {
button_area.set_if_modified(Some(e.area));
})

View file

@ -455,7 +455,7 @@ fn scroll_body(args: BodyArgs) -> Element {
.height(Size::px(content_h))
.on_pointer_down(on_body_down)
.on_capture_global_pointer_move(on_global_move)
.on_capture_global_pointer_press(on_global_release)
.on_capture_global_pointer_up(on_global_release)
.on_global_key_down(on_key_down)
.child(log_body(
&lines[..],

View file

@ -86,7 +86,7 @@ impl Component for NotificationPanel {
.opacity(progress)
.margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.))
.background(colors::page_elevated().with_a(220))
.blur(12.)
.backdrop_blur(12.)
.corner_radius(CornerRadius::new_all(12.))
.border(
Border::new()

View file

@ -189,7 +189,7 @@ impl Component for PlayerModel {
Platform::get().send(UserEvent::RequestRedraw);
})
// Global so a release outside the widget still ends the drag
.on_global_pointer_press(move |_: Event<PointerEventData>| {
.on_global_pointer_up(move |_: Event<PointerEventData>| {
if drag.peek().is_some() {
drag.set(None);
}

View file

@ -340,7 +340,7 @@ impl Component for OtherVersionsTile {
} else {
colors::ghost_overlay_hover()
})
.blur(32.)
.backdrop_blur(32.)
.a11y_id(a11y_id)
.a11y_focusable(true)
.a11y_role(AccessibilityRole::Button)

View file

@ -655,7 +655,7 @@ impl ScrollArea {
})
.on_wheel(on_wheel)
.on_capture_global_pointer_move(on_global_move)
.on_capture_global_pointer_press(on_global_release)
.on_capture_global_pointer_up(on_global_release)
.on_global_key_down(move |e: Event<KeyboardEventData>| {
let held = e.modifiers.contains(Modifiers::SHIFT)
|| matches!(e.code, Code::ShiftLeft | Code::ShiftRight);

View file

@ -252,7 +252,7 @@ impl Component for AnimatedAppOutlet {
.height(Size::fill())
.overflow(Overflow::Clip)
.on_global_key_down(on_global_key)
.on_global_pointer_press(on_global_pointer)
.on_global_pointer_up(on_global_pointer)
.maybe_child(overlay)
.child(column)
}

View file

@ -355,7 +355,7 @@ impl Component for PackageBanner {
.position(Position::new_absolute().top(0.).left(0.))
.width(Size::fill())
.height(Size::fill())
.blur(12.)
.backdrop_blur(12.)
.background(BANNER_BG.with_a(120))
.overflow(Overflow::Clip)
.layer(Layer::Relative(3)),

View file

@ -169,7 +169,7 @@ pub(super) fn toolbar_bar(
.direction(Direction::Horizontal)
.invert_scroll_wheel(true)
.show_scrollbar(true)
.scrollbar_theme(tabs_scrollbar_theme())
.scrollbar(|context| ScrollBar::new(context).theme(tabs_scrollbar_theme()).into())
.width(if stacked {
Size::fill()
} else {
@ -495,7 +495,7 @@ impl Component for FilterPopover {
.offset_y((progress - 1.0) * 6.)
.corner_radius(CornerRadius::new_all(10.))
.background(colors::page_elevated().with_a(230))
.blur(12.)
.backdrop_blur(12.)
.border(crate::ui::border_all_color(1., colors::component_border()))
.shadow(Shadow::from((
0.,

View file

@ -1,3 +1,4 @@
#![recursion_limit = "256"]
pub mod bundles;
pub mod packages;

View file

@ -1,8 +1,6 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use futures_lite::AsyncReadExt;
use oneclient_db::models::ClusterRow;
use serde::Deserialize;
use uuid::Uuid;
@ -51,23 +49,19 @@ impl MrpackInstaller {
let cluster = PackageStore::get_cluster(cluster_id, ctx).await?;
let cluster_root = oneclient_common::paths::clusters_dir()?.join(&cluster.folder_name);
let bytes = Arc::new(polyio::read(archive_path).await?);
let mut manifest_bytes: Option<Vec<u8>> = None;
polyio::read_zip_entries_bytes(bytes.to_vec(), async |_, entry, reader| {
let entry_name = entry.filename().as_str().map_err(io_err)?;
if entry_name == "modrinth.index.json" {
let mut buf = Vec::new();
reader.read_to_end(&mut buf).await.map_err(io_err)?;
manifest_bytes = Some(buf);
let file = tokio::fs::File::open(&archive_path).await?;
let manifest_bytes = match polyio::try_read_zip_entry_bytes(
tokio::io::BufReader::new(file),
"modrinth.index.json",
)
.await
{
Err(polyio::IOError::FileNotFoundInZip { .. }) => {
return Err(PackageError::UnsupportedModpackFormat.into());
}
Ok(())
})
.await?;
result => result?,
};
let manifest_bytes = manifest_bytes.ok_or(PackageError::UnsupportedModpackFormat)?;
let manifest: MrpackManifest = serde_json::from_slice(&manifest_bytes)?;
let mut failed = 0u64;
@ -99,26 +93,15 @@ impl MrpackInstaller {
ctx.events
.progress(progress_id, "Installing Modpack Files", total, total);
polyio::read_zip_entries_bytes(bytes.to_vec(), async |_, entry, reader| {
let name = entry.filename().as_str().map_err(io_err)?;
let Some(rest) = name.strip_prefix("overrides/") else {
return Ok(());
};
if rest.is_empty() || name.ends_with('/') {
return Ok(());
}
let dest = cluster_root.join(rest);
if let Some(parent) = dest.parent() {
polyio::create_dir_all(parent).await.map_err(io_err)?;
}
let mut file_bytes = Vec::new();
reader.read_to_end(&mut file_bytes).await.map_err(io_err)?;
polyio::write(&dest, &file_bytes).await.map_err(io_err)?;
Ok(())
})
polyio::extract_zip_filtered(
&archive_path,
&cluster_root,
Some(|name: &str| {
name.strip_prefix("overrides/")
.is_some_and(|rest| !rest.is_empty())
}),
Some(|name: &str| name.strip_prefix("overrides/").unwrap_or(name).to_string()),
)
.await?;
if failed > 0 {
@ -183,13 +166,6 @@ fn content_type_from_path(path: &str) -> ContentType {
ContentType::from_folder_name(top).unwrap_or(ContentType::Mod)
}
fn io_err(err: impl std::error::Error + Send + Sync + 'static) -> polyio::IOError {
polyio::IOError::PathIOError {
source: std::io::Error::other(err),
path: String::new(),
}
}
#[tracing::instrument(level = "debug", skip(ctx))]
pub async fn install_mrpack_to_cluster(
archive_path: PathBuf,

View file

@ -17,7 +17,7 @@ path = "src/lib.rs"
default = []
[dev-dependencies]
async-tungstenite = {version = "=0.32.1", features = ["tokio-runtime"]}
async-tungstenite = {version = "=0.35.0", features = ["tokio-runtime"]}
polyio = {workspace = true, features = ["testing"]}
[dependencies]

View file

@ -1,5 +1,4 @@
use std::panic;
use std::sync::Arc;
use std::time::Duration;
use sentry::protocol::{Breadcrumb, Context, Event};
@ -59,15 +58,13 @@ pub fn init(enabled: bool) -> Option<ClientInitGuard> {
let guard = sentry::init((
SENTRY_DSN,
ClientOptions {
release: Some(format!("oneclient@{}", env!("CARGO_PKG_VERSION")).into()),
environment: Some(ENVIRONMENT.into()),
attach_stacktrace: true,
send_default_pii: false,
before_send: Some(Arc::new(drop_opted_out_events)),
before_breadcrumb: Some(Arc::new(drop_opted_out_breadcrumbs)),
..Default::default()
},
ClientOptions::new()
.release(format!("oneclient@{}", env!("CARGO_PKG_VERSION")))
.environment(ENVIRONMENT)
.attach_stacktrace(true)
.send_default_pii(false)
.before_send(drop_opted_out_events)
.before_breadcrumb(drop_opted_out_breadcrumbs),
));
if !guard.is_enabled() {

View file

@ -276,45 +276,22 @@ pub fn processor_arguments<T: AsRef<str>, S: std::hash::BuildHasher>(
#[tracing::instrument(skip_all, level = "debug")]
pub async fn main_class(path: impl AsRef<std::path::Path>) -> McResult<Option<String>> {
let data = polyio::read(path.as_ref()).await?;
let mut class_name = None;
let file = tokio::fs::File::open(path.as_ref()).await?;
let manifest = match polyio::try_read_zip_entry_bytes(
tokio::io::BufReader::new(file),
"META-INF/MANIFEST.MF",
)
.await
{
Err(polyio::IOError::FileNotFoundInZip { .. }) => return Ok(None),
result => result?,
};
use futures_util::TryStreamExt;
let stream = polyio::stream_zip_entries_bytes(data);
let mut stream = std::pin::pin!(stream);
while let Some(item) = stream.try_next().await? {
let (index, entry, reader) = item;
if entry.dir().map_err(polyio::IOError::from)? {
continue;
}
if entry.filename().as_str().map_err(polyio::IOError::from)? != "META-INF/MANIFEST.MF" {
continue;
}
let mut buf = String::new();
let mut entry_reader = reader
.reader_without_entry(index)
.await
.map_err(polyio::IOError::from)?;
futures_util::AsyncReadExt::read_to_string(&mut entry_reader, &mut buf)
.await
.map_err(polyio::IOError::from)?;
for line in buf.lines() {
let line = line.trim();
if line.starts_with("Main-Class:")
&& let Some(class) = line.split(':').nth(1)
{
class_name = Some(class.trim().to_string());
break;
}
}
}
Ok(class_name)
Ok(String::from_utf8_lossy(&manifest).lines().find_map(|line| {
line.trim()
.strip_prefix("Main-Class:")
.map(|class| class.trim().to_string())
}))
}
#[allow(clippy::too_many_arguments)]

View file

@ -34,5 +34,5 @@ url.workspace = true
uuid.workspace = true
[dev-dependencies]
async-tungstenite = { version = "=0.32.1", features = ["tokio-runtime"] }
async-tungstenite = { version = "=0.35.0", features = ["tokio-runtime"] }
tokio = { workspace = true }

View file

@ -29,7 +29,6 @@ futures-lite.workspace = true
async-tempfile.workspace = true
async-compression.workspace = true
async-stream.workspace = true
async_zip.workspace = true
astral-tokio-tar.workspace = true
sanitize-filename.workspace = true

View file

@ -1,65 +1,33 @@
use std::sync::Arc;
use std::borrow::Cow;
use std::path::Path;
use async_stream::try_stream;
use async_zip::StoredZipEntry;
use async_zip::base::read::WithoutEntry;
use futures_util::{Stream, TryStreamExt, pin_mut};
use tokio_util::compat::TokioAsyncWriteCompatExt;
use async_zip::base::read1::ZipOptions;
use async_zip::base::read1::seek::ZipArchiveReader;
use async_zip::spec::constructs::CDR;
use futures_lite::{AsyncBufRead, AsyncSeek};
use tokio_util::compat::{Compat, TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use crate::{IOError, PolyIOResult};
#[tracing::instrument(skip(data, f), level = "debug")]
pub async fn read_zip_entries_bytes<F>(data: Vec<u8>, mut f: F) -> PolyIOResult<()>
where
F: AsyncFnMut(
usize,
&StoredZipEntry,
&mut async_zip::base::read::ZipEntryReader<
'_,
futures_lite::io::Cursor<&[u8]>,
WithoutEntry,
>,
) -> PolyIOResult<()>,
{
let reader = async_zip::base::read::mem::ZipFileReader::new(data).await?;
type FileZipReader = ZipArchiveReader<Compat<tokio::io::BufReader<tokio::fs::File>>>;
let entries = reader.file().entries();
for index in 0..entries.len() {
let entry = entries.get(index).expect("expected more zip entries");
let mut entry_reader = reader
.reader_without_entry(index)
.await
.unwrap_or_else(|_| panic!("expected zip entry at index '{index}'"));
f(index, entry, &mut entry_reader).await?;
}
Ok(())
async fn open_zip<R: AsyncBufRead + AsyncSeek + Unpin>(
reader: R,
) -> PolyIOResult<ZipArchiveReader<R>> {
Ok(ZipArchiveReader::open_with_options(reader, ZipOptions::untrusted()).await?)
}
#[tracing::instrument(skip(data), level = "debug")]
pub fn stream_zip_entries_bytes(
data: Vec<u8>,
) -> impl Stream<
Item = Result<
(
usize,
StoredZipEntry,
Arc<async_zip::base::read::mem::ZipFileReader>,
),
IOError,
>,
> {
try_stream! {
let reader = Arc::new(async_zip::base::read::mem::ZipFileReader::new(data).await?);
let entries = reader.file().entries();
async fn open_zip_file(zip_path: &Path) -> PolyIOResult<FileZipReader> {
let file = tokio::fs::File::open(zip_path).await?;
open_zip(tokio::io::BufReader::new(file).compat()).await
}
for index in 0..entries.len() {
let entry = entries.get(index).expect("expected more zip entries");
fn entry_name(cdr: &CDR) -> Cow<'_, str> {
String::from_utf8_lossy(cdr.insecure_file_name.as_bytes())
}
yield (index, entry.clone(), reader.clone());
}
}
fn unix_permissions(cdr: &CDR) -> Option<u32> {
(cdr.cdrh.v_made_by >> 8 == 3).then_some(cdr.cdrh.exter_attr >> 16)
}
#[tracing::instrument(
@ -88,39 +56,15 @@ pub async fn unzip_bytes_filtered(
filter_entries: Option<impl Fn(&str) -> bool + Send + Sync>,
dest_path: impl AsRef<std::path::Path>,
) -> PolyIOResult<()> {
let stream = stream_zip_entries_bytes(data);
pin_mut!(stream);
let reader = open_zip(futures_lite::io::Cursor::new(data)).await?;
while let Some((index, entry, reader)) = stream.try_next().await? {
let file_name = entry.filename().as_str()?;
if let Some(filter) = &filter_entries
&& !filter(file_name)
{
continue;
}
let path = dest_path.as_ref().join(crate::sanitize_path(file_name));
let is_dir = entry.dir()?;
if is_dir {
crate::create_dir_all(path).await?;
} else {
if let Some(parent) = path.parent() {
crate::create_dir_all(parent).await?;
}
let entry_reader = reader.reader_without_entry(index).await?;
let file = tokio::fs::File::create(&path).await?;
let writer = tokio::io::BufWriter::new(file);
futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?;
}
}
Ok(())
extract_entries(
reader,
dest_path.as_ref(),
filter_entries,
None::<fn(&str) -> String>,
)
.await
}
#[tracing::instrument(
@ -142,7 +86,7 @@ pub async fn extract_zip(
extract_zip_filtered(
zip_path,
dest_path,
None::<fn(&StoredZipEntry) -> bool>,
None::<fn(&str) -> bool>,
None::<fn(&str) -> String>,
)
.await
@ -176,35 +120,44 @@ where
pub async fn extract_zip_filtered(
zip_path: impl AsRef<std::path::Path>,
dest_path: impl AsRef<std::path::Path>,
filter_entries: Option<impl Fn(&StoredZipEntry) -> bool + Send + Sync>,
filter_entries: Option<impl Fn(&str) -> bool + Send + Sync>,
modify_entry_name: Option<impl Fn(&str) -> String>,
) -> PolyIOResult<()> {
let zip_path = zip_path.as_ref();
let dest_path = dest_path.as_ref();
let reader = open_zip_file(zip_path.as_ref()).await?;
let reader = async_zip::tokio::read::fs::ZipFileReader::new(zip_path).await?;
let entries = reader.file().entries();
extract_entries(
reader,
dest_path.as_ref(),
filter_entries,
modify_entry_name,
)
.await
}
for index in 0..entries.len() {
let entry = entries.get(index).expect("expected more zip entries");
async fn extract_entries<R: AsyncBufRead + AsyncSeek + Unpin>(
mut reader: ZipArchiveReader<R>,
dest_path: &Path,
filter_entries: Option<impl Fn(&str) -> bool>,
modify_entry_name: Option<impl Fn(&str) -> String>,
) -> PolyIOResult<()> {
let inner = reader.inner().clone();
for (index, cdr) in inner.cdrs().iter().enumerate() {
let old_name = entry_name(cdr);
if let Some(filter) = &filter_entries
&& !filter(entry)
&& !filter(&old_name)
{
continue;
}
let old_name = entry.filename().as_str()?;
let name: String = modify_entry_name
.as_ref()
.map_or_else(|| old_name.to_string(), |modify| modify(old_name));
.map_or_else(|| old_name.to_string(), |modify| modify(&old_name));
let name = crate::sanitize_path(name);
let path = dest_path.join(crate::sanitize_path(name));
let path = dest_path.join(name);
let is_dir = entry.dir()?;
if is_dir {
if old_name.ends_with('/') {
crate::create_dir_all(&path).await?;
} else {
if let Some(parent) = path.parent() {
@ -213,15 +166,15 @@ pub async fn extract_zip_filtered(
let file = tokio::fs::File::create(&path).await?;
let writer = tokio::io::BufWriter::new(file);
let entry_reader = reader.reader_without_entry(index).await?;
let entry_reader = reader.file(index).await?;
futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?;
#[cfg(unix)]
if let Some(mode) = entry.unix_permissions().filter(|mode| mode & 0o777 != 0) {
if let Some(mode) = unix_permissions(cdr).filter(|mode| mode & 0o777 != 0) {
use std::os::unix::fs::PermissionsExt;
let mode = u32::from(mode & 0o777) | 0o600;
let mode = (mode & 0o777) | 0o600;
tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).await?;
}
}
@ -248,31 +201,30 @@ pub async fn read_zip_file_entries(
filter: impl Fn(&str) -> bool,
) -> PolyIOResult<Vec<(String, Vec<u8>)>> {
let zip_path = zip_path.as_ref();
let reader = async_zip::tokio::read::fs::ZipFileReader::new(zip_path).await?;
let entries = reader.file().entries();
let mut reader = open_zip_file(zip_path).await?;
let inner = reader.inner().clone();
let mut out = Vec::new();
for index in 0..entries.len() {
let entry = entries.get(index).expect("expected more zip entries");
let Ok(name) = entry.filename().as_str() else {
for (index, cdr) in inner.cdrs().iter().enumerate() {
let Some(name) = cdr.insecure_file_name.as_str() else {
continue;
};
let name = name.to_string();
if entry.dir().unwrap_or(false) || !filter(&name) {
if name.ends_with('/') || !filter(&name) {
continue;
}
if entry.uncompressed_size() > MAX_ZIP_ENTRY_BYTES {
let uncompressed_size = cdr.uncompressed_size()?;
if uncompressed_size > MAX_ZIP_ENTRY_BYTES {
tracing::warn!(
"skipping '{name}' in {}: declares {} bytes over the {MAX_ZIP_ENTRY_BYTES} byte entry cap",
"skipping '{name}' in {}: declares {uncompressed_size} bytes over the {MAX_ZIP_ENTRY_BYTES} byte entry cap",
zip_path.display(),
entry.uncompressed_size()
);
continue;
}
let entry_reader = reader.reader_without_entry(index).await?;
let entry_reader = reader.file(index).await?;
let mut data = Vec::new();
futures_lite::AsyncReadExt::read_to_end(
&mut futures_lite::AsyncReadExt::take(entry_reader, MAX_ZIP_ENTRY_BYTES + 1),
@ -300,13 +252,12 @@ pub async fn read_zip_file_entries(
fields(zip_path = %zip_path.as_ref().display())
)]
pub async fn zip_entry_names(zip_path: impl AsRef<std::path::Path>) -> PolyIOResult<Vec<String>> {
let reader = async_zip::tokio::read::fs::ZipFileReader::new(zip_path.as_ref()).await?;
let reader = open_zip_file(zip_path.as_ref()).await?;
Ok(reader
.file()
.entries()
.cdrs()
.iter()
.filter_map(|entry| entry.filename().as_str().ok().map(ToString::to_string))
.filter_map(|cdr| cdr.insecure_file_name.as_str().map(ToString::to_string))
.collect())
}
@ -316,21 +267,16 @@ pub async fn try_read_zip_entry_bytes<R>(reader: R, file_name: &str) -> PolyIORe
where
R: tokio::io::AsyncRead + tokio::io::AsyncBufRead + tokio::io::AsyncSeek + Unpin,
{
use tokio_util::compat::TokioAsyncReadCompatExt;
let compat = reader.compat();
let mut zip_reader = async_zip::base::read::seek::ZipFileReader::new(compat).await?;
let mut zip_reader = open_zip(reader.compat()).await?;
let index = zip_reader
.file()
.entries()
.iter()
.position(|entry| entry.filename().as_str().is_ok_and(|n| n == file_name))
.find(file_name.as_bytes())?
.next()
.ok_or_else(|| IOError::FileNotFoundInZip {
file_name: file_name.to_string(),
})?;
let mut entry_reader = zip_reader.reader_without_entry(index).await?;
let mut entry_reader = zip_reader.file(index).await?;
let mut data: Vec<u8> = Vec::new();