diff --git a/apps/recorder/.github/workflows/ci.yaml b/.github.bk/workflows/ci.yaml similarity index 100% rename from apps/recorder/.github/workflows/ci.yaml rename to .github.bk/workflows/ci.yaml diff --git a/Cargo.lock b/Cargo.lock index cef874d..5af41cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,13 +14,19 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.0" @@ -534,17 +540,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", - "windows-targets 0.52.6", ] [[package]] @@ -870,6 +876,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cfg_rust_features" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e2a7913e334891bf80b781c61a6cafce022f6f021d81151f8d11edc9697d72b" +dependencies = [ + "autocfg", + "version_check", +] + [[package]] name = "chrono" version = "0.4.39" @@ -957,6 +973,33 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1744,7 +1787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.2", ] [[package]] @@ -1978,9 +2021,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -3445,6 +3488,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -3649,9 +3701,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -3825,6 +3877,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parcel_selectors" version = "0.28.0" @@ -4442,12 +4500,13 @@ dependencies = [ [[package]] name = "quirks_path" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6437134ea524dd9b37c6ce5bebb35721607f64aa50f71615f8f5a68081162a0" dependencies = [ + "cfg_rust_features", "nom", "percent-encoding", - "serde", - "thiserror 1.0.69", - "tracing", + "thiserror 2.0.9", "url", ] @@ -4544,7 +4603,7 @@ dependencies = [ "bollard", "bytes", "chrono", - "eyre", + "color-eyre", "fancy-regex", "fastrand", "figment", @@ -6725,6 +6784,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 859e345..dbb8ae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["apps/recorder", "packages/quirks-path"] +members = ["apps/recorder"] resolver = "2" [patch.crates-io] diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index 3bf76e3..4268ea2 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -22,11 +22,9 @@ testcontainers = [ ] [dependencies] -quirks_path = { path = "../../packages/quirks-path" } loco-rs = { version = "0.13" } serde = { version = "1", features = ["derive"] } serde_json = "1" -eyre = "0.6" tokio = { version = "1.42", features = ["macros", "fs", "rt-multi-thread"] } async-trait = "0.1.83" tracing = "0.1" @@ -82,6 +80,9 @@ testcontainers = { version = "0.23.1", features = [ ], optional = true } testcontainers-modules = { version = "0.11.4", optional = true } +color-eyre = "0.6" + + log = "0.4.22" anyhow = "1.0.95" bollard = { version = "0.18", optional = true } @@ -89,7 +90,7 @@ async-graphql = { version = "7.0.13", features = [] } async-graphql-axum = "7.0.13" fastrand = "2.3.0" seaography = "1.1.2" - +quirks_path = "0.1.0" [dev-dependencies] serial_test = "3" diff --git a/apps/recorder/examples/playground.rs b/apps/recorder/examples/playground.rs index 9e12319..d1e5ad6 100644 --- a/apps/recorder/examples/playground.rs +++ b/apps/recorder/examples/playground.rs @@ -1,5 +1,5 @@ #![allow(unused_imports)] -use eyre::Context; +use color_eyre::eyre::Context; use itertools::Itertools; use loco_rs::{ app::Hooks, @@ -18,7 +18,8 @@ use recorder::{ }; use sea_orm_migration::MigratorTrait; -async fn pull_mikan_bangumi_rss(ctx: &AppContext) -> eyre::Result<()> { +async fn pull_mikan_bangumi_rss(ctx: &AppContext) -> color_eyre::eyre::Result<()> { + color_eyre::install()?; let rss_link = "https://mikanani.me/RSS/Bangumi?bangumiId=3416&subgroupid=370"; // let rss_link = @@ -47,7 +48,7 @@ async fn pull_mikan_bangumi_rss(ctx: &AppContext) -> eyre::Result<()> { Ok(()) } -async fn init() -> eyre::Result { +async fn init() -> color_eyre::eyre::Result { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .with_test_writer() @@ -61,7 +62,7 @@ async fn init() -> eyre::Result { } #[tokio::main] -async fn main() -> eyre::Result<()> { +async fn main() -> color_eyre::eyre::Result<()> { let ctx = init().await?; pull_mikan_bangumi_rss(&ctx).await?; diff --git a/apps/recorder/src/bin/main.rs b/apps/recorder/src/bin/main.rs index 5983ff0..a963aef 100644 --- a/apps/recorder/src/bin/main.rs +++ b/apps/recorder/src/bin/main.rs @@ -2,7 +2,8 @@ use loco_rs::cli; use recorder::{app::App, migrations::Migrator}; #[tokio::main] -async fn main() -> eyre::Result<()> { +async fn main() -> color_eyre::eyre::Result<()> { + color_eyre::install()?; cli::main::().await?; Ok(()) } diff --git a/apps/recorder/src/dal/client.rs b/apps/recorder/src/dal/client.rs index 4985c3f..9c54544 100644 --- a/apps/recorder/src/dal/client.rs +++ b/apps/recorder/src/dal/client.rs @@ -87,7 +87,7 @@ impl AppDalClient { bucket: Option<&str>, filename: &str, data: Bytes, - ) -> eyre::Result { + ) -> color_eyre::eyre::Result { match content_category { DalContentCategory::Image => { let fullname = [ @@ -124,7 +124,7 @@ impl AppDalClient { subscriber_pid: &str, bucket: Option<&str>, filename: &str, - ) -> eyre::Result> { + ) -> color_eyre::eyre::Result> { match content_category { DalContentCategory::Image => { let fullname = [ @@ -158,7 +158,7 @@ impl AppDalClient { subscriber_pid: &str, bucket: Option<&str>, filename: &str, - ) -> eyre::Result { + ) -> color_eyre::eyre::Result { match content_category { DalContentCategory::Image => { let fullname = [ diff --git a/apps/recorder/src/extract/mikan/rss_parser.rs b/apps/recorder/src/extract/mikan/rss_parser.rs index 13513af..a047d8e 100644 --- a/apps/recorder/src/extract/mikan/rss_parser.rs +++ b/apps/recorder/src/extract/mikan/rss_parser.rs @@ -166,7 +166,7 @@ pub fn build_mikan_bangumi_rss_link( mikan_base_url: &str, mikan_bangumi_id: &str, mikan_fansub_id: Option<&str>, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let mut url = Url::parse(mikan_base_url)?; url.set_path("/RSS/Bangumi"); url.query_pairs_mut() @@ -181,7 +181,7 @@ pub fn build_mikan_bangumi_rss_link( pub fn build_mikan_subscriber_aggregation_rss_link( mikan_base_url: &str, mikan_aggregation_id: &str, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let mut url = Url::parse(mikan_base_url)?; url.set_path("/RSS/MyBangumi"); url.query_pairs_mut() @@ -222,7 +222,7 @@ pub fn parse_mikan_subscriber_aggregation_id_from_rss_link( pub async fn parse_mikan_rss_items_from_rss_link( client: Option<&AppMikanClient>, url: impl IntoUrl, -) -> eyre::Result> { +) -> color_eyre::eyre::Result> { let channel = parse_mikan_rss_channel_from_rss_link(client, url).await?; Ok(channel.into_items()) @@ -231,7 +231,7 @@ pub async fn parse_mikan_rss_items_from_rss_link( pub async fn parse_mikan_rss_channel_from_rss_link( client: Option<&AppMikanClient>, url: impl IntoUrl, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let http_client = client.map(|s| s.deref()); let bytes = fetch_bytes(http_client, url.as_str()).await?; diff --git a/apps/recorder/src/extract/mikan/web_parser.rs b/apps/recorder/src/extract/mikan/web_parser.rs index a61c54a..677f27b 100644 --- a/apps/recorder/src/extract/mikan/web_parser.rs +++ b/apps/recorder/src/extract/mikan/web_parser.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use bytes::Bytes; -use eyre::ContextCompat; +use color_eyre::eyre::ContextCompat; use html_escape::decode_html_entities; use itertools::Itertools; use lazy_static::lazy_static; @@ -65,7 +65,7 @@ pub fn build_mikan_bangumi_homepage( mikan_base_url: &str, mikan_bangumi_id: &str, mikan_fansub_id: Option<&str>, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let mut url = Url::parse(mikan_base_url)?; url.set_path(&format!("/Home/Bangumi/{mikan_bangumi_id}")); url.set_fragment(mikan_fansub_id); @@ -75,7 +75,7 @@ pub fn build_mikan_bangumi_homepage( pub fn build_mikan_episode_homepage( mikan_base_url: &str, mikan_episode_id: &str, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let mut url = Url::parse(mikan_base_url)?; url.set_path(&format!("/Home/Episode/{mikan_episode_id}")); Ok(url) @@ -93,7 +93,7 @@ pub fn parse_mikan_episode_id_from_homepage(url: &Url) -> Option, origin_poster_src: Url, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let http_client = client.map(|s| s.deref()); let poster_data = fetch_image(http_client, origin_poster_src.clone()).await?; Ok(MikanBangumiPosterMeta { @@ -107,7 +107,7 @@ pub async fn parse_mikan_bangumi_poster_from_origin_poster_src_with_cache( ctx: &AppContext, origin_poster_src: Url, subscriber_id: i32, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let dal_client = ctx.get_dal_client(); let mikan_client = ctx.get_mikan_client(); let subscriber_pid = &subscribers::Model::find_pid_by_id_with_cache(ctx, subscriber_id).await?; @@ -149,7 +149,7 @@ pub async fn parse_mikan_bangumi_poster_from_origin_poster_src_with_cache( pub async fn parse_mikan_bangumi_meta_from_mikan_homepage( client: Option<&AppMikanClient>, url: Url, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let http_client = client.map(|s| s.deref()); let url_host = url.origin().unicode_serialization(); let content = fetch_html(http_client, url.as_str()).await?; @@ -272,7 +272,7 @@ pub async fn parse_mikan_bangumi_meta_from_mikan_homepage( pub async fn parse_mikan_episode_meta_from_mikan_homepage( client: Option<&AppMikanClient>, url: Url, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let http_client = client.map(|s| s.deref()); let url_host = url.origin().unicode_serialization(); let content = fetch_html(http_client, url.as_str()).await?; @@ -417,7 +417,7 @@ mod test { #[tokio::test] async fn test_parse_mikan_episode() { - let test_fn = async || -> eyre::Result<()> { + let test_fn = async || -> color_eyre::eyre::Result<()> { let url_str = "https://mikanani.me/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a"; let url = Url::parse(url_str)?; @@ -461,7 +461,7 @@ mod test { #[tokio::test] async fn test_parse_mikan_bangumi() { - let test_fn = async || -> eyre::Result<()> { + let test_fn = async || -> color_eyre::eyre::Result<()> { let url_str = "https://mikanani.me/Home/Bangumi/3416#370"; let url = Url::parse(url_str)?; diff --git a/apps/recorder/src/extract/rawname/parser.rs b/apps/recorder/src/extract/rawname/parser.rs index ef597d0..316a27b 100644 --- a/apps/recorder/src/extract/rawname/parser.rs +++ b/apps/recorder/src/extract/rawname/parser.rs @@ -68,7 +68,10 @@ fn replace_ch_bracket_to_en(raw_name: &str) -> String { raw_name.replace('【', "[").replace('】', "]") } -fn title_body_pre_process(title_body: &str, fansub: Option<&str>) -> eyre::Result { +fn title_body_pre_process( + title_body: &str, + fansub: Option<&str>, +) -> color_eyre::eyre::Result { let raw_without_fansub = if let Some(fansub) = fansub { let fan_sub_re = Regex::new(&format!(".{fansub}."))?; fan_sub_re.replace_all(title_body, "") @@ -256,7 +259,7 @@ pub fn check_is_movie(title: &str) -> bool { MOVIE_TITLE_RE.is_match(title) } -pub fn parse_episode_meta_from_raw_name(s: &str) -> eyre::Result { +pub fn parse_episode_meta_from_raw_name(s: &str) -> color_eyre::eyre::Result { let raw_title = s.trim(); let raw_title_without_ch_brackets = replace_ch_bracket_to_en(raw_title); let fansub = extract_fansub(&raw_title_without_ch_brackets); @@ -309,7 +312,7 @@ pub fn parse_episode_meta_from_raw_name(s: &str) -> eyre::Result resolution, }) } else { - Err(eyre::eyre!( + Err(color_eyre::eyre::eyre!( "Can not parse episode meta from raw filename {}", raw_title )) diff --git a/apps/recorder/src/extract/torrent/parser.rs b/apps/recorder/src/extract/torrent/parser.rs index 371c7fa..ec71952 100644 --- a/apps/recorder/src/extract/torrent/parser.rs +++ b/apps/recorder/src/extract/torrent/parser.rs @@ -1,4 +1,4 @@ -use eyre::OptionExt; +use color_eyre::eyre::OptionExt; use fancy_regex::Regex as FancyRegex; use lazy_static::lazy_static; use quirks_path::Path; @@ -101,10 +101,10 @@ pub fn parse_episode_media_meta_from_torrent( torrent_path: &Path, torrent_name: Option<&str>, season: Option, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let media_name = torrent_path .file_name() - .ok_or_else(|| eyre::eyre!("failed to get file name of {}", torrent_path))?; + .ok_or_else(|| color_eyre::eyre::eyre!("failed to get file name of {}", torrent_path))?; let mut match_obj = None; for rule in TORRENT_EP_PARSE_RULES.iter() { match_obj = if let Some(torrent_name) = torrent_name.as_ref() { @@ -119,7 +119,7 @@ pub fn parse_episode_media_meta_from_torrent( if let Some(match_obj) = match_obj { let group_season_and_title = match_obj .get(1) - .ok_or_else(|| eyre::eyre!("should have 1 group"))? + .ok_or_else(|| color_eyre::eyre::eyre!("should have 1 group"))? .as_str(); let (fansub, season_and_title) = get_fansub(group_season_and_title); let (title, season) = if let Some(season) = season { @@ -146,7 +146,7 @@ pub fn parse_episode_media_meta_from_torrent( extname, }) } else { - Err(eyre::eyre!( + Err(color_eyre::eyre::eyre!( "failed to parse episode media meta from torrent_path='{}' torrent_name='{:?}'", torrent_path, torrent_name @@ -158,11 +158,11 @@ pub fn parse_episode_subtitle_meta_from_torrent( torrent_path: &Path, torrent_name: Option<&str>, season: Option, -) -> eyre::Result { +) -> color_eyre::eyre::Result { let media_meta = parse_episode_media_meta_from_torrent(torrent_path, torrent_name, season)?; let media_name = torrent_path .file_name() - .ok_or_else(|| eyre::eyre!("failed to get file name of {}", torrent_path))?; + .ok_or_else(|| color_eyre::eyre::eyre!("failed to get file name of {}", torrent_path))?; let lang = get_subtitle_lang(media_name); diff --git a/apps/recorder/src/fetch/bytes.rs b/apps/recorder/src/fetch/bytes.rs index c54939d..25b98e8 100644 --- a/apps/recorder/src/fetch/bytes.rs +++ b/apps/recorder/src/fetch/bytes.rs @@ -3,7 +3,7 @@ use reqwest::IntoUrl; use super::HttpClient; -pub async fn fetch_bytes(client: Option<&HttpClient>, url: T) -> eyre::Result { +pub async fn fetch_bytes(client: Option<&HttpClient>, url: T) -> color_eyre::eyre::Result { let client = client.unwrap_or_default(); let bytes = client diff --git a/apps/recorder/src/fetch/html.rs b/apps/recorder/src/fetch/html.rs index d160445..430f0bb 100644 --- a/apps/recorder/src/fetch/html.rs +++ b/apps/recorder/src/fetch/html.rs @@ -2,7 +2,10 @@ use reqwest::IntoUrl; use super::HttpClient; -pub async fn fetch_html(client: Option<&HttpClient>, url: T) -> eyre::Result { +pub async fn fetch_html( + client: Option<&HttpClient>, + url: T, +) -> color_eyre::eyre::Result { let client = client.unwrap_or_default(); let content = client .get(url) diff --git a/apps/recorder/src/fetch/image.rs b/apps/recorder/src/fetch/image.rs index 1b5bb0e..e30afad 100644 --- a/apps/recorder/src/fetch/image.rs +++ b/apps/recorder/src/fetch/image.rs @@ -3,6 +3,6 @@ use reqwest::IntoUrl; use super::{bytes::fetch_bytes, HttpClient}; -pub async fn fetch_image(client: Option<&HttpClient>, url: T) -> eyre::Result { +pub async fn fetch_image(client: Option<&HttpClient>, url: T) -> color_eyre::eyre::Result { fetch_bytes(client, url).await } diff --git a/apps/recorder/src/models/bangumi.rs b/apps/recorder/src/models/bangumi.rs index 9668000..af4230a 100644 --- a/apps/recorder/src/models/bangumi.rs +++ b/apps/recorder/src/models/bangumi.rs @@ -112,9 +112,9 @@ impl Model { mikan_bangumi_id: String, mikan_fansub_id: String, f: F, - ) -> eyre::Result + ) -> color_eyre::eyre::Result where - F: AsyncFnOnce(&mut ActiveModel) -> eyre::Result<()>, + F: AsyncFnOnce(&mut ActiveModel) -> color_eyre::eyre::Result<()>, { let db = &ctx.db; if let Some(existed) = Entity::find() diff --git a/apps/recorder/src/models/episodes.rs b/apps/recorder/src/models/episodes.rs index 27a4b63..06c2ee4 100644 --- a/apps/recorder/src/models/episodes.rs +++ b/apps/recorder/src/models/episodes.rs @@ -137,7 +137,7 @@ impl Model { ctx: &AppContext, subscription_id: i32, creations: impl IntoIterator, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { let db = &ctx.db; let new_episode_active_modes = creations .into_iter() @@ -187,7 +187,7 @@ impl ActiveModel { pub fn from_mikan_episode_meta( ctx: &AppContext, creation: MikanEpsiodeCreation, - ) -> eyre::Result { + ) -> color_eyre::eyre::Result { let item = creation.episode; let bgm = creation.bangumi; let raw_meta = parse_episode_meta_from_raw_name(&item.episode_title) diff --git a/apps/recorder/src/models/subscribers.rs b/apps/recorder/src/models/subscribers.rs index a6c0570..6e41f22 100644 --- a/apps/recorder/src/models/subscribers.rs +++ b/apps/recorder/src/models/subscribers.rs @@ -128,7 +128,10 @@ impl Model { subscriber.ok_or_else(|| ModelError::EntityNotFound) } - pub async fn find_pid_by_id_with_cache(ctx: &AppContext, id: i32) -> eyre::Result { + pub async fn find_pid_by_id_with_cache( + ctx: &AppContext, + id: i32, + ) -> color_eyre::eyre::Result { let db = &ctx.db; let cache = &ctx.cache; let pid = cache diff --git a/apps/recorder/src/models/subscriptions.rs b/apps/recorder/src/models/subscriptions.rs index 718f867..11e0b4e 100644 --- a/apps/recorder/src/models/subscriptions.rs +++ b/apps/recorder/src/models/subscriptions.rs @@ -182,7 +182,7 @@ impl Model { ctx: &AppContext, create_dto: SubscriptionCreateDto, subscriber_id: i32, - ) -> eyre::Result { + ) -> color_eyre::eyre::Result { let db = &ctx.db; let subscription = ActiveModel::from_create_dto(create_dto, subscriber_id); @@ -193,7 +193,7 @@ impl Model { ctx: &AppContext, ids: impl Iterator, enabled: bool, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { let db = &ctx.db; Entity::update_many() .col_expr(Column::Enabled, Expr::value(enabled)) @@ -206,7 +206,7 @@ impl Model { pub async fn delete_with_ids( ctx: &AppContext, ids: impl Iterator, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { let db = &ctx.db; Entity::delete_many() .filter(Column::Id.is_in(ids)) @@ -215,7 +215,7 @@ impl Model { Ok(()) } - pub async fn pull_subscription(&self, ctx: &AppContext) -> eyre::Result<()> { + pub async fn pull_subscription(&self, ctx: &AppContext) -> color_eyre::eyre::Result<()> { match &self.category { SubscriptionCategory::Mikan => { let mikan_client = ctx.get_mikan_client(); @@ -288,7 +288,7 @@ impl Model { self.id, mikan_bangumi_id.to_string(), mikan_fansub_id.to_string(), - async |am| -> eyre::Result<()> { + async |am| -> color_eyre::eyre::Result<()> { let bgm_meta = parse_mikan_bangumi_meta_from_mikan_homepage( Some(mikan_client), bgm_homepage.clone(), diff --git a/apps/recorder/src/sync/core.rs b/apps/recorder/src/sync/core.rs index af3da0d..2d18c1c 100644 --- a/apps/recorder/src/sync/core.rs +++ b/apps/recorder/src/sync/core.rs @@ -57,7 +57,7 @@ pub enum TorrentSource { } impl TorrentSource { - pub async fn parse(client: Option<&HttpClient>, url: &str) -> eyre::Result { + pub async fn parse(client: Option<&HttpClient>, url: &str) -> color_eyre::eyre::Result { let url = Url::parse(url)?; let source = if url.scheme() == MAGNET_SCHEMA { TorrentSource::from_magnet_url(url)? @@ -82,7 +82,10 @@ impl TorrentSource { Ok(source) } - pub fn from_torrent_file(file: Vec, name: Option) -> eyre::Result { + pub fn from_torrent_file( + file: Vec, + name: Option, + ) -> color_eyre::eyre::Result { let torrent: TorrentMetaV1Owned = torrent_from_bytes(&file) .map_err(|_| TorrentDownloadError::InvalidTorrentFileFormat)?; let hash = torrent.info_hash.as_string(); @@ -93,7 +96,7 @@ impl TorrentSource { }) } - pub fn from_magnet_url(url: Url) -> eyre::Result { + pub fn from_magnet_url(url: Url) -> color_eyre::eyre::Result { if url.scheme() != MAGNET_SCHEMA { Err(TorrentDownloadError::InvalidUrlSchema { found: url.scheme().to_string(), @@ -117,7 +120,7 @@ impl TorrentSource { } } - pub fn from_torrent_url(url: Url, hash: String) -> eyre::Result { + pub fn from_torrent_url(url: Url, hash: String) -> color_eyre::eyre::Result { Ok(TorrentSource::TorrentUrl { url, hash }) } @@ -246,35 +249,47 @@ pub trait TorrentDownloader { status_filter: TorrentFilter, category: Option, tag: Option, - ) -> eyre::Result>; + ) -> color_eyre::eyre::Result>; async fn add_torrents( &self, source: TorrentSource, save_path: String, category: Option<&str>, - ) -> eyre::Result<()>; + ) -> color_eyre::eyre::Result<()>; - async fn delete_torrents(&self, hashes: Vec) -> eyre::Result<()>; + async fn delete_torrents(&self, hashes: Vec) -> color_eyre::eyre::Result<()>; async fn rename_torrent_file( &self, hash: &str, old_path: &str, new_path: &str, - ) -> eyre::Result<()>; + ) -> color_eyre::eyre::Result<()>; - async fn move_torrents(&self, hashes: Vec, new_path: &str) -> eyre::Result<()>; + async fn move_torrents( + &self, + hashes: Vec, + new_path: &str, + ) -> color_eyre::eyre::Result<()>; - async fn get_torrent_path(&self, hashes: String) -> eyre::Result>; + async fn get_torrent_path(&self, hashes: String) -> color_eyre::eyre::Result>; - async fn check_connection(&self) -> eyre::Result<()>; + async fn check_connection(&self) -> color_eyre::eyre::Result<()>; - async fn set_torrents_category(&self, hashes: Vec, category: &str) -> eyre::Result<()>; + async fn set_torrents_category( + &self, + hashes: Vec, + category: &str, + ) -> color_eyre::eyre::Result<()>; - async fn add_torrent_tags(&self, hashes: Vec, tags: Vec) -> eyre::Result<()>; + async fn add_torrent_tags( + &self, + hashes: Vec, + tags: Vec, + ) -> color_eyre::eyre::Result<()>; - async fn add_category(&self, category: &str) -> eyre::Result<()>; + async fn add_category(&self, category: &str) -> color_eyre::eyre::Result<()>; fn get_save_path(&self, sub_path: &Path) -> PathBuf; } diff --git a/apps/recorder/src/sync/mod.rs b/apps/recorder/src/sync/mod.rs index ba69ae7..de064fb 100644 --- a/apps/recorder/src/sync/mod.rs +++ b/apps/recorder/src/sync/mod.rs @@ -1,6 +1,7 @@ pub mod core; pub mod error; pub mod qbit; +mod utils; pub use core::{ Torrent, TorrentContent, TorrentDownloader, TorrentFilter, TorrentSource, BITTORRENT_MIME_TYPE, diff --git a/apps/recorder/src/sync/qbit.rs b/apps/recorder/src/sync/qbit.rs index 11be75b..6c97f27 100644 --- a/apps/recorder/src/sync/qbit.rs +++ b/apps/recorder/src/sync/qbit.rs @@ -3,7 +3,7 @@ use std::{ }; use async_trait::async_trait; -use eyre::OptionExt; +use color_eyre::eyre::OptionExt; use futures::future::try_join_all; pub use qbit_rs::model::{ Torrent as QbitTorrent, TorrentContent as QbitTorrentContent, TorrentFile as QbitTorrentFile, @@ -13,12 +13,15 @@ use qbit_rs::{ model::{AddTorrentArg, Credential, GetTorrentListArg, NonEmptyStr, SyncData}, Qbit, }; -use quirks_path::{path_equals_as_file_url, Path, PathBuf}; +use quirks_path::{Path, PathBuf}; use tokio::time::sleep; use tracing::instrument; use url::Url; -use super::{Torrent, TorrentDownloadError, TorrentDownloader, TorrentFilter, TorrentSource}; +use super::{ + utils::path_equals_as_file_url, Torrent, TorrentDownloadError, TorrentDownloader, + TorrentFilter, TorrentSource, +}; impl From for QbitTorrentSource { fn from(value: TorrentSource) -> Self { @@ -105,7 +108,7 @@ impl QBittorrentDownloader { } #[instrument(level = "debug")] - pub async fn api_version(&self) -> eyre::Result { + pub async fn api_version(&self) -> color_eyre::eyre::Result { let result = self.client.get_webapi_version().await?; Ok(result) } @@ -116,11 +119,11 @@ impl QBittorrentDownloader { fetch_data_fn: G, mut stop_wait_fn: F, timeout: Option, - ) -> eyre::Result<()> + ) -> color_eyre::eyre::Result<()> where H: FnOnce() -> E, G: Fn(Arc, E) -> Fut, - Fut: Future>, + Fut: Future>, F: FnMut(&D) -> bool, E: Clone, D: Debug + serde::Serialize, @@ -161,7 +164,7 @@ impl QBittorrentDownloader { arg: GetTorrentListArg, stop_wait_fn: F, timeout: Option, - ) -> eyre::Result<()> + ) -> color_eyre::eyre::Result<()> where F: FnMut(&Vec) -> bool, { @@ -169,7 +172,7 @@ impl QBittorrentDownloader { || arg, async move |client: Arc, arg: GetTorrentListArg| - -> eyre::Result> { + -> color_eyre::eyre::Result> { let data = client.get_torrent_list(arg).await?; Ok(data) }, @@ -184,10 +187,10 @@ impl QBittorrentDownloader { &self, stop_wait_fn: F, timeout: Option, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { self.wait_until( || (), - async move |client: Arc, _| -> eyre::Result { + async move |client: Arc, _| -> color_eyre::eyre::Result { let data = client.sync(None).await?; Ok(data) }, @@ -203,12 +206,12 @@ impl QBittorrentDownloader { hash: &str, stop_wait_fn: F, timeout: Option, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { self.wait_until( || Arc::new(hash.to_string()), async move |client: Arc, hash_arc: Arc| - -> eyre::Result> { + -> color_eyre::eyre::Result> { let data = client.get_torrent_contents(hash_arc.as_str(), None).await?; Ok(data) }, @@ -227,7 +230,7 @@ impl TorrentDownloader for QBittorrentDownloader { status_filter: TorrentFilter, category: Option, tag: Option, - ) -> eyre::Result> { + ) -> color_eyre::eyre::Result> { let arg = GetTorrentListArg { filter: Some(status_filter.into()), category, @@ -256,7 +259,7 @@ impl TorrentDownloader for QBittorrentDownloader { source: TorrentSource, save_path: String, category: Option<&str>, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { let arg = AddTorrentArg { source: source.clone().into(), savepath: Some(save_path), @@ -290,7 +293,7 @@ impl TorrentDownloader for QBittorrentDownloader { } #[instrument(level = "debug", skip(self))] - async fn delete_torrents(&self, hashes: Vec) -> eyre::Result<()> { + async fn delete_torrents(&self, hashes: Vec) -> color_eyre::eyre::Result<()> { self.client .delete_torrents(hashes.clone(), Some(true)) .await?; @@ -311,7 +314,7 @@ impl TorrentDownloader for QBittorrentDownloader { hash: &str, old_path: &str, new_path: &str, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { self.client.rename_file(hash, old_path, new_path).await?; let new_path = self.save_path.join(new_path); let save_path = self.save_path.as_path(); @@ -333,7 +336,11 @@ impl TorrentDownloader for QBittorrentDownloader { } #[instrument(level = "debug", skip(self))] - async fn move_torrents(&self, hashes: Vec, new_path: &str) -> eyre::Result<()> { + async fn move_torrents( + &self, + hashes: Vec, + new_path: &str, + ) -> color_eyre::eyre::Result<()> { self.client .set_torrent_location(hashes.clone(), new_path) .await?; @@ -357,7 +364,7 @@ impl TorrentDownloader for QBittorrentDownloader { Ok(()) } - async fn get_torrent_path(&self, hashes: String) -> eyre::Result> { + async fn get_torrent_path(&self, hashes: String) -> color_eyre::eyre::Result> { let mut torrent_list = self .client .get_torrent_list(GetTorrentListArg { @@ -370,13 +377,17 @@ impl TorrentDownloader for QBittorrentDownloader { } #[instrument(level = "debug", skip(self))] - async fn check_connection(&self) -> eyre::Result<()> { + async fn check_connection(&self) -> color_eyre::eyre::Result<()> { self.api_version().await?; Ok(()) } #[instrument(level = "debug", skip(self))] - async fn set_torrents_category(&self, hashes: Vec, category: &str) -> eyre::Result<()> { + async fn set_torrents_category( + &self, + hashes: Vec, + category: &str, + ) -> color_eyre::eyre::Result<()> { let result = self .client .set_torrent_category(hashes.clone(), category) @@ -405,9 +416,13 @@ impl TorrentDownloader for QBittorrentDownloader { } #[instrument(level = "debug", skip(self))] - async fn add_torrent_tags(&self, hashes: Vec, tags: Vec) -> eyre::Result<()> { + async fn add_torrent_tags( + &self, + hashes: Vec, + tags: Vec, + ) -> color_eyre::eyre::Result<()> { if tags.is_empty() { - return Err(eyre::eyre!("add torrent tags can not be empty")); + return Err(color_eyre::eyre::eyre!("add torrent tags can not be empty")); } self.client .add_torrent_tags(hashes.clone(), tags.clone()) @@ -435,7 +450,7 @@ impl TorrentDownloader for QBittorrentDownloader { } #[instrument(level = "debug", skip(self))] - async fn add_category(&self, category: &str) -> eyre::Result<()> { + async fn add_category(&self, category: &str) -> color_eyre::eyre::Result<()> { self.client .add_category( NonEmptyStr::new(category).ok_or_eyre("category can not be empty")?, @@ -486,7 +501,8 @@ pub mod tests { #[cfg(feature = "testcontainers")] pub async fn create_qbit_testcontainer( - ) -> eyre::Result> { + ) -> color_eyre::eyre::Result> + { use testcontainers::{ core::{ ContainerPort, @@ -522,7 +538,7 @@ pub mod tests { #[cfg(feature = "testcontainers")] #[tokio::test(flavor = "multi_thread")] - async fn test_qbittorrent_downloader() -> eyre::Result<()> { + async fn test_qbittorrent_downloader() -> color_eyre::eyre::Result<()> { use testcontainers::runners::AsyncRunner; use tokio::io::AsyncReadExt; @@ -573,7 +589,7 @@ pub mod tests { async fn test_qbittorrent_downloader_impl( username: Option<&str>, password: Option<&str>, - ) -> eyre::Result<()> { + ) -> color_eyre::eyre::Result<()> { let base_save_path = Path::new(get_tmp_qbit_test_folder()); let mut downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation { @@ -607,7 +623,7 @@ pub mod tests { .add_torrents(torrent_source, save_path.to_string(), Some("bangumi")) .await?; - let get_torrent = async || -> eyre::Result { + let get_torrent = async || -> color_eyre::eyre::Result { let torrent_infos = downloader .get_torrents_info(TorrentFilter::All, None, None) .await?; diff --git a/apps/recorder/src/sync/utils.rs b/apps/recorder/src/sync/utils.rs new file mode 100644 index 0000000..c50ebcf --- /dev/null +++ b/apps/recorder/src/sync/utils.rs @@ -0,0 +1,11 @@ +use quirks_path::{Path, PathToUrlError}; + +pub fn path_equals_as_file_url, B: AsRef>( + a: A, + b: B, +) -> Result { + let u1 = a.as_ref().to_file_url()?; + let u2 = b.as_ref().to_file_url()?; + + Ok(u1.as_str() == u2.as_str()) +} diff --git a/apps/recorder/src/test_utils/testcontainers.rs b/apps/recorder/src/test_utils/testcontainers.rs index 477b7b3..fce85bb 100644 --- a/apps/recorder/src/test_utils/testcontainers.rs +++ b/apps/recorder/src/test_utils/testcontainers.rs @@ -19,7 +19,7 @@ where container_label: &str, prune: bool, force: bool, - ) -> eyre::Result; + ) -> color_eyre::eyre::Result; fn with_default_log_consumer(self) -> Self; } @@ -34,7 +34,7 @@ where container_label: &str, prune: bool, force: bool, - ) -> eyre::Result { + ) -> color_eyre::eyre::Result { use std::collections::HashMap; use bollard::container::PruneContainersOptions; diff --git a/packages/quirks-path/Cargo.toml b/packages/quirks-path/Cargo.toml deleted file mode 100644 index da4b7b3..0000000 --- a/packages/quirks-path/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "quirks_path" -version = "0.1.0" -edition = "2021" - -[dependencies] -nom = "7.1.3" -percent-encoding = "2.3.1" -serde = { version = "1.0.197", features = ["derive"] } -thiserror = "1.0.57" -tracing = "0.1.41" -url = "2.5.0" - -[dev-dependencies] -tracing = "0.1.41" diff --git a/packages/quirks-path/src/lib.rs b/packages/quirks-path/src/lib.rs deleted file mode 100644 index f5d9040..0000000 --- a/packages/quirks-path/src/lib.rs +++ /dev/null @@ -1,1716 +0,0 @@ -#![feature(extend_one)] - -mod url; -pub mod windows; - -use std::{ - borrow::{Borrow, Cow}, - cmp, - collections::TryReserveError, - error::Error, - fmt, - fmt::Formatter, - hash::{Hash, Hasher}, - io, - iter::FusedIterator, - ops::{Deref, DerefMut}, - rc::Rc, - str::FromStr, - sync::Arc, -}; - -use ::url::Url; -pub use url::{path_equals_as_file_url, PathToUrlError}; -use windows::is_windows_sep; - -use crate::{ - url::path_to_file_url_segments, - windows::{is_windows_verbatim_sep, parse_windows_path_prefix}, -}; - -#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub enum Prefix<'a> { - Verbatim { prefix: &'a str }, - VerbatimUNC { server: &'a str, share: &'a str }, - VerbatimDisk { drive: char }, - DeviceNS { device: &'a str }, - UNC { server: &'a str, share: &'a str }, - Disk { drive: char }, -} - -impl Prefix<'_> { - #[inline] - pub fn is_verbatim(&self) -> bool { - use Prefix::*; - - matches!( - *self, - Verbatim { .. } | VerbatimDisk { .. } | VerbatimUNC { .. } - ) - } - - pub fn len(&self) -> usize { - use Prefix::*; - - match *self { - Verbatim { prefix } => 4 + prefix.len(), - VerbatimUNC { server, share } => { - 8 + server.len() - + if !share.is_empty() { - 1 + share.len() - } else { - 0 - } - } - VerbatimDisk { .. } => 6, - UNC { server, share } => { - 2 + server.len() - + if !share.is_empty() { - 1 + share.len() - } else { - 0 - } - } - DeviceNS { device } => 4 + device.len(), - Disk { .. } => 2, - } - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - #[inline] - pub fn is_drive(&self) -> bool { - use Prefix::*; - - matches!(*self, Disk { .. } | VerbatimDisk { .. }) - } - - #[inline] - pub fn has_implicit_root(&self) -> bool { - !self.is_drive() - } -} - -fn iter_after<'a, 'b, I, J>(mut iter: I, mut prefix: J) -> Option -where - I: Iterator> + Clone, - J: Iterator>, -{ - loop { - let mut iter_next = iter.clone(); - match (iter_next.next(), prefix.next()) { - (Some(ref x), Some(ref y)) if x == y => (), - (Some(_), Some(_)) => return None, - (Some(_), None) => return Some(iter), - (None, None) => return Some(iter), - (None, Some(_)) => return None, - } - iter = iter_next; - } -} - -pub fn is_separator(c: char) -> bool { - is_windows_sep(c) -} - -pub fn is_sep_byte(c: char) -> bool { - is_windows_sep(c) -} - -pub fn is_verbatim_sep(c: char) -> bool { - is_windows_verbatim_sep(c) -} - -fn has_physical_root<'a>(s: &'a str, prefix: Option>) -> Option<&'a str> { - let (len, path) = if let Some(p) = prefix { - (p.len(), &s[p.len()..]) - } else { - (0, s) - }; - let path_bytes = path.as_bytes(); - if !path.is_empty() && is_separator(path_bytes[0] as char) { - Some(&s[len..len + 1]) - } else { - None - } -} - -fn rsplit_file_at_dot(file: &str) -> (Option<&str>, Option<&str>) { - if file == ".." { - return (Some(file), None); - } - - let mut iter = file.rsplitn(2, '.'); - let after = iter.next(); - let before = iter.next(); - if before == Some("") { - (Some(file), None) - } else { - (before, after) - } -} - -fn split_file_at_dot(file: &str) -> (&str, Option<&str>) { - if file == ".." { - return (file, None); - } - - let i = match file[1..].bytes().position(|b| b == b'.') { - Some(i) => i + 1, - None => return (file, None), - }; - let before = &file[..i]; - let after = &file[i + 1..]; - (before, Some(after)) -} - -#[derive(Debug, PartialEq, Clone, PartialOrd, Copy)] -enum State { - Prefix = 0, - StartDir = 1, - Body = 2, - Done = 3, -} - -#[derive(Debug, Eq, Clone, Copy)] -pub struct PrefixComponent<'a> { - raw: &'a str, - parsed: Prefix<'a>, -} - -impl<'a> PrefixComponent<'a> { - #[inline] - pub fn kind(&self) -> Prefix<'a> { - self.parsed - } - - #[inline] - pub fn as_str(&self) -> &'a str { - self.raw - } -} - -#[allow(clippy::non_canonical_partial_ord_impl)] -impl PartialOrd for PrefixComponent<'_> { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - self.parsed.partial_cmp(&other.parsed) - } -} - -impl PartialEq for PrefixComponent<'_> { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.parsed == other.parsed - } -} - -impl Ord for PrefixComponent<'_> { - #[inline] - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.parsed.cmp(&other.parsed) - } -} - -impl Hash for PrefixComponent<'_> { - fn hash(&self, h: &mut H) { - self.parsed.hash(h) - } -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub enum Component<'a> { - Prefix(PrefixComponent<'a>), - RootDir(Option<&'a str>), - CurDir, - ParentDir, - Normal(&'a str), -} - -impl<'a> Component<'a> { - pub fn as_str(self) -> &'a str { - match self { - Component::Prefix(p) => p.as_str(), - Component::RootDir(root) => root.unwrap_or("/"), - Component::CurDir => ".", - Component::ParentDir => "..", - Component::Normal(path) => path, - } - } -} - -impl AsRef for Component<'_> { - #[inline] - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl AsRef for Component<'_> { - #[inline] - fn as_ref(&self) -> &Path { - self.as_str().as_ref() - } -} - -#[derive(Clone)] -pub struct Components<'a> { - path: &'a str, - prefix: Option>, - has_physical_root: Option<&'a str>, - front: State, - back: State, -} - -#[derive(Clone)] -pub struct Iter<'a> { - inner: Components<'a>, -} - -impl fmt::Debug for Components<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - struct DebugHelper<'a>(&'a Path); - - impl fmt::Debug for DebugHelper<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list().entries(self.0.components()).finish() - } - } - - f.debug_tuple("Components") - .field(&DebugHelper(self.as_path())) - .finish() - } -} - -impl<'a> Components<'a> { - #[inline] - fn prefix_len(&self) -> usize { - self.prefix.as_ref().map(Prefix::len).unwrap_or(0) - } - - #[inline] - fn prefix_verbatim(&self) -> bool { - self.prefix - .as_ref() - .map(Prefix::is_verbatim) - .unwrap_or(false) - } - - #[inline] - fn prefix_remaining(&self) -> usize { - if self.front == State::Prefix { - self.prefix_len() - } else { - 0 - } - } - - // Given the iteration so far, how much of the pre-State::Body path is left? - #[inline] - fn len_before_body(&self) -> usize { - let root = if self.front <= State::StartDir && matches!(self.has_physical_root, Some(..)) { - 1 - } else { - 0 - }; - let cur_dir = if self.front <= State::StartDir && self.include_cur_dir() { - 1 - } else { - 0 - }; - self.prefix_remaining() + root + cur_dir - } - - #[inline] - fn finished(&self) -> bool { - self.front == State::Done || self.back == State::Done || self.front > self.back - } - - #[inline] - fn is_sep_byte(&self, b: u8) -> bool { - if self.prefix_verbatim() { - is_verbatim_sep(b as char) - } else { - is_sep_byte(b as char) - } - } - - pub fn as_path(&self) -> &'a Path { - let mut comps = self.clone(); - if comps.front == State::Body { - comps.trim_left(); - } - if comps.back == State::Body { - comps.trim_right(); - } - Path::new(comps.path) - } - - fn has_root(&self) -> bool { - if matches!(self.has_physical_root, Some(..)) { - return true; - } - if let Some(p) = self.prefix { - if p.has_implicit_root() { - return true; - } - } - false - } - - fn include_cur_dir(&self) -> bool { - if self.has_root() { - return false; - } - let mut iter = self.path[self.prefix_remaining()..].as_bytes().iter(); - match (iter.next(), iter.next()) { - (Some(&b'.'), None) => true, - (Some(&b'.'), Some(&b)) => self.is_sep_byte(b), - _ => false, - } - } - - fn parse_single_component<'b>(&self, comp: &'b str) -> Option> { - match comp { - "." if self.prefix_verbatim() => Some(Component::CurDir), - "." => None, - ".." => Some(Component::ParentDir), - "" => None, - _ => Some(Component::Normal(comp)), - } - } - - fn parse_next_component(&self) -> (usize, Option>) { - debug_assert!(self.front == State::Body); - let (extra, comp) = match self - .path - .as_bytes() - .iter() - .position(|b| self.is_sep_byte(*b)) - { - None => (0, self.path), - Some(i) => (1, &self.path[..i]), - }; - (comp.len() + extra, self.parse_single_component(comp)) - } - - fn parse_next_component_back(&self) -> (usize, Option>) { - debug_assert!(self.back == State::Body); - let start = self.len_before_body(); - let (extra, comp) = match self.path[start..] - .as_bytes() - .iter() - .rposition(|b| self.is_sep_byte(*b)) - { - None => (0, &self.path[start..]), - Some(i) => (1, &self.path[start + i + 1..]), - }; - (comp.len() + extra, self.parse_single_component(comp)) - } - - fn trim_left(&mut self) { - while !self.path.is_empty() { - let (size, comp) = self.parse_next_component(); - if comp.is_some() { - return; - } else { - self.path = &self.path[size..]; - } - } - } - - fn trim_right(&mut self) { - while self.path.len() > self.len_before_body() { - let (size, comp) = self.parse_next_component_back(); - if comp.is_some() { - return; - } else { - self.path = &self.path[..self.path.len() - size]; - } - } - } -} - -impl AsRef for Components<'_> { - #[inline] - fn as_ref(&self) -> &Path { - self.as_path() - } -} - -impl AsRef for Components<'_> { - #[inline] - fn as_ref(&self) -> &str { - self.as_path().as_str() - } -} - -impl fmt::Debug for Iter<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - struct DebugHelper<'a>(&'a Path); - - impl fmt::Debug for DebugHelper<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list().entries(self.0.iter()).finish() - } - } - - f.debug_tuple("Iter") - .field(&DebugHelper(self.as_path())) - .finish() - } -} - -impl<'a> Iter<'a> { - #[inline] - pub fn as_path(&self) -> &'a Path { - self.inner.as_path() - } -} - -impl AsRef for Iter<'_> { - #[inline] - fn as_ref(&self) -> &Path { - self.as_path() - } -} - -impl AsRef for Iter<'_> { - #[inline] - fn as_ref(&self) -> &str { - self.as_path().as_str() - } -} - -impl<'a> Iterator for Iter<'a> { - type Item = &'a str; - - #[inline] - fn next(&mut self) -> Option<&'a str> { - self.inner.next().map(Component::as_str) - } -} - -impl<'a> DoubleEndedIterator for Iter<'a> { - #[inline] - fn next_back(&mut self) -> Option<&'a str> { - self.inner.next_back().map(Component::as_str) - } -} - -impl FusedIterator for Iter<'_> {} - -impl<'a> Iterator for Components<'a> { - type Item = Component<'a>; - - fn next(&mut self) -> Option> { - while !self.finished() { - match self.front { - State::Prefix if self.prefix_len() > 0 => { - self.front = State::StartDir; - debug_assert!(self.prefix_len() <= self.path.len()); - let raw = &self.path[..self.prefix_len()]; - self.path = &self.path[self.prefix_len()..]; - return Some(Component::Prefix(PrefixComponent { - raw, - parsed: self.prefix.unwrap(), - })); - } - State::Prefix => { - self.front = State::StartDir; - } - State::StartDir => { - self.front = State::Body; - if let Some(root) = self.has_physical_root { - debug_assert!(!self.path.is_empty()); - self.path = &self.path[1..]; - return Some(Component::RootDir(Some(root))); - } else if let Some(p) = self.prefix { - if p.has_implicit_root() && !p.is_verbatim() { - return Some(Component::RootDir(None)); - } - } else if self.include_cur_dir() { - debug_assert!(!self.path.is_empty()); - self.path = &self.path[1..]; - return Some(Component::CurDir); - } - } - State::Body if !self.path.is_empty() => { - let (size, comp) = self.parse_next_component(); - self.path = &self.path[size..]; - if comp.is_some() { - return comp; - } - } - State::Body => { - self.front = State::Done; - } - State::Done => unreachable!(), - } - } - None - } -} - -impl<'a> DoubleEndedIterator for Components<'a> { - fn next_back(&mut self) -> Option> { - while !self.finished() { - match self.back { - State::Body if self.path.len() > self.len_before_body() => { - let (size, comp) = self.parse_next_component_back(); - self.path = &self.path[..self.path.len() - size]; - if comp.is_some() { - return comp; - } - } - State::Body => { - self.back = State::StartDir; - } - State::StartDir => { - self.back = State::Prefix; - if let Some(root) = self.has_physical_root { - self.path = &self.path[..self.path.len() - 1]; - return Some(Component::RootDir(Some(root))); - } else if let Some(p) = self.prefix { - if p.has_implicit_root() && !p.is_verbatim() { - return Some(Component::RootDir(None)); - } - } else if self.include_cur_dir() { - self.path = &self.path[..self.path.len() - 1]; - return Some(Component::CurDir); - } - } - State::Prefix if self.prefix_len() > 0 => { - self.back = State::Done; - return Some(Component::Prefix(PrefixComponent { - raw: self.path, - parsed: self.prefix.unwrap(), - })); - } - State::Prefix => { - self.back = State::Done; - return None; - } - State::Done => unreachable!(), - } - } - None - } -} - -impl FusedIterator for Components<'_> {} - -impl<'a> PartialEq for Components<'a> { - #[inline] - fn eq(&self, other: &Components<'a>) -> bool { - let Components { - path: _, - front: _, - back: _, - has_physical_root: _, - prefix: _, - } = self; - - if self.path.len() == other.path.len() - && self.front == other.front - && self.back == State::Body - && other.back == State::Body - && self.prefix_verbatim() == other.prefix_verbatim() - && self.path == other.path - { - return true; - } - - Iterator::eq(self.clone().rev(), other.clone().rev()) - } -} - -impl Eq for Components<'_> {} - -#[allow(clippy::non_canonical_partial_ord_impl)] -impl<'a> PartialOrd for Components<'a> { - #[inline] - fn partial_cmp(&self, other: &Components<'a>) -> Option { - Some(compare_components(self.clone(), other.clone())) - } -} - -impl Ord for Components<'_> { - #[inline] - fn cmp(&self, other: &Self) -> cmp::Ordering { - compare_components(self.clone(), other.clone()) - } -} - -fn compare_components(mut left: Components<'_>, mut right: Components<'_>) -> cmp::Ordering { - if left.prefix.is_none() && right.prefix.is_none() && left.front == right.front { - let first_difference = match left - .path - .as_bytes() - .iter() - .zip(right.path.as_bytes()) - .position(|(&a, &b)| a != b) - { - None if left.path.len() == right.path.len() => return cmp::Ordering::Equal, - None => left.path.len().min(right.path.len()), - Some(diff) => diff, - }; - - if let Some(previous_sep) = left.path[..first_difference] - .as_bytes() - .iter() - .rposition(|&b| left.is_sep_byte(b)) - { - let mismatched_component_start = previous_sep + 1; - left.path = &left.path[mismatched_component_start..]; - left.front = State::Body; - right.path = &right.path[mismatched_component_start..]; - right.front = State::Body; - } - } - - Iterator::cmp(left, right) -} - -#[derive(Copy, Clone, Debug)] -pub struct Ancestors<'a> { - next: Option<&'a Path>, -} - -impl<'a> Iterator for Ancestors<'a> { - type Item = &'a Path; - - #[inline] - fn next(&mut self) -> Option { - let next = self.next; - self.next = next.and_then(Path::parent); - next - } -} - -impl FusedIterator for Ancestors<'_> {} - -pub struct PathBuf { - inner: String, -} - -impl PathBuf { - #[inline] - fn as_mut_vec(&mut self) -> &mut Vec { - unsafe { self.inner.as_mut_vec() } - } - - #[inline] - pub fn new() -> PathBuf { - PathBuf { - inner: String::new(), - } - } - - #[inline] - pub fn with_capacity(capacity: usize) -> PathBuf { - PathBuf { - inner: String::with_capacity(capacity), - } - } - - #[inline] - pub fn as_path(&self) -> &Path { - self - } - - pub fn push>(&mut self, path: P) { - self._push(path.as_ref()) - } - - fn _push(&mut self, path: &Path) { - let main_sep_str = self.get_main_sep(); - - let mut need_sep = self - .as_mut_vec() - .last() - .is_some_and(|c| !is_separator(*c as char)); - - let comps = self.components(); - - if comps.prefix_len() > 0 - && comps.prefix_len() == comps.path.len() - && comps.prefix.unwrap().is_drive() - { - need_sep = true; - } - - if path.is_absolute() || path.prefix().is_some() { - self.as_mut_vec().truncate(0); - } else if comps.prefix_verbatim() && !path.inner.is_empty() { - let mut buf: Vec<_> = comps.collect(); - for c in path.components() { - match c { - Component::RootDir { .. } => { - buf.truncate(1); - buf.push(c); - } - Component::CurDir => (), - Component::ParentDir => { - if let Some(Component::Normal(_)) = buf.last() { - buf.pop(); - } - } - _ => buf.push(c), - } - } - let mut res = String::new(); - let mut need_sep = false; - for c in buf { - if need_sep && !matches!(c, Component::RootDir { .. }) { - res.push(main_sep_str); - } - res.push_str(c.as_str()); - - need_sep = match c { - Component::RootDir { .. } => false, - Component::Prefix(prefix) => { - !prefix.parsed.is_drive() && !prefix.parsed.is_empty() - } - _ => true, - }; - } - self.inner = res; - return; - } else if path.has_root() { - let prefix_len = self.components().prefix_remaining(); - self.as_mut_vec().truncate(prefix_len); - } else if need_sep { - self.inner.push(main_sep_str) - } - - self.inner.push_str(path.as_str()) - } - - pub fn pop(&mut self) -> bool { - match self.parent().map(|p| p.inner.len()) { - Some(len) => { - self.as_mut_vec().truncate(len); - true - } - None => false, - } - } - - pub fn set_file_name>(&mut self, file_name: S) { - self._set_file_name(file_name.as_ref()) - } - - fn _set_file_name(&mut self, file_name: &str) { - if self.file_name().is_some() { - let popped = self.pop(); - debug_assert!(popped); - } - self.push(file_name); - } - - pub fn set_extension>(&mut self, extension: S) -> bool { - self._set_extension(extension.as_ref()) - } - - fn _set_extension(&mut self, extension: &str) -> bool { - let file_stem = match self.file_stem() { - None => return false, - Some(f) => f.as_bytes(), - }; - - // truncate until right after the file stem - let end_file_stem = file_stem[file_stem.len()..].as_ptr().addr(); - let start = self.inner.as_bytes().as_ptr().addr(); - let v = self.as_mut_vec(); - v.truncate(end_file_stem.wrapping_sub(start)); - - // add the new extension, if any - let new = extension.as_bytes(); - if !new.is_empty() { - v.reserve_exact(new.len() + 1); - v.push(b'.'); - v.extend_from_slice(new); - } - - true - } - - #[inline] - pub fn as_mut_string(&mut self) -> &mut String { - &mut self.inner - } - - #[inline] - pub fn into_string(self) -> String { - self.inner - } - - #[inline] - pub fn into_boxed_path(self) -> Box { - let rw = Box::into_raw(self.inner.into_boxed_str()) as *mut Path; - unsafe { Box::from_raw(rw) } - } - - #[inline] - pub fn capacity(&self) -> usize { - self.inner.capacity() - } - - #[inline] - pub fn clear(&mut self) { - self.inner.clear() - } - - #[inline] - pub fn reserve(&mut self, additional: usize) { - self.inner.reserve(additional) - } - - #[inline] - pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError> { - self.inner.try_reserve(additional) - } - - #[inline] - pub fn reserve_exact(&mut self, additional: usize) { - self.inner.reserve_exact(additional) - } - - #[inline] - pub fn try_reserve_exact(&mut self, additional: usize) -> Result<(), TryReserveError> { - self.inner.try_reserve_exact(additional) - } - - #[inline] - pub fn shrink_to_fit(&mut self) { - self.inner.shrink_to_fit() - } - - #[inline] - pub fn shrink_to(&mut self, min_capacity: usize) { - self.inner.shrink_to(min_capacity) - } -} - -impl Clone for PathBuf { - #[inline] - fn clone(&self) -> Self { - PathBuf { - inner: self.inner.clone(), - } - } - - #[inline] - fn clone_from(&mut self, source: &Self) { - self.inner.clone_from(&source.inner) - } -} - -impl From<&Path> for Box { - /// Creates a boxed [`Path`] from a reference. - /// - /// This will allocate and clone `path` to it. - fn from(path: &Path) -> Box { - let boxed: Box = path.inner.into(); - let rw = Box::into_raw(boxed) as *mut Path; - unsafe { Box::from_raw(rw) } - } -} - -impl From> for Box { - #[inline] - fn from(cow: Cow<'_, Path>) -> Box { - match cow { - Cow::Borrowed(path) => Box::from(path), - Cow::Owned(path) => Box::from(path), - } - } -} - -impl From> for PathBuf { - #[inline] - fn from(boxed: Box) -> PathBuf { - boxed.into_path_buf() - } -} - -impl From for Box { - #[inline] - fn from(p: PathBuf) -> Box { - p.into_boxed_path() - } -} - -impl Clone for Box { - #[inline] - fn clone(&self) -> Self { - self.to_path_buf().into_boxed_path() - } -} - -impl> From<&T> for PathBuf { - #[inline] - fn from(s: &T) -> PathBuf { - PathBuf::from(s.as_ref().to_string()) - } -} - -impl From for PathBuf { - #[inline] - fn from(s: String) -> PathBuf { - PathBuf { inner: s } - } -} - -impl From for String { - #[inline] - fn from(path_buf: PathBuf) -> String { - path_buf.inner - } -} - -impl FromStr for PathBuf { - type Err = core::convert::Infallible; - - #[inline] - fn from_str(s: &str) -> Result { - Ok(PathBuf::from(s)) - } -} - -impl> FromIterator

for PathBuf { - fn from_iter>(iter: I) -> PathBuf { - let mut buf = PathBuf::new(); - buf.extend(iter); - buf - } -} - -impl> Extend

for PathBuf { - fn extend>(&mut self, iter: I) { - iter.into_iter().for_each(move |p| self.push(p.as_ref())); - } - - #[inline] - fn extend_one(&mut self, p: P) { - self.push(p.as_ref()); - } -} - -impl fmt::Debug for PathBuf { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, formatter) - } -} - -impl fmt::Display for PathBuf { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&**self, formatter) - } -} - -impl Deref for PathBuf { - type Target = Path; - #[inline] - fn deref(&self) -> &Path { - Path::new(&self.inner) - } -} - -impl DerefMut for PathBuf { - #[inline] - fn deref_mut(&mut self) -> &mut Path { - Path::from_inner_mut(&mut self.inner) - } -} - -impl Borrow for PathBuf { - #[inline] - fn borrow(&self) -> &Path { - self.deref() - } -} - -impl Default for PathBuf { - #[inline] - fn default() -> Self { - PathBuf::new() - } -} - -impl<'a> From<&'a Path> for Cow<'a, Path> { - #[inline] - fn from(s: &'a Path) -> Cow<'a, Path> { - Cow::Borrowed(s) - } -} - -impl<'a> From for Cow<'a, Path> { - #[inline] - fn from(s: PathBuf) -> Cow<'a, Path> { - Cow::Owned(s) - } -} - -impl<'a> From<&'a PathBuf> for Cow<'a, Path> { - #[inline] - fn from(p: &'a PathBuf) -> Cow<'a, Path> { - Cow::Borrowed(p.as_path()) - } -} - -impl<'a> From> for PathBuf { - #[inline] - fn from(p: Cow<'a, Path>) -> Self { - p.into_owned() - } -} - -impl From for Arc { - #[inline] - fn from(s: PathBuf) -> Arc { - let arc: Arc = Arc::from(s.into_string()); - unsafe { Arc::from_raw(Arc::into_raw(arc) as *const Path) } - } -} - -impl From<&Path> for Arc { - /// Converts a [`Path`] into an [`Arc`] by copying the [`Path`] data into a - /// new [`Arc`] buffer. - #[inline] - fn from(s: &Path) -> Arc { - let arc: Arc = Arc::from(s.as_str()); - unsafe { Arc::from_raw(Arc::into_raw(arc) as *const Path) } - } -} - -impl From for Rc { - /// Converts a [`PathBuf`] into an [Rc]<[Path]> by moving the - /// [`PathBuf`] data into a new [`Rc`] buffer. - #[inline] - fn from(s: PathBuf) -> Rc { - let rc: Rc = Rc::from(s.into_string()); - unsafe { Rc::from_raw(Rc::into_raw(rc) as *const Path) } - } -} - -impl From<&Path> for Rc { - #[inline] - fn from(s: &Path) -> Rc { - let rc: Rc = Rc::from(s.as_str()); - unsafe { Rc::from_raw(Rc::into_raw(rc) as *const Path) } - } -} - -impl ToOwned for Path { - type Owned = PathBuf; - #[inline] - fn to_owned(&self) -> PathBuf { - self.to_path_buf() - } - #[inline] - fn clone_into(&self, target: &mut PathBuf) { - self.inner.clone_into(&mut target.inner); - } -} - -impl PartialEq for PathBuf { - #[inline] - fn eq(&self, other: &PathBuf) -> bool { - self.components() == other.components() - } -} - -impl Hash for PathBuf { - fn hash(&self, h: &mut H) { - self.as_path().hash(h) - } -} - -impl Eq for PathBuf {} - -impl PartialOrd for PathBuf { - #[inline] - fn partial_cmp(&self, other: &PathBuf) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PathBuf { - #[inline] - fn cmp(&self, other: &PathBuf) -> cmp::Ordering { - compare_components(self.components(), other.components()) - } -} - -impl AsRef for PathBuf { - #[inline] - fn as_ref(&self) -> &str { - &self.inner[..] - } -} - -pub struct Path { - inner: str, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StripPrefixError(()); - -impl Path { - pub fn new + ?Sized>(s: &S) -> &Path { - unsafe { &*(s.as_ref() as *const str as *const Path) } - } - - pub fn from_inner_mut(s: &mut str) -> &mut Path { - unsafe { &mut *(s as *mut str as *mut Path) } - } - - #[inline] - pub fn as_str(&self) -> &str { - &self.inner - } - - #[inline] - pub fn as_mut_str(&mut self) -> &mut str { - &mut self.inner - } - - pub fn to_path_buf(&self) -> PathBuf { - PathBuf { - inner: self.inner.to_owned(), - } - } - - pub fn is_absolute(&self) -> bool { - self.has_root() - } - - pub fn is_relative(&self) -> bool { - !self.is_absolute() - } - - fn prefix(&self) -> Option> { - self.components().prefix - } - - pub fn has_root(&self) -> bool { - self.components().has_root() - } - - pub fn parent(&self) -> Option<&Path> { - let mut comps = self.components(); - let comp = comps.next_back(); - comp.and_then(|p| match p { - Component::Normal(_) | Component::CurDir | Component::ParentDir => { - Some(comps.as_path()) - } - _ => None, - }) - } - - pub fn ancestors(&self) -> Ancestors<'_> { - Ancestors { next: Some(self) } - } - - pub fn file_name(&self) -> Option<&str> { - self.components().next_back().and_then(|p| match p { - Component::Normal(p) => Some(p), - _ => None, - }) - } - - pub fn strip_prefix

(&self, base: P) -> Result<&Path, StripPrefixError> - where - P: AsRef, - { - self._strip_prefix(base.as_ref()) - } - - fn _strip_prefix(&self, base: &Path) -> Result<&Path, StripPrefixError> { - iter_after(self.components(), base.components()) - .map(|c| c.as_path()) - .ok_or(StripPrefixError(())) - } - - pub fn starts_with>(&self, base: P) -> bool { - self._starts_with(base.as_ref()) - } - - fn _starts_with(&self, base: &Path) -> bool { - iter_after(self.components(), base.components()).is_some() - } - - pub fn ends_with>(&self, child: P) -> bool { - self._ends_with(child.as_ref()) - } - - fn _ends_with(&self, child: &Path) -> bool { - iter_after(self.components().rev(), child.components().rev()).is_some() - } - - pub fn file_stem(&self) -> Option<&str> { - self.file_name() - .map(rsplit_file_at_dot) - .and_then(|(before, after)| before.or(after)) - } - - pub fn file_prefix(&self) -> Option<&str> { - self.file_name() - .map(split_file_at_dot) - .map(|(before, _after)| before) - } - - pub fn extension(&self) -> Option<&str> { - self.file_name() - .map(rsplit_file_at_dot) - .and_then(|(before, after)| before.and(after)) - } - - pub fn join>(&self, path: P) -> PathBuf { - self._join(path.as_ref()) - } - - fn _join(&self, path: &Path) -> PathBuf { - let mut buf = self.to_path_buf(); - buf.push(path); - buf - } - - pub fn with_file_name>(&self, file_name: S) -> PathBuf { - self._with_file_name(file_name.as_ref()) - } - - fn _with_file_name(&self, file_name: &str) -> PathBuf { - let mut buf = self.to_path_buf(); - buf.set_file_name(file_name); - buf - } - - pub fn with_extension>(&self, extension: S) -> PathBuf { - self._with_extension(extension.as_ref()) - } - - fn _with_extension(&self, extension: &str) -> PathBuf { - let self_len = self.as_str().len(); - let self_bytes = self.as_str().as_bytes(); - - let (new_capacity, slice_to_copy) = match self.extension() { - None => { - // Enough capacity for the extension and the dot - let capacity = self_len + extension.len() + 1; - let whole_path = self_bytes.iter(); - (capacity, whole_path) - } - Some(previous_extension) => { - let capacity = self_len + extension.len() - previous_extension.len(); - let path_till_dot = self_bytes[..self_len - previous_extension.len()].iter(); - (capacity, path_till_dot) - } - }; - - let mut new_path = PathBuf::with_capacity(new_capacity); - new_path.as_mut_vec().extend(slice_to_copy); - new_path.set_extension(extension); - new_path - } - - pub fn components(&self) -> Components<'_> { - let prefix = parse_windows_path_prefix(self.as_str()); - let prefix = prefix.ok().map(|(_, prefix)| prefix); - Components { - path: self.as_str(), - has_physical_root: has_physical_root(self.as_str(), prefix), - prefix, - front: State::Prefix, - back: State::Body, - } - } - - #[inline] - pub fn iter(&self) -> Iter<'_> { - Iter { - inner: self.components(), - } - } - - pub fn into_path_buf(self: Box) -> PathBuf { - let rw = Box::into_raw(self) as *mut str; - let inner = unsafe { Box::from_raw(rw) }; - PathBuf { - inner: String::from(inner), - } - } - - pub fn get_main_sep(&self) -> char { - if let Ok((_, prefix)) = parse_windows_path_prefix(self.as_str()) { - if prefix.is_verbatim() { - return '\\'; - } - } - '/' - } - - pub fn get_main_sep_str(&self) -> &'static str { - if let Ok((_, prefix)) = parse_windows_path_prefix(self.as_str()) { - if prefix.is_verbatim() { - return r"\\"; - } - } - "/" - } - - pub fn to_file_url(&self) -> Result { - let mut serialization = "file://".to_owned(); - let _host_start = serialization.len() as u32; - let (_host_end, _host) = path_to_file_url_segments(self, &mut serialization)?; - let u = Url::parse(&serialization)?; - Ok(u) - } -} - -impl AsRef for Path { - #[inline] - fn as_ref(&self) -> &str { - &self.inner - } -} - -impl fmt::Debug for Path { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.inner, formatter) - } -} - -impl fmt::Display for Path { - fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.inner, formatter) - } -} - -impl PartialEq for Path { - #[inline] - fn eq(&self, other: &Path) -> bool { - self.components() == other.components() - } -} - -impl Hash for Path { - fn hash(&self, h: &mut H) { - let path_str = self.as_str(); - let path_bytes = path_str.as_bytes(); - let (prefix_len, verbatim) = match parse_windows_path_prefix(path_str) { - Ok((_, prefix)) => { - prefix.hash(h); - (prefix.len(), prefix.is_verbatim()) - } - _ => (0, false), - }; - let bytes = &path_bytes[prefix_len..]; - - let mut component_start = 0; - let mut bytes_hashed = 0; - - for i in 0..bytes.len() { - let is_sep = if verbatim { - is_verbatim_sep(bytes[i] as char) - } else { - is_sep_byte(bytes[i] as char) - }; - if is_sep { - if i > component_start { - let to_hash = &bytes[component_start..i]; - h.write(to_hash); - bytes_hashed += to_hash.len(); - } - - // skip over separator and optionally a following CurDir item - // since components() would normalize these away. - component_start = i + 1; - - let tail = &bytes[component_start..]; - - if !verbatim { - component_start += match tail { - [b'.'] => 1, - [b'.', sep, ..] if is_sep_byte(*sep as char) => 1, - _ => 0, - }; - } - } - } - - if component_start < bytes.len() { - let to_hash = &bytes[component_start..]; - h.write(to_hash); - bytes_hashed += to_hash.len(); - } - - h.write_usize(bytes_hashed); - } -} - -impl Eq for Path {} - -#[allow(clippy::non_canonical_partial_ord_impl)] -impl PartialOrd for Path { - #[inline] - fn partial_cmp(&self, other: &Path) -> Option { - Some(compare_components(self.components(), other.components())) - } -} - -impl Ord for Path { - #[inline] - fn cmp(&self, other: &Path) -> cmp::Ordering { - compare_components(self.components(), other.components()) - } -} - -impl AsRef for Path { - #[inline] - fn as_ref(&self) -> &Path { - self - } -} - -impl AsRef for str { - #[inline] - fn as_ref(&self) -> &Path { - Path::new(self) - } -} - -impl AsRef for Cow<'_, str> { - #[inline] - fn as_ref(&self) -> &Path { - Path::new(self) - } -} - -impl AsRef for String { - #[inline] - fn as_ref(&self) -> &Path { - Path::new(self) - } -} - -impl AsRef for PathBuf { - #[inline] - fn as_ref(&self) -> &Path { - self - } -} - -impl<'a> IntoIterator for &'a PathBuf { - type Item = &'a str; - type IntoIter = Iter<'a>; - #[inline] - fn into_iter(self) -> Iter<'a> { - self.iter() - } -} - -impl<'a> IntoIterator for &'a Path { - type Item = &'a str; - type IntoIter = Iter<'a>; - #[inline] - fn into_iter(self) -> Iter<'a> { - self.iter() - } -} - -macro_rules! impl_cmp { - (<$($life:lifetime),*> $lhs:ty, $rhs: ty) => { - impl<$($life),*> PartialEq<$rhs> for $lhs { - #[inline] - fn eq(&self, other: &$rhs) -> bool { - ::eq(self, other) - } - } - - impl<$($life),*> PartialEq<$lhs> for $rhs { - #[inline] - fn eq(&self, other: &$lhs) -> bool { - ::eq(self, other) - } - } - - impl<$($life),*> PartialOrd<$rhs> for $lhs { - #[inline] - fn partial_cmp(&self, other: &$rhs) -> Option { - ::partial_cmp(self, other) - } - } - - impl<$($life),*> PartialOrd<$lhs> for $rhs { - #[inline] - fn partial_cmp(&self, other: &$lhs) -> Option { - ::partial_cmp(self, other) - } - } - }; -} - -impl_cmp!(<> PathBuf, Path); -impl_cmp!(<'a> PathBuf, &'a Path); -impl_cmp!(<'a> Cow<'a, Path>, Path); -impl_cmp!(<'a, 'b> Cow<'a, Path>, &'b Path); -impl_cmp!(<'a> Cow<'a, Path>, PathBuf); - -macro_rules! impl_cmp_str { - (<$($life:lifetime),*> $lhs:ty, $rhs: ty) => { - impl<$($life),*> PartialEq<$rhs> for $lhs { - #[inline] - fn eq(&self, other: &$rhs) -> bool { - ::eq(self, other.as_ref()) - } - } - - impl<$($life),*> PartialEq<$lhs> for $rhs { - #[inline] - fn eq(&self, other: &$lhs) -> bool { - ::eq(self.as_ref(), other) - } - } - - impl<$($life),*> PartialOrd<$rhs> for $lhs { - #[inline] - fn partial_cmp(&self, other: &$rhs) -> Option { - ::partial_cmp(self, other.as_ref()) - } - } - - impl<$($life),*> PartialOrd<$lhs> for $rhs { - #[inline] - fn partial_cmp(&self, other: &$lhs) -> Option { - ::partial_cmp(self.as_ref(), other) - } - } - }; -} - -impl_cmp_str!(<> PathBuf, str); -impl_cmp_str!(<'a> PathBuf, &'a str); -impl_cmp_str!(<'a> PathBuf, Cow<'a, str>); -impl_cmp_str!(<> PathBuf, String); -impl_cmp_str!(<> Path, str); -impl_cmp_str!(<'a> Path, &'a str); -impl_cmp_str!(<'a> Path, Cow<'a, str>); -impl_cmp_str!(<> Path, String); -impl_cmp_str!(<'a> &'a Path, str); -impl_cmp_str!(<'a, 'b> &'a Path, Cow<'b, str>); -impl_cmp_str!(<'a> &'a Path, String); - -impl fmt::Display for StripPrefixError { - #[allow(deprecated, deprecated_in_future)] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.description().fmt(f) - } -} - -impl Error for StripPrefixError { - #[allow(deprecated)] - fn description(&self) -> &str { - "prefix not found" - } -} - -pub fn absolute>(path: P) -> io::Result { - let path = path.as_ref(); - let path_str = path.as_str(); - if path_str.is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "cannot make an empty path absolute", - )); - } - let prefix = parse_windows_path_prefix(path_str); - - if prefix.map(|(_, p)| p.is_verbatim()).unwrap_or(false) { - if path_str.contains('\0') { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "strings passed to WinAPI cannot contain NULs", - )); - } - return Ok(PathBuf::from(path_str.to_owned())); - } - - let mut components = path.strip_prefix(".").unwrap_or(path).components(); - - let path_str = path.as_str(); - let mut normalized = if path.is_absolute() { - if path_str.starts_with("//") && !path_str.starts_with("///") { - components.next(); - PathBuf::from("//") - } else { - PathBuf::new() - } - } else { - PathBuf::new() - }; - normalized.extend(components); - - if path_str.ends_with('/') || path_str.ends_with('\\') { - normalized.push(""); - } - - Ok(normalized) -} - -#[cfg(test)] -mod tests { - - use url::Url; - - use crate::{url::PathToUrlError, Path, PathBuf}; - - #[test] - fn test_black_slash_and_slash_mix_path() { - let mut path = PathBuf::from(r"\\?\C:\Users\test\Desktop/1.txt"); - path.push("abc"); - assert_eq!(path.as_str(), r"\\?\C:\Users\test\Desktop/1.txt\abc"); - } - - #[test] - fn test_to_file_url() { - let test_fn = |source: &str| -> Result { - let p = Path::new(source); - let u: Url = p.to_file_url()?; - Ok(u.to_string()) - }; - assert_eq!( - test_fn(r"\\?\UNC\server\share\path").expect("parse success"), - "file://server/share/path" - ); - assert_eq!( - test_fn(r"\\?\C:\path").expect("parse success"), - "file:///C:/path" - ); - assert_eq!( - test_fn(r"\\server\share\path").expect("parse success"), - "file://server/share/path" - ); - assert_eq!( - test_fn(r"C:\path").expect("parse success"), - "file:///C:/path" - ); - assert!(matches!( - test_fn(r"\\?\abc\path"), - Err(PathToUrlError::NotSupportedPrefixError { .. }) - )); - assert!(matches!( - test_fn(r"\\.\device\path"), - Err(PathToUrlError::NotSupportedPrefixError { .. }) - )); - assert!(matches!( - test_fn(r"~/a"), - Err(PathToUrlError::PathNotAbsoluteError { .. }) - )); - } -} diff --git a/packages/quirks-path/src/url.rs b/packages/quirks-path/src/url.rs deleted file mode 100644 index 801385a..0000000 --- a/packages/quirks-path/src/url.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::{borrow::Cow, fmt::Write}; - -use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; - -use crate::{windows::parse_drive, Component, Path, Prefix}; - -const URL_FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); -const URL_PATH: &AsciiSet = &URL_FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); -const URL_PATH_SEGMENT: &AsciiSet = &URL_PATH.add(b'/').add(b'%'); - -#[derive(thiserror::Error, Debug)] -pub enum PathToUrlError { - #[error(transparent)] - UrlParseError(#[from] url::ParseError), - #[error("PathNotAbsoluteError {{ path = {path} }}")] - PathNotAbsoluteError { path: Cow<'static, str> }, - #[error("NotSupportedPrefixError {{ path = {path}, prefix = {prefix} }}")] - NotSupportedPrefixError { - path: Cow<'static, str>, - prefix: Cow<'static, str>, - }, - #[error("NotSupportedFirstComponentError {{ path = {path}, comp = {comp} }}")] - NotSupportedFirstComponentError { - path: Cow<'static, str>, - comp: Cow<'static, str>, - }, -} - -#[inline] -pub fn to_u32(i: usize) -> Result { - if i <= u32::MAX as usize { - Ok(i as u32) - } else { - Err(url::ParseError::Overflow) - } -} - -pub(crate) fn path_to_file_url_segments( - path: &Path, - serialization: &mut String, -) -> Result<(u32, Option<::url::Host>), PathToUrlError> { - if !path.is_absolute() { - return Err(PathToUrlError::PathNotAbsoluteError { - path: Cow::Owned(path.as_str().to_string()), - }); - } - let mut components = path.components(); - - let host_start = serialization.len() + 1; - let host_end; - let host_internal: Option>; - - match components.next() { - Some(Component::Prefix(ref p)) => match p.kind() { - Prefix::Disk { drive } | Prefix::VerbatimDisk { drive } => { - host_end = to_u32(serialization.len()).unwrap(); - host_internal = None; - serialization.push('/'); - serialization.push(drive); - serialization.push(':'); - } - Prefix::UNC { server, share } | Prefix::VerbatimUNC { server, share } => { - let host = url::Host::parse(server)?; - write!(serialization, "{}", host).unwrap(); - host_end = to_u32(serialization.len()).unwrap(); - host_internal = Some(host); - serialization.push('/'); - serialization.extend(percent_encode(share.as_bytes(), URL_PATH_SEGMENT)); - } - _ => { - return Err(PathToUrlError::NotSupportedPrefixError { - path: Cow::Owned(path.as_str().to_string()), - prefix: Cow::Owned(p.as_str().to_string()), - }); - } - }, - Some(Component::RootDir(_)) => { - let host_end = to_u32(serialization.len()).unwrap(); - let mut empty = true; - for component in components { - empty = false; - serialization.push('/'); - - serialization.extend(percent_encode( - component.as_str().as_bytes(), - URL_PATH_SEGMENT, - )); - } - - if empty { - serialization.push('/'); - } - return Ok((host_end, None)); - } - Some(comp) => { - return Err(PathToUrlError::NotSupportedFirstComponentError { - path: Cow::Owned(path.as_str().to_string()), - comp: Cow::Owned(comp.as_str().to_string()), - }); - } - None => { - return Err(PathToUrlError::NotSupportedFirstComponentError { - path: Cow::Owned(path.as_str().to_string()), - comp: Cow::Borrowed("null"), - }); - } - } - - let mut path_only_has_prefix = true; - for component in components { - if matches!(component, Component::RootDir(..)) { - continue; - } - - path_only_has_prefix = false; - let component = component.as_str(); - - serialization.push('/'); - serialization.extend(percent_encode(component.as_bytes(), URL_PATH_SEGMENT)); - } - - // A windows drive letter must end with a slash. - if serialization.len() > host_start - && matches!(parse_drive(&serialization[host_start..]), Ok(..)) - && path_only_has_prefix - { - serialization.push('/'); - } - - Ok((host_end, host_internal)) -} - -pub fn path_equals_as_file_url, B: AsRef>( - a: A, - b: B, -) -> Result { - let u1 = a.as_ref().to_file_url()?; - let u2 = b.as_ref().to_file_url()?; - - Ok(u1.as_str() == u2.as_str()) -} diff --git a/packages/quirks-path/src/windows.rs b/packages/quirks-path/src/windows.rs deleted file mode 100644 index 34fcc0c..0000000 --- a/packages/quirks-path/src/windows.rs +++ /dev/null @@ -1,138 +0,0 @@ -use nom::{ - bytes::complete::tag, - character::complete::{self, satisfy}, - combinator::peek, - error::{context, ContextError, Error, ErrorKind, ParseError}, - sequence::pair, - AsChar, IResult, InputIter, InputTakeAtPosition, -}; - -use crate::Prefix; - -fn non_slash(input: &str) -> IResult<&str, &str> { - input.split_at_position_complete(|item| item != '/') -} - -pub fn parse_drive(path: &str) -> IResult<&str, char> { - context("drive", satisfy(char::is_alpha))(path).map(|a: (&str, char)| a) -} - -pub fn parse_drive_exact(path: &str) -> IResult<&str, char> { - context("drive_exact", pair(parse_drive, complete::char(':')))(path) - .map(|(path, (drive, _))| (path, drive)) -} - -pub fn is_windows_verbatim_sep(c: char) -> bool { - c == '\\' -} - -pub fn is_windows_sep(c: char) -> bool { - c == '\\' || c == '/' -} - -pub fn parse_windows_next_component(path: &str, verbatim: bool) -> (&str, &str, &str) { - let separator = if verbatim { - is_windows_verbatim_sep - } else { - is_windows_sep - }; - let p = path.as_bytes(); - match p.position(|x| separator(x as char)) { - Some(separator_start) => { - let separator_end = separator_start + 1; - let component = &path[0..separator_start]; - let path_with_sep = &path[separator_start..]; - let path_without_sep = &path[separator_end..]; - - (component, path_with_sep, path_without_sep) - } - None => (path, "", ""), - } -} - -fn context_verify_error<'a>(input: &'a str, context: &'static str) -> nom::Err> { - nom::Err::Error(Error::add_context( - input, - context, - Error::from_error_kind(input, ErrorKind::Verify), - )) -} - -pub fn parse_windows_path_prefix(raw_path: &str) -> IResult<&str, Prefix<'_>> { - if let Ok((path, _)) = tag(r"\\")(raw_path) as IResult<&str, &str> { - if let Ok((path, _)) = tag(r"?\")(path) as IResult<&str, &str> { - if let Ok((path, _)) = peek(non_slash)(path) as IResult<&str, &str> { - if let Ok((path, _)) = tag(r"UNC\")(path) as IResult<&str, &str> { - let (server, _, other) = parse_windows_next_component(path, true); - let (share, next_input, _) = parse_windows_next_component(other, true); - - return Ok((next_input, Prefix::VerbatimUNC { server, share })); - } else if let Ok((path, drive)) = parse_drive_exact(path) { - return Ok((path, Prefix::VerbatimDisk { drive })); - } else { - let (prefix, next_input, _) = parse_windows_next_component(path, true); - return Ok((next_input, Prefix::Verbatim { prefix })); - } - } - } - if let Ok((path, _)) = tag(r".\")(path) as IResult<&str, &str> { - let (prefix, next_input, _) = parse_windows_next_component(path, false); - return Ok((next_input, Prefix::DeviceNS { device: prefix })); - } - let (server, _, other) = parse_windows_next_component(path, false); - let (share, next_input, _) = parse_windows_next_component(other, false); - - if !server.is_empty() && !share.is_empty() { - return Ok((next_input, Prefix::UNC { server, share })); - } - Err(context_verify_error(raw_path, "windows path prefix")) - } else if let Ok((path, drive)) = parse_drive_exact(raw_path) { - Ok((path, Prefix::Disk { drive })) - } else { - Err(context_verify_error(raw_path, "windows path prefix")) - } -} - -#[cfg(test)] -mod tests { - #[test] - fn test_parse_windows_path_prefix() { - use super::*; - assert_eq!( - parse_windows_path_prefix(r"\\?\UNC\server\share\path"), - Ok(( - r"\path", - Prefix::VerbatimUNC { - server: "server", - share: "share" - } - )) - ); - assert_eq!( - parse_windows_path_prefix(r"\\?\C:\path"), - Ok((r"\path", Prefix::VerbatimDisk { drive: 'C' })) - ); - assert_eq!( - parse_windows_path_prefix(r"\\server\share\path"), - Ok(( - r"\path", - Prefix::UNC { - server: "server", - share: "share" - } - )) - ); - assert_eq!( - parse_windows_path_prefix(r"C:\path"), - Ok((r"\path", Prefix::Disk { drive: 'C' })) - ); - assert_eq!( - parse_windows_path_prefix(r"\\.\device\path"), - Ok((r"\path", Prefix::DeviceNS { device: "device" })) - ); - assert_eq!( - parse_windows_path_prefix(r"\\?\abc\path"), - Ok((r"\path", Prefix::Verbatim { prefix: "abc" })) - ) - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ba7abb..f8ca06c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2907,7 +2907,7 @@ packages: '@opentelemetry/api': ^1.3.0 '@opentelemetry/instrumentation-lru-memoizer@0.43.0': - resolution: {integrity: sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1asweYrEb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg==} + resolution: {integrity: sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1aswcolor_eyre::eyreb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0