From 5666e44484b3c6485289e603b8a52f35bda87ef0 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 19 Dec 2022 19:24:03 -0800 Subject: [PATCH 1/4] Support more image content types --- src/inscription.rs | 59 +++++++++++++++++++++++++++++----------- src/main.rs | 3 +- src/subcommand/server.rs | 1 - tests/wallet.rs | 6 ++-- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/inscription.rs b/src/inscription.rs index a5e0ee5789..ca388241c6 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -16,6 +16,41 @@ const PROTOCOL_ID: &[u8] = b"ord"; const CONTENT_TAG: &[u8] = &[]; const CONTENT_TYPE_TAG: &[u8] = &[1]; +const CONTENT_TYPES: &[(&str, bool, &[&str])] = &[ + ("image/apng", true, &["apng"]), + ("image/avif", true, &["avif"]), + ("image/gif", true, &["gif"]), + ("image/jpeg", true, &["jpg", "jpeg"]), + ("image/png", true, &["png"]), + ("image/webp", true, &["webp"]), + ("text/plain;charset=utf-8", false, &["txt"]), +]; + +lazy_static! { + static ref IMAGE_CONTENT_TYPES: HashSet<&'static str> = CONTENT_TYPES + .iter() + .filter(|(_, image, _)| *image) + .map(|(content_type, _, _)| *content_type) + .collect(); +} + +fn content_type_for_extension(extension: &str) -> Result<&'static str, Error> { + for (content_type, _, extensions) in CONTENT_TYPES { + if extensions.contains(&extension) { + return Ok(content_type); + } + } + + Err(anyhow!( + "file extension `.{extension}`, supported extensions: {}", + CONTENT_TYPES + .iter() + .map(|(_, _, extensions)| extensions[0]) + .collect::>() + .join(" "), + )) +} + #[derive(Debug, PartialEq)] pub(crate) struct Inscription { content: Option>, @@ -47,21 +82,13 @@ impl Inscription { } } - let content_type = match path - .extension() - .ok_or_else(|| anyhow!("file must have extension"))? - .to_str() - .ok_or_else(|| anyhow!("unrecognized extension"))? - { - "txt" => "text/plain;charset=utf-8", - "png" => "image/png", - "gif" => "image/gif", - other => { - return Err(anyhow!( - "unrecognized file extension `.{other}`, only .txt, .png and .gif accepted" - )) - } - }; + let content_type = content_type_for_extension( + path + .extension() + .ok_or_else(|| anyhow!("file must have extension"))? + .to_str() + .ok_or_else(|| anyhow!("unrecognized extension"))?, + )?; Ok(Self { content: Some(content), @@ -96,7 +123,7 @@ impl Inscription { match self.content_type()? { "text/plain;charset=utf-8" => Some(Content::Text(str::from_utf8(content).ok()?)), - "image/png" | "image/gif" => Some(Content::Image), + content_type if IMAGE_CONTENT_TYPES.contains(content_type) => Some(Content::Image), _ => None, } } diff --git a/src/main.rs b/src/main.rs index 66b84eaa1f..8de2d99120 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,11 +42,12 @@ use { clap::{ArgGroup, Parser}, derive_more::{Display, FromStr}, html_escaper::{Escape, Trusted}, + lazy_static::lazy_static, regex::Regex, serde::{Deserialize, Serialize}, std::{ cmp::Ordering, - collections::{BTreeMap, VecDeque}, + collections::{BTreeMap, HashSet, VecDeque}, env, fmt::{self, Display, Formatter}, fs, io, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index d4240137eb..1c909d772c 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -17,7 +17,6 @@ use { Router, }, axum_server::Handle, - lazy_static::lazy_static, rust_embed::RustEmbed, rustls_acme::{ acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY}, diff --git a/tests/wallet.rs b/tests/wallet.rs index 642077bf7a..ed03fc31ee 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -286,12 +286,12 @@ fn inscribe_unknown_file_extension() { let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); CommandBuilder::new(format!( - "--chain regtest wallet inscribe --satpoint {txid}:0:0 --file pepe.jpg" + "--chain regtest wallet inscribe --satpoint {txid}:0:0 --file pepe.xyz" )) - .write("pepe.jpg", [1; 520]) + .write("pepe.xyz", [1; 520]) .rpc_server(&rpc_server) .expected_exit_code(1) - .expected_stderr("error: unrecognized file extension `.jpg`, only .txt, .png and .gif accepted\n") + .stderr_regex(r"error: file extension `\.xyz`, supported extensions: apng .*\n") .run(); } From a9e6ba64f91f5d7e1e5b9361343941dd7ee73794 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 19 Dec 2022 19:27:21 -0800 Subject: [PATCH 2/4] Remove avif since it's not well supported yet --- src/inscription.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/inscription.rs b/src/inscription.rs index ca388241c6..8b0e11ade0 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -18,7 +18,6 @@ const CONTENT_TYPE_TAG: &[u8] = &[1]; const CONTENT_TYPES: &[(&str, bool, &[&str])] = &[ ("image/apng", true, &["apng"]), - ("image/avif", true, &["avif"]), ("image/gif", true, &["gif"]), ("image/jpeg", true, &["jpg", "jpeg"]), ("image/png", true, &["png"]), From 0e4580683ee35cf2c98391eaa534e5a9b867e115 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 19 Dec 2022 19:34:25 -0800 Subject: [PATCH 3/4] Move into module --- src/inscription.rs | 40 +++-------------------- src/inscription/content_type.rs | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 36 deletions(-) create mode 100644 src/inscription/content_type.rs diff --git a/src/inscription.rs b/src/inscription.rs index 8b0e11ade0..20b0a10bbf 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -11,45 +11,13 @@ use { std::{iter::Peekable, str}, }; +mod content_type; + const PROTOCOL_ID: &[u8] = b"ord"; const CONTENT_TAG: &[u8] = &[]; const CONTENT_TYPE_TAG: &[u8] = &[1]; -const CONTENT_TYPES: &[(&str, bool, &[&str])] = &[ - ("image/apng", true, &["apng"]), - ("image/gif", true, &["gif"]), - ("image/jpeg", true, &["jpg", "jpeg"]), - ("image/png", true, &["png"]), - ("image/webp", true, &["webp"]), - ("text/plain;charset=utf-8", false, &["txt"]), -]; - -lazy_static! { - static ref IMAGE_CONTENT_TYPES: HashSet<&'static str> = CONTENT_TYPES - .iter() - .filter(|(_, image, _)| *image) - .map(|(content_type, _, _)| *content_type) - .collect(); -} - -fn content_type_for_extension(extension: &str) -> Result<&'static str, Error> { - for (content_type, _, extensions) in CONTENT_TYPES { - if extensions.contains(&extension) { - return Ok(content_type); - } - } - - Err(anyhow!( - "file extension `.{extension}`, supported extensions: {}", - CONTENT_TYPES - .iter() - .map(|(_, _, extensions)| extensions[0]) - .collect::>() - .join(" "), - )) -} - #[derive(Debug, PartialEq)] pub(crate) struct Inscription { content: Option>, @@ -81,7 +49,7 @@ impl Inscription { } } - let content_type = content_type_for_extension( + let content_type = content_type::for_extension( path .extension() .ok_or_else(|| anyhow!("file must have extension"))? @@ -122,7 +90,7 @@ impl Inscription { match self.content_type()? { "text/plain;charset=utf-8" => Some(Content::Text(str::from_utf8(content).ok()?)), - content_type if IMAGE_CONTENT_TYPES.contains(content_type) => Some(Content::Image), + content_type if content_type::is_image(content_type) => Some(Content::Image), _ => None, } } diff --git a/src/inscription/content_type.rs b/src/inscription/content_type.rs new file mode 100644 index 0000000000..399ef1240d --- /dev/null +++ b/src/inscription/content_type.rs @@ -0,0 +1,58 @@ +use super::*; + +const TABLE: &[(&str, bool, &[&str])] = &[ + ("image/apng", true, &["apng"]), + ("image/gif", true, &["gif"]), + ("image/jpeg", true, &["jpg", "jpeg"]), + ("image/png", true, &["png"]), + ("image/webp", true, &["webp"]), + ("text/plain;charset=utf-8", false, &["txt"]), +]; + +lazy_static! { + static ref IMAGE_CONTENT_TYPES: HashSet<&'static str> = TABLE + .iter() + .filter(|(_, image, _)| *image) + .map(|(content_type, _, _)| *content_type) + .collect(); +} + +pub(crate) fn is_image(content_type: &str) -> bool { + IMAGE_CONTENT_TYPES.contains(content_type) +} + +pub(crate) fn for_extension(extension: &str) -> Result<&'static str, Error> { + for (content_type, _, extensions) in TABLE { + if extensions.contains(&extension) { + return Ok(content_type); + } + } + + Err(anyhow!( + "file extension `.{extension}`, supported extensions: {}", + TABLE + .iter() + .map(|(_, _, extensions)| extensions[0]) + .collect::>() + .join(" "), + )) +} + +#[cfg(test)] +mod tests { + #[test] + fn is_image() { + assert!(super::is_image("image/apng")); + assert!(!super::is_image("foo")); + } + + #[test] + fn for_extension() { + assert_eq!(super::for_extension("jpg").unwrap(), "image/jpeg"); + assert_eq!(super::for_extension("jpeg").unwrap(), "image/jpeg"); + assert_eq!( + super::for_extension("foo").unwrap_err().to_string(), + "file extension `.foo`, supported extensions: apng gif jpg png webp txt" + ); + } +} From 9a66819993efdb3c1393a181e08223c817339dca Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 20 Dec 2022 10:19:37 -0800 Subject: [PATCH 4/4] tweak --- src/inscription/content_type.rs | 4 ++-- tests/wallet.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/inscription/content_type.rs b/src/inscription/content_type.rs index 399ef1240d..4e1f1cf1ff 100644 --- a/src/inscription/content_type.rs +++ b/src/inscription/content_type.rs @@ -29,7 +29,7 @@ pub(crate) fn for_extension(extension: &str) -> Result<&'static str, Error> { } Err(anyhow!( - "file extension `.{extension}`, supported extensions: {}", + "unsupported file extension `.{extension}`, supported extensions: {}", TABLE .iter() .map(|(_, _, extensions)| extensions[0]) @@ -52,7 +52,7 @@ mod tests { assert_eq!(super::for_extension("jpeg").unwrap(), "image/jpeg"); assert_eq!( super::for_extension("foo").unwrap_err().to_string(), - "file extension `.foo`, supported extensions: apng gif jpg png webp txt" + "unsupported file extension `.foo`, supported extensions: apng gif jpg png webp txt" ); } } diff --git a/tests/wallet.rs b/tests/wallet.rs index ed03fc31ee..d8394f5c4a 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -291,7 +291,7 @@ fn inscribe_unknown_file_extension() { .write("pepe.xyz", [1; 520]) .rpc_server(&rpc_server) .expected_exit_code(1) - .stderr_regex(r"error: file extension `\.xyz`, supported extensions: apng .*\n") + .stderr_regex(r"error: unsupported file extension `\.xyz`, supported extensions: apng .*\n") .run(); }