From 8543d2d61fbafde6222e9b95f9604b41570db477 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:26:08 +0800 Subject: [PATCH 01/19] Switch to `qrcode` lib --- Cargo.lock | 68 +++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 2 +- src/consts.rs | 4 +++ src/listing.rs | 41 ++++------------------------- src/main.rs | 52 ++++++++++++++++++------------------- src/renderer.rs | 40 +++++++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 69 deletions(-) create mode 100644 src/consts.rs diff --git a/Cargo.lock b/Cargo.lock index 174d5bc58..62c1ff0f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,12 @@ version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +[[package]] +name = "bytemuck" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" + [[package]] name = "byteorder" version = "1.4.3" @@ -522,6 +528,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.22" @@ -626,6 +638,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "comrak" version = "0.14.0" @@ -1072,7 +1090,7 @@ dependencies = [ "indexmap", "lasso", "num-bigint", - "num-rational", + "num-rational 0.4.1", "num-traits", "once_cell", "phf 0.9.0", @@ -1267,6 +1285,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-iter", + "num-rational 0.3.2", + "num-traits", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -1546,7 +1578,7 @@ dependencies = [ "port_check", "predicates", "pretty_assertions", - "qrcodegen", + "qrcode", "regex", "reqwest", "rstest", @@ -1629,6 +1661,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2024,10 +2078,14 @@ dependencies = [ ] [[package]] -name = "qrcodegen" -version = "1.8.0" +name = "qrcode" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] [[package]] name = "quote" diff --git a/Cargo.toml b/Cargo.toml index de73987d4..909488381 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ mime = "0.3" nanoid = "0.4" percent-encoding = "2" port_check = "0.1" -qrcodegen = "1" +qrcode = "0.12.0" rustls = { version = "0.20", optional = true } rustls-pemfile = { version = "1.0", optional = true } serde = { version = "1", features = ["derive"] } diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 000000000..07f47b0da --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,4 @@ +use qrcode::EcLevel; + +/// The error correction level to use for all QR code generation. +pub const QR_EC_LEVEL: EcLevel = EcLevel::M; diff --git a/src/listing.rs b/src/listing.rs index 82b4cdba0..f90c40c31 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -9,14 +9,14 @@ use actix_web::{HttpMessage, HttpRequest, HttpResponse}; use bytesize::ByteSize; use comrak::{markdown_to_html, ComrakOptions}; use percent_encoding::{percent_decode_str, utf8_percent_encode}; -use qrcodegen::{QrCode, QrCodeEcc}; +use qrcode::QrCode; use serde::Deserialize; use strum_macros::{Display, EnumString}; use crate::archive::ArchiveMethod; use crate::auth::CurrentUser; use crate::errors::{self, ContextualError}; -use crate::renderer; +use crate::{consts, renderer}; use self::percent_encode_sets::PATH_SEGMENT; @@ -220,10 +220,10 @@ pub fn directory_listing( // If the `qrcode` parameter is included in the url, then should respond to the QR code if let Some(url) = query_params.qrcode { - let res = match QrCode::encode_text(&url, QrCodeEcc::Medium) { + let res = match QrCode::with_error_correction_level(url, consts::QR_EC_LEVEL) { Ok(qr) => HttpResponse::Ok() - .append_header(("Content-Type", "image/svg+xml")) - .body(qr_to_svg_string(&qr, 2)), + .content_type("text/html; charset=utf-8") + .body(renderer::qr_code_page(&qr).into_string()), Err(err) => { log::error!("URL is invalid (too long?): {:?}", err); HttpResponse::UriTooLong().finish() @@ -407,34 +407,3 @@ pub fn extract_query_parameters(req: &HttpRequest) -> QueryParameters { } } } - -// Returns a string of SVG code for an image depicting -// the given QR Code, with the given number of border modules. -// The string always uses Unix newlines (\n), regardless of the platform. -fn qr_to_svg_string(qr: &QrCode, border: i32) -> String { - assert!(border >= 0, "Border must be non-negative"); - let mut result = String::new(); - result += "\n"; - result += "\n"; - let dimension = qr - .size() - .checked_add(border.checked_mul(2).unwrap()) - .unwrap(); - result += &format!( - "\n", dimension); - result += "\t\n"; - result += "\t\n"; - result += "\n"; - result -} diff --git a/src/main.rs b/src/main.rs index b46262db3..558dabb30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,13 +13,14 @@ use anyhow::Result; use clap::{crate_version, IntoApp, Parser}; use clap_complete::generate; use log::{error, warn}; -use qrcodegen::{QrCode, QrCodeEcc}; +use qrcode::QrCode; use yansi::{Color, Paint}; mod archive; mod args; mod auth; mod config; +mod consts; mod errors; mod file_upload; mod listing; @@ -239,7 +240,7 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { .iter() .filter(|url| !url.contains("//127.0.0.1:") && !url.contains("//[::1]:")) { - match QrCode::encode_text(url, QrCodeEcc::Low) { + match QrCode::with_error_correction_level(url, consts::QR_EC_LEVEL) { Ok(qr) => { println!("QR code for {}:", Color::Green.paint(url).bold()); print_qr(&qr); @@ -352,30 +353,27 @@ async fn css() -> impl Responder { .body(css) } -// Prints to the console two inverted QrCodes side by side. +// Prints to the console a normal and an inverted QrCode side by side. fn print_qr(qr: &QrCode) { - let border = 4; - let size = qr.size() + 2 * border; - - for y in (0..size).step_by(2) { - for x in 0..2 * size { - let inverted = x >= size; - let (x, y) = (x % size - border, y - border); - - //each char represents two vertical modules - let (mod1, mod2) = match inverted { - false => (qr.get_module(x, y), qr.get_module(x, y + 1)), - true => (!qr.get_module(x, y), !qr.get_module(x, y + 1)), - }; - let c = match (mod1, mod2) { - (false, false) => ' ', - (true, false) => '▀', - (false, true) => '▄', - (true, true) => '█', - }; - print!("{0}", c); - } - println!(); - } - println!(); + use qrcode::render::unicode::Dense1x2; + + let normal = qr + .render() + .quiet_zone(true) + .dark_color(Dense1x2::Dark) + .light_color(Dense1x2::Light) + .build(); + let inverted = qr + .render() + .quiet_zone(true) + .dark_color(Dense1x2::Light) + .light_color(Dense1x2::Dark) + .build(); + let codes = normal + .lines() + .zip(inverted.lines()) + .map(|(l, r)| format!("{} {}", l, r)) + .collect::>() + .join("\n"); + println!("{}", codes); } diff --git a/src/renderer.rs b/src/renderer.rs index 7ec48b0e1..48b74b6d4 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -3,6 +3,7 @@ use chrono::{DateTime, Utc}; use chrono_humanize::Humanize; use clap::{crate_name, crate_version}; use maud::{html, Markup, PreEscaped, DOCTYPE}; +use qrcode::QrCode; use std::time::SystemTime; use strum::IntoEnumIterator; @@ -224,6 +225,45 @@ pub fn raw(entries: Vec, is_root: bool) -> Markup { } } +/// Renders the QR code page +pub fn qr_code_page(qr: &QrCode) -> Markup { + use qrcode::render::svg; + + html! { + (DOCTYPE) + html { + body { + // make QR code expand and fill page + style { + (PreEscaped("\ + html {\ + width: 100vw;\ + height: 100vh;\ + }\ + body {\ + width: 100%;\ + height: 100%;\ + margin: 0;\ + display: grid;\ + align-items: center;\ + justify-items: center;\ + }\ + svg {\ + width: 80%;\ + height: 80%;\ + }\ + ")) + } + (PreEscaped(qr.render() + .quiet_zone(false) + .dark_color(svg::Color("#000000")) + .light_color(svg::Color("#ffffff")) + .build())) + } + } + } +} + // Partial: version footer fn version_footer() -> Markup { html! { From c2ece55040d4f1f03e189636895645435d762322 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Wed, 20 Jul 2022 20:09:55 +0800 Subject: [PATCH 02/19] Fix tests --- Cargo.lock | 7 ++++ Cargo.toml | 1 + src/renderer.rs | 2 +- tests/qrcode.rs | 85 +++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 77 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62c1ff0f1..e84e76b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -817,6 +817,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" +[[package]] +name = "fake-tty" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1caac348c61d1f9a7d43c3629abc75540754a4971944953f84b98cb8280e3971" + [[package]] name = "fancy-regex" version = "0.7.1" @@ -1563,6 +1569,7 @@ dependencies = [ "clap_complete", "clap_mangen", "comrak", + "fake-tty", "futures", "get_if_addrs", "grass", diff --git a/Cargo.toml b/Cargo.toml index 909488381..1bac97ea2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ tls = ["rustls", "rustls-pemfile", "actix-web/rustls"] [dev-dependencies] assert_cmd = "2" assert_fs = "1" +fake-tty = "0.2.0" predicates = "2" pretty_assertions = "1.2" regex = "1" diff --git a/src/renderer.rs b/src/renderer.rs index 48b74b6d4..95aeb5fb3 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -232,7 +232,7 @@ pub fn qr_code_page(qr: &QrCode) -> Markup { html! { (DOCTYPE) html { - body { + body.qr_code_page { // make QR code expand and fill page style { (PreEscaped("\ diff --git a/tests/qrcode.rs b/tests/qrcode.rs index a9c27fe85..eb37abc07 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -1,43 +1,94 @@ mod fixtures; -use fixtures::{server, server_no_stderr, Error, TestServer}; +use assert_cmd::prelude::CommandCargoExt; +use assert_fs::TempDir; +use fixtures::{port, server_no_stderr, tmpdir, Error, TestServer}; use reqwest::StatusCode; use rstest::rstest; -use select::document::Document; -use select::predicate::Attr; -use std::iter::repeat_with; +use std::process::Command; +use std::thread::sleep; +use std::time::Duration; + +fn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result { + use fake_tty::{bash_command, get_stdout}; + + let cmd = { + let bin = template.get_program().to_str().expect("not UTF8"); + let args = template + .get_args() + .map(|s| s.to_str().expect("not UTF8")) + .collect::>() + .join(" "); + format!("{} {}", bin, args) + }; + let mut child = bash_command(&cmd).spawn()?; + + sleep(Duration::from_secs(1)); + + child.kill()?; + let output = child.wait_with_output().expect("Failed to read stdout"); + let all_text = get_stdout(output.stdout)?; + + Ok(all_text) +} #[rstest] -fn hide_qrcode_element(server: TestServer) -> Result<(), Error> { - let body = reqwest::blocking::get(server.url())?.error_for_status()?; - let parsed = Document::from_read(body)?; - assert!(parsed.find(Attr("id", "qrcode")).next().is_none()); +fn qrcode_hidden_in_tty_when_disabled(tmpdir: TempDir, port: u16) -> Result<(), Error> { + let mut template = Command::cargo_bin("miniserve")?; + template.arg("-p").arg(port.to_string()).arg(tmpdir.path()); + let output = run_in_faketty_kill_and_get_stdout(&template)?; + + assert!(!output.contains("QR code for ")); Ok(()) } #[rstest] -fn show_qrcode_element(#[with(&["-q"])] server: TestServer) -> Result<(), Error> { - let body = reqwest::blocking::get(server.url())?.error_for_status()?; - let parsed = Document::from_read(body)?; - assert!(parsed.find(Attr("id", "qrcode")).next().is_some()); +fn qrcode_shown_in_tty_when_enabled(tmpdir: TempDir, port: u16) -> Result<(), Error> { + let mut template = Command::cargo_bin("miniserve")?; + template + .arg("-p") + .arg(port.to_string()) + .arg("-q") + .arg(tmpdir.path()); + + let output = run_in_faketty_kill_and_get_stdout(&template)?; + + assert!(output.contains("QR code for ")); + Ok(()) +} + +#[rstest] +fn qrcode_hidden_in_non_tty_when_enabled(tmpdir: TempDir, port: u16) -> Result<(), Error> { + let mut child = Command::cargo_bin("miniserve")? + .arg("-p") + .arg(port.to_string()) + .arg("-q") + .arg(tmpdir.path()) + .spawn()?; + + sleep(Duration::from_secs(1)); + + child.kill()?; + let output = child.wait_with_output().expect("Failed to read stdout"); + let stdout = String::from_utf8(output.stdout)?; + assert!(!stdout.contains("QR code for ")); Ok(()) } #[rstest] fn get_svg_qrcode(#[from(server_no_stderr)] server: TestServer) -> Result<(), Error> { // Ok - let resp = reqwest::blocking::get(server.url().join("/?qrcode=test")?)?; + let resp = reqwest::blocking::get(server.url().join("?qrcode=test")?)?; assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.headers()["Content-Type"], "image/svg+xml"); let body = resp.text()?; - assert!(body.starts_with(" Date: Thu, 21 Jul 2022 14:33:37 +0800 Subject: [PATCH 03/19] Use `mime` for content type --- src/listing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/listing.rs b/src/listing.rs index f90c40c31..b201ebbe1 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -222,7 +222,7 @@ pub fn directory_listing( if let Some(url) = query_params.qrcode { let res = match QrCode::with_error_correction_level(url, consts::QR_EC_LEVEL) { Ok(qr) => HttpResponse::Ok() - .content_type("text/html; charset=utf-8") + .content_type(mime::TEXT_HTML_UTF_8) .body(renderer::qr_code_page(&qr).into_string()), Err(err) => { log::error!("URL is invalid (too long?): {:?}", err); From 3c0d0cfffe6a647ff91bd74ebe0c3085032fc646 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Sat, 23 Jul 2022 00:46:55 +0800 Subject: [PATCH 04/19] Disable broken tests for Windows --- Cargo.toml | 5 ++++- tests/qrcode.rs | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1bac97ea2..1876320fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,6 @@ tls = ["rustls", "rustls-pemfile", "actix-web/rustls"] [dev-dependencies] assert_cmd = "2" assert_fs = "1" -fake-tty = "0.2.0" predicates = "2" pretty_assertions = "1.2" regex = "1" @@ -78,5 +77,9 @@ rstest = "0.15" select = "0.5" url = "2" +[target.'cfg(not(windows))'.dev-dependencies] +# fake_tty does not support Windows for now +fake-tty = "0.2.0" + [build-dependencies] grass = "0.11" diff --git a/tests/qrcode.rs b/tests/qrcode.rs index eb37abc07..21dae6a12 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -9,6 +9,7 @@ use std::process::Command; use std::thread::sleep; use std::time::Duration; +#[cfg(not(windows))] fn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result { use fake_tty::{bash_command, get_stdout}; @@ -33,6 +34,8 @@ fn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result Result<(), Error> { let mut template = Command::cargo_bin("miniserve")?; template.arg("-p").arg(port.to_string()).arg(tmpdir.path()); @@ -44,6 +47,8 @@ fn qrcode_hidden_in_tty_when_disabled(tmpdir: TempDir, port: u16) -> Result<(), } #[rstest] +// Disabled for Windows because `fake_tty` does not currently support it. +#[cfg(not(windows))] fn qrcode_shown_in_tty_when_enabled(tmpdir: TempDir, port: u16) -> Result<(), Error> { let mut template = Command::cargo_bin("miniserve")?; template From a99d8bdcadf4f822bb86a2710e5d73783ac7097a Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Tue, 26 Jul 2022 12:27:14 +0800 Subject: [PATCH 05/19] Move QR code page style to `style.scss` --- data/style.scss | 22 ++++++++++++++++++++++ src/listing.rs | 23 ++++++++--------------- src/renderer.rs | 47 +++++++++++++++++------------------------------ 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/data/style.scss b/data/style.scss index 0fcc90d77..a5b3707a5 100644 --- a/data/style.scss +++ b/data/style.scss @@ -17,7 +17,29 @@ $themes: squirrel, archlinux, monokai, zenburn; @return $s; } +// make QR code expand and fill page +// -------------------------------------------------- +html.qr_code_page { + width: 100vw; + height: 100vh; +} + +html.qr_code_page body { + width: 100%; + height: 100%; + margin: 0; + display: grid; + align-items: center; + justify-items: center; +} + +html.qr_code_page svg { + width: 80%; + height: 80%; +} + +// -------------------------------------------------- html { font-smoothing: antialiased; diff --git a/src/listing.rs b/src/listing.rs index b201ebbe1..66e541c0b 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -154,10 +154,7 @@ pub async fn file_handler(req: HttpRequest) -> actix_web::Result io::Result { +pub fn directory_listing(dir: &actix_files::Directory, req: &HttpRequest) -> io::Result { let extensions = req.extensions(); let current_user: Option<&CurrentUser> = extensions.get::(); @@ -198,8 +195,7 @@ pub fn directory_listing( } Component::Normal(s) => { name = s.to_string_lossy().to_string(); - link_accumulator - .push_str(&(utf8_percent_encode(&name, PATH_SEGMENT).to_string() + "/")); + link_accumulator.push_str(&(utf8_percent_encode(&name, PATH_SEGMENT).to_string() + "/")); } _ => name = "".to_string(), }; @@ -223,7 +219,7 @@ pub fn directory_listing( let res = match QrCode::with_error_correction_level(url, consts::QR_EC_LEVEL) { Ok(qr) => HttpResponse::Ok() .content_type(mime::TEXT_HTML_UTF_8) - .body(renderer::qr_code_page(&qr).into_string()), + .body(renderer::qr_code_page(&breadcrumbs, &qr, conf).into_string()), Err(err) => { log::error!("URL is invalid (too long?): {:?}", err); HttpResponse::UriTooLong().finish() @@ -301,9 +297,9 @@ pub fn directory_listing( } match query_params.sort.unwrap_or(SortingMethod::Name) { - SortingMethod::Name => entries.sort_by(|e1, e2| { - alphanumeric_sort::compare_str(e1.name.to_lowercase(), e2.name.to_lowercase()) - }), + SortingMethod::Name => { + entries.sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.to_lowercase(), e2.name.to_lowercase())) + } SortingMethod::Size => entries.sort_by(|e1, e2| { // If we can't get the size of the entry (directory for instance) // let's consider it's 0b @@ -371,10 +367,7 @@ pub fn directory_listing( .content_type(archive_method.content_type()) .append_header(archive_method.content_encoding()) .append_header(("Content-Transfer-Encoding", "binary")) - .append_header(( - "Content-Disposition", - format!("attachment; filename={:?}", file_name), - )) + .append_header(("Content-Disposition", format!("attachment; filename={:?}", file_name))) .body(actix_web::body::BodyStream::new(rx)), )) } else { @@ -386,7 +379,7 @@ pub fn directory_listing( readme, is_root, query_params, - breadcrumbs, + &breadcrumbs, &encoded_dir, conf, current_user, diff --git a/src/renderer.rs b/src/renderer.rs index 95aeb5fb3..0154ce092 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -18,7 +18,7 @@ pub fn page( readme: Option<(String, String)>, is_root: bool, query_params: QueryParameters, - breadcrumbs: Vec, + breadcrumbs: &[Breadcrumb], encoded_dir: &str, conf: &MiniserveConfig, current_user: Option<&CurrentUser>, @@ -34,11 +34,7 @@ pub fn page( let upload_action = build_upload_action(&upload_route, encoded_dir, sort_method, sort_order); let mkdir_action = build_mkdir_action(&upload_route, encoded_dir); - let title_path = breadcrumbs - .iter() - .map(|el| el.name.clone()) - .collect::>() - .join("/"); + let title_path = breadcrumbs_to_path_string(breadcrumbs); html! { (DOCTYPE) @@ -226,34 +222,16 @@ pub fn raw(entries: Vec, is_root: bool) -> Markup { } /// Renders the QR code page -pub fn qr_code_page(qr: &QrCode) -> Markup { +pub fn qr_code_page(breadcrumbs: &[Breadcrumb], qr: &QrCode, conf: &MiniserveConfig) -> Markup { use qrcode::render::svg; + let title = breadcrumbs_to_path_string(breadcrumbs); + html! { (DOCTYPE) - html { - body.qr_code_page { - // make QR code expand and fill page - style { - (PreEscaped("\ - html {\ - width: 100vw;\ - height: 100vh;\ - }\ - body {\ - width: 100%;\ - height: 100%;\ - margin: 0;\ - display: grid;\ - align-items: center;\ - justify-items: center;\ - }\ - svg {\ - width: 80%;\ - height: 80%;\ - }\ - ")) - } + html.qr_code_page { + (page_header(&title, false, &conf.favicon_route, &conf.css_route)) + body { (PreEscaped(qr.render() .quiet_zone(false) .dark_color(svg::Color("#000000")) @@ -264,6 +242,15 @@ pub fn qr_code_page(qr: &QrCode) -> Markup { } } +/// Build a path string from a list of breadcrumbs. +fn breadcrumbs_to_path_string(breadcrumbs: &[Breadcrumb]) -> String { + breadcrumbs + .iter() + .map(|el| el.name.clone()) + .collect::>() + .join("/") +} + // Partial: version footer fn version_footer() -> Markup { html! { From ef3ec4039e220d3dc4a83ac267101c4dc45dace8 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Tue, 26 Jul 2022 13:10:21 +0800 Subject: [PATCH 06/19] Use low EC level for QR code --- src/consts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/consts.rs b/src/consts.rs index 07f47b0da..f10570a51 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,4 +1,4 @@ use qrcode::EcLevel; /// The error correction level to use for all QR code generation. -pub const QR_EC_LEVEL: EcLevel = EcLevel::M; +pub const QR_EC_LEVEL: EcLevel = EcLevel::L; From 8c5a5b6bce1c846279aa8fb215d6b412ca134338 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Tue, 26 Jul 2022 15:20:10 +0800 Subject: [PATCH 07/19] Fix QR test screwing up terminal --- tests/qrcode.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/qrcode.rs b/tests/qrcode.rs index 21dae6a12..32d943868 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -11,6 +11,8 @@ use std::time::Duration; #[cfg(not(windows))] fn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result { + use std::process::Stdio; + use fake_tty::{bash_command, get_stdout}; let cmd = { @@ -22,7 +24,7 @@ fn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result Date: Mon, 22 Aug 2022 17:17:33 +0800 Subject: [PATCH 08/19] Remove separators in CSS --- data/style.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/data/style.scss b/data/style.scss index a5b3707a5..187b445d8 100644 --- a/data/style.scss +++ b/data/style.scss @@ -17,9 +17,6 @@ $themes: squirrel, archlinux, monokai, zenburn; @return $s; } -// make QR code expand and fill page -// -------------------------------------------------- - html.qr_code_page { width: 100vw; height: 100vh; @@ -39,8 +36,6 @@ html.qr_code_page svg { height: 80%; } -// -------------------------------------------------- - html { font-smoothing: antialiased; text-rendering: optimizeLegibility; From 5e32400e20da831cee16272f0d1d52be6b7928ea Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Mon, 22 Aug 2022 17:33:06 +0800 Subject: [PATCH 09/19] Use SASS nesting --- data/style.scss | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/data/style.scss b/data/style.scss index 187b445d8..e3dc17e20 100644 --- a/data/style.scss +++ b/data/style.scss @@ -20,22 +20,23 @@ $themes: squirrel, archlinux, monokai, zenburn; html.qr_code_page { width: 100vw; height: 100vh; -} -html.qr_code_page body { - width: 100%; - height: 100%; - margin: 0; - display: grid; - align-items: center; - justify-items: center; -} + body { + width: 100%; + height: 100%; + margin: 0; + display: grid; + align-items: center; + justify-items: center; + } -html.qr_code_page svg { - width: 80%; - height: 80%; + svg { + width: 80%; + height: 80%; + } } + html { font-smoothing: antialiased; text-rendering: optimizeLegibility; From 682a84d960f2fc915d176ef1b64196c9ba6fc776 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Mon, 22 Aug 2022 18:21:23 +0800 Subject: [PATCH 10/19] Fix QR code test --- tests/qrcode.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/qrcode.rs b/tests/qrcode.rs index 32d943868..c2bebd685 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -5,14 +5,12 @@ use assert_fs::TempDir; use fixtures::{port, server_no_stderr, tmpdir, Error, TestServer}; use reqwest::StatusCode; use rstest::rstest; -use std::process::Command; +use std::process::{Command, Stdio}; use std::thread::sleep; use std::time::Duration; #[cfg(not(windows))] fn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result { - use std::process::Stdio; - use fake_tty::{bash_command, get_stdout}; let cmd = { @@ -72,6 +70,8 @@ fn qrcode_hidden_in_non_tty_when_enabled(tmpdir: TempDir, port: u16) -> Result<( .arg(port.to_string()) .arg("-q") .arg(tmpdir.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) .spawn()?; sleep(Duration::from_secs(1)); From 4fca60cc5b06fff1aa7f9e52b774d868dc7bfe1a Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:07:24 +0800 Subject: [PATCH 11/19] Fix rustfmt complaints --- src/listing.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/listing.rs b/src/listing.rs index 66e541c0b..bc8dcfb8e 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -154,7 +154,10 @@ pub async fn file_handler(req: HttpRequest) -> actix_web::Result io::Result { +pub fn directory_listing( + dir: &actix_files::Directory, + req: &HttpRequest, +) -> io::Result { let extensions = req.extensions(); let current_user: Option<&CurrentUser> = extensions.get::(); @@ -195,7 +198,8 @@ pub fn directory_listing(dir: &actix_files::Directory, req: &HttpRequest) -> io: } Component::Normal(s) => { name = s.to_string_lossy().to_string(); - link_accumulator.push_str(&(utf8_percent_encode(&name, PATH_SEGMENT).to_string() + "/")); + link_accumulator + .push_str(&(utf8_percent_encode(&name, PATH_SEGMENT).to_string() + "/")); } _ => name = "".to_string(), }; @@ -297,9 +301,9 @@ pub fn directory_listing(dir: &actix_files::Directory, req: &HttpRequest) -> io: } match query_params.sort.unwrap_or(SortingMethod::Name) { - SortingMethod::Name => { - entries.sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.to_lowercase(), e2.name.to_lowercase())) - } + SortingMethod::Name => entries.sort_by(|e1, e2| { + alphanumeric_sort::compare_str(e1.name.to_lowercase(), e2.name.to_lowercase()) + }), SortingMethod::Size => entries.sort_by(|e1, e2| { // If we can't get the size of the entry (directory for instance) // let's consider it's 0b @@ -367,7 +371,10 @@ pub fn directory_listing(dir: &actix_files::Directory, req: &HttpRequest) -> io: .content_type(archive_method.content_type()) .append_header(archive_method.content_encoding()) .append_header(("Content-Transfer-Encoding", "binary")) - .append_header(("Content-Disposition", format!("attachment; filename={:?}", file_name))) + .append_header(( + "Content-Disposition", + format!("attachment; filename={:?}", file_name), + )) .body(actix_web::body::BodyStream::new(rx)), )) } else { From 9f7d5a8aeaf252fb51d88b965b3a324e8e815f37 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 1 Sep 2022 19:37:39 +0800 Subject: [PATCH 12/19] Fix drop-down QR code --- Cargo.lock | 1 + Cargo.toml | 3 +- data/style.scss | 25 +++---------- src/listing.rs | 26 +++++--------- src/renderer.rs | 95 +++++++++++++++++++++++++++++-------------------- 5 files changed, 73 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e84e76b26..cc8d3f623 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1576,6 +1576,7 @@ dependencies = [ "hex", "http", "httparse", + "lazy_static", "libflate", "log", "maud", diff --git a/Cargo.toml b/Cargo.toml index 1876320fb..b7eb5208f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ get_if_addrs = "0.5" hex = "0.4" http = "0.2" httparse = "1" +lazy_static = "1.4.0" libflate = "1" log = "0.4" maud = "0.23" @@ -45,6 +46,7 @@ nanoid = "0.4" percent-encoding = "2" port_check = "0.1" qrcode = "0.12.0" +regex = "1" rustls = { version = "0.20", optional = true } rustls-pemfile = { version = "1.0", optional = true } serde = { version = "1", features = ["derive"] } @@ -71,7 +73,6 @@ assert_cmd = "2" assert_fs = "1" predicates = "2" pretty_assertions = "1.2" -regex = "1" reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false } rstest = "0.15" select = "0.5" diff --git a/data/style.scss b/data/style.scss index e3dc17e20..87ecc4c58 100644 --- a/data/style.scss +++ b/data/style.scss @@ -17,26 +17,6 @@ $themes: squirrel, archlinux, monokai, zenburn; @return $s; } -html.qr_code_page { - width: 100vw; - height: 100vh; - - body { - width: 100%; - height: 100%; - margin: 0; - display: grid; - align-items: center; - justify-items: center; - } - - svg { - width: 80%; - height: 80%; - } -} - - html { font-smoothing: antialiased; text-rendering: optimizeLegibility; @@ -198,8 +178,11 @@ nav .qrcode { background: var(--switch_theme_background); } -nav .qrcode img { +nav .qrcode svg { display: block; + border: 0.3rem; + border-style: solid; + border-color: #ffffff; } nav .theme { diff --git a/src/listing.rs b/src/listing.rs index bc8dcfb8e..b00064974 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -9,14 +9,13 @@ use actix_web::{HttpMessage, HttpRequest, HttpResponse}; use bytesize::ByteSize; use comrak::{markdown_to_html, ComrakOptions}; use percent_encoding::{percent_decode_str, utf8_percent_encode}; -use qrcode::QrCode; use serde::Deserialize; use strum_macros::{Display, EnumString}; use crate::archive::ArchiveMethod; use crate::auth::CurrentUser; use crate::errors::{self, ContextualError}; -use crate::{consts, renderer}; +use crate::renderer; use self::percent_encode_sets::PATH_SEGMENT; @@ -38,7 +37,6 @@ pub struct QueryParameters { pub order: Option, pub raw: Option, pub mkdir_name: Option, - qrcode: Option, download: Option, } @@ -166,6 +164,13 @@ pub fn directory_listing( let base = Path::new(serve_path); let random_route_abs = format!("/{}", conf.route_prefix); + let abs_url = format!( + "{}://{}{}", + req.connection_info().scheme(), + req.connection_info().host(), + req.uri() + ); + dbg!(&abs_url); let is_root = base.parent().is_none() || Path::new(&req.path()) == Path::new(&random_route_abs); let encoded_dir = match base.strip_prefix(random_route_abs) { @@ -218,20 +223,6 @@ pub fn directory_listing( let query_params = extract_query_parameters(req); - // If the `qrcode` parameter is included in the url, then should respond to the QR code - if let Some(url) = query_params.qrcode { - let res = match QrCode::with_error_correction_level(url, consts::QR_EC_LEVEL) { - Ok(qr) => HttpResponse::Ok() - .content_type(mime::TEXT_HTML_UTF_8) - .body(renderer::qr_code_page(&breadcrumbs, &qr, conf).into_string()), - Err(err) => { - log::error!("URL is invalid (too long?): {:?}", err); - HttpResponse::UriTooLong().finish() - } - }; - return Ok(ServiceResponse::new(req.clone(), res)); - } - let mut entries: Vec = Vec::new(); let mut readme: Option<(String, String)> = None; @@ -384,6 +375,7 @@ pub fn directory_listing( renderer::page( entries, readme, + abs_url, is_root, query_params, &breadcrumbs, diff --git a/src/renderer.rs b/src/renderer.rs index 0154ce092..614672f90 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -2,12 +2,15 @@ use actix_web::http::StatusCode; use chrono::{DateTime, Utc}; use chrono_humanize::Humanize; use clap::{crate_name, crate_version}; +use lazy_static::lazy_static; use maud::{html, Markup, PreEscaped, DOCTYPE}; -use qrcode::QrCode; +use qrcode::{types::QrError, QrCode}; +use regex::Regex; use std::time::SystemTime; use strum::IntoEnumIterator; use crate::auth::CurrentUser; +use crate::consts; use crate::listing::{Breadcrumb, Entry, QueryParameters, SortingMethod, SortingOrder}; use crate::{archive::ArchiveMethod, MiniserveConfig}; @@ -16,6 +19,7 @@ use crate::{archive::ArchiveMethod, MiniserveConfig}; pub fn page( entries: Vec, readme: Option<(String, String)>, + abs_url: impl AsRef, is_root: bool, query_params: QueryParameters, breadcrumbs: &[Breadcrumb], @@ -86,7 +90,10 @@ pub fn page( } } } - (color_scheme_selector(conf.show_qrcode, conf.hide_theme_selector)) + nav { + (qr_spoiler(conf.show_qrcode, abs_url)) + (color_scheme_selector(conf.hide_theme_selector)) + } div.container { span #top { } h1.title dir="ltr" { @@ -221,25 +228,30 @@ pub fn raw(entries: Vec, is_root: bool) -> Markup { } } -/// Renders the QR code page -pub fn qr_code_page(breadcrumbs: &[Breadcrumb], qr: &QrCode, conf: &MiniserveConfig) -> Markup { +/// Renders the QR code SVG +fn qr_code_svg(url: impl AsRef, no_width_height_attr: bool) -> Result { use qrcode::render::svg; - - let title = breadcrumbs_to_path_string(breadcrumbs); - - html! { - (DOCTYPE) - html.qr_code_page { - (page_header(&title, false, &conf.favicon_route, &conf.css_route)) - body { - (PreEscaped(qr.render() - .quiet_zone(false) - .dark_color(svg::Color("#000000")) - .light_color(svg::Color("#ffffff")) - .build())) - } + let qr = QrCode::with_error_correction_level(url.as_ref(), consts::QR_EC_LEVEL)?; + let mut svg = qr + .render() + .quiet_zone(false) + .dark_color(svg::Color("#000000")) + .light_color(svg::Color("#ffffff")) + .build(); + + if no_width_height_attr { + // HACK: qrcode crate hard-codes height and width into SVG's attributes. + // This behaviour may be undesirable because we want it to fit its HTML container. + // The proper way to remove them is to use a XML parser, but regex is good enough for a + // simple case like this. + lazy_static! { + static ref RE: Regex = + Regex::new(r#"(?P.+?>)"#).unwrap(); } + svg = RE.replace(&svg, "$front$aft").to_string(); } + + Ok(svg) } /// Build a path string from a list of breadcrumbs. @@ -317,30 +329,37 @@ const THEME_PICKER_CHOICES: &[(&str, &str)] = &[ pub const THEME_SLUGS: &[&str] = &["squirrel", "archlinux", "zenburn", "monokai"]; -/// Partial: color scheme selector -fn color_scheme_selector(show_qrcode: bool, hide_theme_selector: bool) -> Markup { +/// Partial: qr code spoiler +fn qr_spoiler(show_qrcode: bool, content: impl AsRef) -> Markup { html! { - nav { - @if show_qrcode { - div { - p onmouseover="document.querySelector('#qrcode').src = `?qrcode=${encodeURIComponent(window.location.href)}`" { - "QR code" - } - div.qrcode { - img #qrcode alt="QR code" title="QR code of this page"; + @if show_qrcode { + div { + p { + "QR code" + } + div.qrcode #qrcode { + @match qr_code_svg(content, true) { + Ok(svg) => (PreEscaped(svg)), + Err(err) => (format!("QR generation error: {}", err)), } } } - @if !hide_theme_selector { - div { - p { - "Change theme..." - } - ul.theme { - @for color_scheme in THEME_PICKER_CHOICES { - li.(format!("theme_{}", color_scheme.1)) { - (color_scheme_link(color_scheme)) - } + } + } +} + +/// Partial: color scheme selector +fn color_scheme_selector(hide_theme_selector: bool) -> Markup { + html! { + @if !hide_theme_selector { + div { + p { + "Change theme..." + } + ul.theme { + @for color_scheme in THEME_PICKER_CHOICES { + li.(format!("theme_{}", color_scheme.1)) { + (color_scheme_link(color_scheme)) } } } From 97db5065f4c02835509560b920cbd9680eac3f78 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 1 Sep 2022 19:41:49 +0800 Subject: [PATCH 13/19] Remove outdated test --- tests/qrcode.rs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/tests/qrcode.rs b/tests/qrcode.rs index c2bebd685..162c33af6 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -2,8 +2,7 @@ mod fixtures; use assert_cmd::prelude::CommandCargoExt; use assert_fs::TempDir; -use fixtures::{port, server_no_stderr, tmpdir, Error, TestServer}; -use reqwest::StatusCode; +use fixtures::{port, tmpdir, Error}; use rstest::rstest; use std::process::{Command, Stdio}; use std::thread::sleep; @@ -83,22 +82,3 @@ fn qrcode_hidden_in_non_tty_when_enabled(tmpdir: TempDir, port: u16) -> Result<( assert!(!stdout.contains("QR code for ")); Ok(()) } - -#[rstest] -fn get_svg_qrcode(#[from(server_no_stderr)] server: TestServer) -> Result<(), Error> { - // Ok - let resp = reqwest::blocking::get(server.url().join("?qrcode=test")?)?; - - assert_eq!(resp.status(), StatusCode::OK); - let body = resp.text()?; - assert!(body.contains("qr_code_page")); - assert!(body.contains(" Date: Thu, 1 Sep 2022 20:13:37 +0800 Subject: [PATCH 14/19] Remove leftover `dbg!` --- src/listing.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/listing.rs b/src/listing.rs index b00064974..c0104b1ac 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -170,7 +170,6 @@ pub fn directory_listing( req.connection_info().host(), req.uri() ); - dbg!(&abs_url); let is_root = base.parent().is_none() || Path::new(&req.path()) == Path::new(&random_route_abs); let encoded_dir = match base.strip_prefix(random_route_abs) { From aa2dda7885a83354d1519eef62397b90189a9802 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 1 Sep 2022 20:20:27 +0800 Subject: [PATCH 15/19] Switch to `fast_qr` crate --- Cargo.lock | 78 +++++++------------------------------------------ Cargo.toml | 5 ++-- data/style.scss | 3 -- src/consts.rs | 4 +-- src/main.rs | 33 +++------------------ src/renderer.rs | 36 +++++++---------------- 6 files changed, 29 insertions(+), 130 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc8d3f623..6b02de773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,12 +474,6 @@ version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" -[[package]] -name = "bytemuck" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" - [[package]] name = "byteorder" version = "1.4.3" @@ -528,12 +522,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "checked_int_cast" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" - [[package]] name = "chrono" version = "0.4.22" @@ -638,12 +626,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "comrak" version = "0.14.0" @@ -833,6 +815,15 @@ dependencies = [ "regex", ] +[[package]] +name = "fast_qr" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b798dfd6e29b85c0bcf434272db4cde0100ab4d82c5db0a4f422e77b30d0b4e4" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -1096,7 +1087,7 @@ dependencies = [ "indexmap", "lasso", "num-bigint", - "num-rational 0.4.1", + "num-rational", "num-traits", "once_cell", "phf 0.9.0", @@ -1291,20 +1282,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "image" -version = "0.23.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "num-iter", - "num-rational 0.3.2", - "num-traits", -] - [[package]] name = "indexmap" version = "1.9.1" @@ -1570,13 +1547,13 @@ dependencies = [ "clap_mangen", "comrak", "fake-tty", + "fast_qr", "futures", "get_if_addrs", "grass", "hex", "http", "httparse", - "lazy_static", "libflate", "log", "maud", @@ -1586,7 +1563,6 @@ dependencies = [ "port_check", "predicates", "pretty_assertions", - "qrcode", "regex", "reqwest", "rstest", @@ -1669,28 +1645,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.4.1" @@ -2085,16 +2039,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "qrcode" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" -dependencies = [ - "checked_int_cast", - "image", -] - [[package]] name = "quote" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index b7eb5208f..1294a6de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,12 +32,12 @@ clap = { version = "3.2", features = ["derive", "cargo", "wrap_help"] } clap_complete = "3.2.3" clap_mangen = "0.1" comrak = "0.14.0" +fast_qr = "0.3.1" futures = "0.3" get_if_addrs = "0.5" hex = "0.4" http = "0.2" httparse = "1" -lazy_static = "1.4.0" libflate = "1" log = "0.4" maud = "0.23" @@ -45,8 +45,6 @@ mime = "0.3" nanoid = "0.4" percent-encoding = "2" port_check = "0.1" -qrcode = "0.12.0" -regex = "1" rustls = { version = "0.20", optional = true } rustls-pemfile = { version = "1.0", optional = true } serde = { version = "1", features = ["derive"] } @@ -73,6 +71,7 @@ assert_cmd = "2" assert_fs = "1" predicates = "2" pretty_assertions = "1.2" +regex = "1" reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false } rstest = "0.15" select = "0.5" diff --git a/data/style.scss b/data/style.scss index 87ecc4c58..fb76a9ac4 100644 --- a/data/style.scss +++ b/data/style.scss @@ -180,9 +180,6 @@ nav .qrcode { nav .qrcode svg { display: block; - border: 0.3rem; - border-style: solid; - border-color: #ffffff; } nav .theme { diff --git a/src/consts.rs b/src/consts.rs index f10570a51..1a105f7f0 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,4 +1,4 @@ -use qrcode::EcLevel; +use fast_qr::ECL; /// The error correction level to use for all QR code generation. -pub const QR_EC_LEVEL: EcLevel = EcLevel::L; +pub const QR_EC_LEVEL: ECL = ECL::L; diff --git a/src/main.rs b/src/main.rs index 558dabb30..11193cce1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,8 @@ use actix_web_httpauth::middleware::HttpAuthentication; use anyhow::Result; use clap::{crate_version, IntoApp, Parser}; use clap_complete::generate; +use fast_qr::QRBuilder; use log::{error, warn}; -use qrcode::QrCode; use yansi::{Color, Paint}; mod archive; @@ -240,13 +240,13 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { .iter() .filter(|url| !url.contains("//127.0.0.1:") && !url.contains("//[::1]:")) { - match QrCode::with_error_correction_level(url, consts::QR_EC_LEVEL) { + match QRBuilder::new(url.clone()).ecl(consts::QR_EC_LEVEL).build() { Ok(qr) => { println!("QR code for {}:", Color::Green.paint(url).bold()); - print_qr(&qr); + qr.print(); } Err(e) => { - error!("Failed to render QR to terminal: {}", e); + error!("Failed to render QR to terminal: {:?}", e); } }; } @@ -352,28 +352,3 @@ async fn css() -> impl Responder { .insert_header(ContentType(mime::TEXT_CSS)) .body(css) } - -// Prints to the console a normal and an inverted QrCode side by side. -fn print_qr(qr: &QrCode) { - use qrcode::render::unicode::Dense1x2; - - let normal = qr - .render() - .quiet_zone(true) - .dark_color(Dense1x2::Dark) - .light_color(Dense1x2::Light) - .build(); - let inverted = qr - .render() - .quiet_zone(true) - .dark_color(Dense1x2::Light) - .light_color(Dense1x2::Dark) - .build(); - let codes = normal - .lines() - .zip(inverted.lines()) - .map(|(l, r)| format!("{} {}", l, r)) - .collect::>() - .join("\n"); - println!("{}", codes); -} diff --git a/src/renderer.rs b/src/renderer.rs index 614672f90..77b9eed77 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -2,10 +2,10 @@ use actix_web::http::StatusCode; use chrono::{DateTime, Utc}; use chrono_humanize::Humanize; use clap::{crate_name, crate_version}; -use lazy_static::lazy_static; +use fast_qr::convert::svg::SvgBuilder; +use fast_qr::qr::QRCodeError; +use fast_qr::QRBuilder; use maud::{html, Markup, PreEscaped, DOCTYPE}; -use qrcode::{types::QrError, QrCode}; -use regex::Regex; use std::time::SystemTime; use strum::IntoEnumIterator; @@ -229,27 +229,11 @@ pub fn raw(entries: Vec, is_root: bool) -> Markup { } /// Renders the QR code SVG -fn qr_code_svg(url: impl AsRef, no_width_height_attr: bool) -> Result { - use qrcode::render::svg; - let qr = QrCode::with_error_correction_level(url.as_ref(), consts::QR_EC_LEVEL)?; - let mut svg = qr - .render() - .quiet_zone(false) - .dark_color(svg::Color("#000000")) - .light_color(svg::Color("#ffffff")) - .build(); - - if no_width_height_attr { - // HACK: qrcode crate hard-codes height and width into SVG's attributes. - // This behaviour may be undesirable because we want it to fit its HTML container. - // The proper way to remove them is to use a XML parser, but regex is good enough for a - // simple case like this. - lazy_static! { - static ref RE: Regex = - Regex::new(r#"(?P.+?>)"#).unwrap(); - } - svg = RE.replace(&svg, "$front$aft").to_string(); - } +fn qr_code_svg(url: impl AsRef, margin: usize) -> Result { + let qr = QRBuilder::new(url.as_ref().into()) + .ecl(consts::QR_EC_LEVEL) + .build()?; + let svg = SvgBuilder::new().margin(margin).build_qr(qr); Ok(svg) } @@ -338,9 +322,9 @@ fn qr_spoiler(show_qrcode: bool, content: impl AsRef) -> Markup { "QR code" } div.qrcode #qrcode { - @match qr_code_svg(content, true) { + @match qr_code_svg(content, 1) { Ok(svg) => (PreEscaped(svg)), - Err(err) => (format!("QR generation error: {}", err)), + Err(err) => (format!("QR generation error: {:?}", err)), } } } From 044f30c550888429cf8a9d39ef1a282ff3385e09 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 1 Sep 2022 21:10:30 +0800 Subject: [PATCH 16/19] Move QR margin size into `consts` --- src/consts.rs | 3 +++ src/renderer.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/consts.rs b/src/consts.rs index 1a105f7f0..d8646834c 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -2,3 +2,6 @@ use fast_qr::ECL; /// The error correction level to use for all QR code generation. pub const QR_EC_LEVEL: ECL = ECL::L; + +/// The margin size for the SVG QR code on the webpage. +pub const SVG_QR_MARGIN: usize = 1; diff --git a/src/renderer.rs b/src/renderer.rs index 77b9eed77..562f2159d 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -322,7 +322,7 @@ fn qr_spoiler(show_qrcode: bool, content: impl AsRef) -> Markup { "QR code" } div.qrcode #qrcode { - @match qr_code_svg(content, 1) { + @match qr_code_svg(content, consts::SVG_QR_MARGIN) { Ok(svg) => (PreEscaped(svg)), Err(err) => (format!("QR generation error: {:?}", err)), } From b817d9c44c4102c0c141b9ccee7d1cb04fca1baf Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 15 Sep 2022 18:32:49 +0800 Subject: [PATCH 17/19] Restores webpage QR tests --- tests/qrcode.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/qrcode.rs b/tests/qrcode.rs index 162c33af6..de8ac8846 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -2,12 +2,32 @@ mod fixtures; use assert_cmd::prelude::CommandCargoExt; use assert_fs::TempDir; -use fixtures::{port, tmpdir, Error}; +use fixtures::{port, server, tmpdir, Error, TestServer}; use rstest::rstest; +use select::document::Document; +use select::predicate::Attr; use std::process::{Command, Stdio}; use std::thread::sleep; use std::time::Duration; +#[rstest] +fn webpage_hides_qrcode_when_disabled(server: TestServer) -> Result<(), Error> { + let body = reqwest::blocking::get(server.url())?.error_for_status()?; + let parsed = Document::from_read(body)?; + assert!(parsed.find(Attr("id", "qrcode")).next().is_none()); + + Ok(()) +} + +#[rstest] +fn webpage_hides_qrcode_when_enabled(#[with(&["-q"])] server: TestServer) -> Result<(), Error> { + let body = reqwest::blocking::get(server.url())?.error_for_status()?; + let parsed = Document::from_read(body)?; + assert!(parsed.find(Attr("id", "qrcode")).next().is_some()); + + Ok(()) +} + #[cfg(not(windows))] fn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result { use fake_tty::{bash_command, get_stdout}; From af436baf57b274a0146628711e2c8d407c70eff1 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 15 Sep 2022 19:48:11 +0800 Subject: [PATCH 18/19] Fix test name --- tests/qrcode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/qrcode.rs b/tests/qrcode.rs index de8ac8846..3dbd4bd8f 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -20,7 +20,7 @@ fn webpage_hides_qrcode_when_disabled(server: TestServer) -> Result<(), Error> { } #[rstest] -fn webpage_hides_qrcode_when_enabled(#[with(&["-q"])] server: TestServer) -> Result<(), Error> { +fn webpage_shows_qrcode_when_enabled(#[with(&["-q"])] server: TestServer) -> Result<(), Error> { let body = reqwest::blocking::get(server.url())?.error_for_status()?; let parsed = Document::from_read(body)?; assert!(parsed.find(Attr("id", "qrcode")).next().is_some()); From 2d63d3d7a25483293789546a347f6235ef0aea67 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 15 Sep 2022 19:54:50 +0800 Subject: [PATCH 19/19] Add tooltip on QR code --- src/renderer.rs | 2 +- tests/qrcode.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/renderer.rs b/src/renderer.rs index 562f2159d..ebb9f6f80 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -321,7 +321,7 @@ fn qr_spoiler(show_qrcode: bool, content: impl AsRef) -> Markup { p { "QR code" } - div.qrcode #qrcode { + div.qrcode #qrcode title=(PreEscaped(content.as_ref())) { @match qr_code_svg(content, consts::SVG_QR_MARGIN) { Ok(svg) => (PreEscaped(svg)), Err(err) => (format!("QR generation error: {:?}", err)), diff --git a/tests/qrcode.rs b/tests/qrcode.rs index 3dbd4bd8f..98a3c6768 100644 --- a/tests/qrcode.rs +++ b/tests/qrcode.rs @@ -23,7 +23,14 @@ fn webpage_hides_qrcode_when_disabled(server: TestServer) -> Result<(), Error> { fn webpage_shows_qrcode_when_enabled(#[with(&["-q"])] server: TestServer) -> Result<(), Error> { let body = reqwest::blocking::get(server.url())?.error_for_status()?; let parsed = Document::from_read(body)?; - assert!(parsed.find(Attr("id", "qrcode")).next().is_some()); + let qr_container = parsed + .find(Attr("id", "qrcode")) + .next() + .ok_or("QR container not found")?; + let tooltip = qr_container + .attr("title") + .ok_or("QR container has no title")?; + assert_eq!(tooltip, server.url().as_str()); Ok(()) }