From 75937abc510cb93e211d63db3597dc3379ddf9c0 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 19 Jan 2023 10:24:26 -0800 Subject: [PATCH] Fix preview for inscriptions with no body (#1287) --- src/inscription.rs | 80 ++++++++++++++++++------------------ src/media.rs | 1 + src/subcommand/server.rs | 77 +++++++++++++++++++++++++++------- src/templates/inscription.rs | 2 +- src/test.rs | 4 +- templates/inscription.html | 6 +-- tests/server.rs | 2 +- tests/wallet/send.rs | 2 +- 8 files changed, 112 insertions(+), 62 deletions(-) diff --git a/src/inscription.rs b/src/inscription.rs index 374edae27f..99391e9073 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -13,22 +13,19 @@ use { const PROTOCOL_ID: &[u8] = b"ord"; -const CONTENT_TAG: &[u8] = &[]; +const BODY_TAG: &[u8] = &[]; const CONTENT_TYPE_TAG: &[u8] = &[1]; #[derive(Debug, PartialEq, Clone)] pub(crate) struct Inscription { - content: Option>, + body: Option>, content_type: Option>, } impl Inscription { #[cfg(test)] - pub(crate) fn new(content_type: Option>, content: Option>) -> Self { - Self { - content_type, - content, - } + pub(crate) fn new(content_type: Option>, body: Option>) -> Self { + Self { content_type, body } } pub(crate) fn from_transaction(tx: &Transaction) -> Option { @@ -38,10 +35,10 @@ impl Inscription { pub(crate) fn from_file(chain: Chain, path: impl AsRef) -> Result { let path = path.as_ref(); - let content = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?; + let body = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?; if let Some(limit) = chain.inscription_content_size_limit() { - let len = content.len(); + let len = body.len(); if len > limit { bail!("content size of {len} bytes exceeds {limit} byte limit for {chain} inscriptions"); } @@ -56,7 +53,7 @@ impl Inscription { )?; Ok(Self { - content: Some(content), + body: Some(body), content_type: Some(content_type.into()), }) } @@ -73,9 +70,9 @@ impl Inscription { .push_slice(content_type); } - if let Some(content) = &self.content { - builder = builder.push_slice(CONTENT_TAG); - for chunk in content.chunks(520) { + if let Some(body) = &self.body { + builder = builder.push_slice(BODY_TAG); + for chunk in body.chunks(520) { builder = builder.push_slice(chunk); } } @@ -87,20 +84,28 @@ impl Inscription { self.append_reveal_script_to_builder(builder).into_script() } - pub(crate) fn media(&self) -> Option { - self.content_type()?.parse().ok() + pub(crate) fn media(&self) -> Media { + if self.body.is_none() { + return Media::Unknown; + } + + let Some(content_type) = self.content_type() else { + return Media::Unknown; + }; + + content_type.parse().unwrap_or(Media::Unknown) } - pub(crate) fn content_bytes(&self) -> Option<&[u8]> { - Some(self.content.as_ref()?) + pub(crate) fn body(&self) -> Option<&[u8]> { + Some(self.body.as_ref()?) } - pub(crate) fn into_content(self) -> Option> { - self.content + pub(crate) fn into_body(self) -> Option> { + self.body } - pub(crate) fn content_size(&self) -> Option { - Some(self.content_bytes()?.len()) + pub(crate) fn content_length(&self) -> Option { + Some(self.body()?.len()) } pub(crate) fn content_type(&self) -> Option<&str> { @@ -202,12 +207,12 @@ impl<'a> InscriptionParser<'a> { loop { match self.advance()? { - Instruction::PushBytes(CONTENT_TAG) => { - let mut content = Vec::new(); + Instruction::PushBytes(BODY_TAG) => { + let mut body = Vec::new(); while !self.accept(Instruction::Op(opcodes::all::OP_ENDIF))? { - content.extend_from_slice(self.expect_push()?); + body.extend_from_slice(self.expect_push()?); } - fields.insert(CONTENT_TAG, content); + fields.insert(BODY_TAG, body); break; } Instruction::PushBytes(tag) => { @@ -221,7 +226,7 @@ impl<'a> InscriptionParser<'a> { } } - let content = fields.remove(CONTENT_TAG); + let body = fields.remove(BODY_TAG); let content_type = fields.remove(CONTENT_TYPE_TAG); for tag in fields.keys() { @@ -232,10 +237,7 @@ impl<'a> InscriptionParser<'a> { } } - return Ok(Some(Inscription { - content, - content_type, - })); + return Ok(Some(Inscription { body, content_type })); } Ok(None) @@ -377,7 +379,7 @@ mod tests { InscriptionParser::parse(&envelope(&[b"ord", &[1], b"text/plain;charset=utf-8"])), Ok(Inscription { content_type: Some(b"text/plain;charset=utf-8".to_vec()), - content: None, + body: None, }), ); } @@ -388,13 +390,13 @@ mod tests { InscriptionParser::parse(&envelope(&[b"ord", &[], b"foo"])), Ok(Inscription { content_type: None, - content: Some(b"foo".to_vec()), + body: Some(b"foo".to_vec()), }), ); } #[test] - fn valid_content_in_multiple_pushes() { + fn valid_body_in_multiple_pushes() { assert_eq!( InscriptionParser::parse(&envelope(&[ b"ord", @@ -409,7 +411,7 @@ mod tests { } #[test] - fn valid_content_in_zero_pushes() { + fn valid_body_in_zero_pushes() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[]])), Ok(inscription("text/plain;charset=utf-8", "")), @@ -417,7 +419,7 @@ mod tests { } #[test] - fn valid_content_in_multiple_empty_pushes() { + fn valid_body_in_multiple_empty_pushes() { assert_eq!( InscriptionParser::parse(&envelope(&[ b"ord", @@ -544,7 +546,7 @@ mod tests { } #[test] - fn no_content() { + fn empty_envelope() { assert_eq!( InscriptionParser::parse(&envelope(&[])), Err(InscriptionError::NoInscription) @@ -710,7 +712,7 @@ mod tests { witness.push( &Inscription { content_type: None, - content: None, + body: None, } .append_reveal_script(script::Builder::new()), ); @@ -721,7 +723,7 @@ mod tests { InscriptionParser::parse(&witness).unwrap(), Inscription { content_type: None, - content: None, + body: None, } ); } @@ -732,7 +734,7 @@ mod tests { InscriptionParser::parse(&envelope(&[b"ord", &[3], &[0]])), Ok(Inscription { content_type: None, - content: None, + body: None, }), ); } diff --git a/src/media.rs b/src/media.rs index 67e1f7c654..0dfd9e78c2 100644 --- a/src/media.rs +++ b/src/media.rs @@ -6,6 +6,7 @@ pub(crate) enum Media { Iframe, Image, Text, + Unknown, } impl Media { diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index e716659206..97b3c33303 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -696,7 +696,7 @@ impl Server { HeaderValue::from_static("default-src 'unsafe-eval' 'unsafe-inline'"), ); - Some((headers, inscription.into_content()?)) + Some((headers, inscription.into_body()?)) } async fn preview( @@ -707,13 +707,9 @@ impl Server { .get_inscription_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - let content = inscription - .content_bytes() - .ok_or_not_found(|| format!("inscription {inscription_id} content"))?; - return match inscription.media() { - Some(Media::Audio) => Ok(PreviewAudioHtml { inscription_id }.into_response()), - Some(Media::Image) => Ok( + Media::Audio => Ok(PreviewAudioHtml { inscription_id }.into_response()), + Media::Image => Ok( ( [( header::CONTENT_SECURITY_POLICY, @@ -723,18 +719,25 @@ impl Server { ) .into_response(), ), - Some(Media::Iframe) => Ok( + Media::Iframe => Ok( Self::content_response(inscription) .ok_or_not_found(|| format!("inscription {inscription_id} content"))? .into_response(), ), - Some(Media::Text) => Ok( - PreviewTextHtml { - text: str::from_utf8(content).map_err(|err| anyhow!("Failed to decode UTF-8: {err}"))?, - } - .into_response(), - ), - None => Ok(PreviewUnknownHtml.into_response()), + Media::Text => { + let content = inscription + .body() + .ok_or_not_found(|| format!("inscription {inscription_id} content"))?; + + Ok( + PreviewTextHtml { + text: str::from_utf8(content) + .map_err(|err| anyhow!("Failed to decode UTF-8: {err}"))?, + } + .into_response(), + ) + } + Media::Unknown => Ok(PreviewUnknownHtml.into_response()), }; } @@ -2052,4 +2055,48 @@ mod tests { ".*Inscription 0.*", ); } + + #[test] + fn inscription_with_unknown_type_and_no_body_has_unknown_preview() { + let server = TestServer::new_with_sat_index(); + server.mine_blocks(1); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: Inscription::new(Some("foo/bar".as_bytes().to_vec()), None).to_witness(), + ..Default::default() + }); + + let inscription_id = InscriptionId::from(txid); + + server.mine_blocks(1); + + server.assert_response( + format!("/preview/{}", inscription_id), + StatusCode::OK, + &fs::read_to_string("templates/preview-unknown.html").unwrap(), + ); + } + + #[test] + fn inscription_with_known_type_and_no_body_has_unknown_preview() { + let server = TestServer::new_with_sat_index(); + server.mine_blocks(1); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: Inscription::new(Some("image/png".as_bytes().to_vec()), None).to_witness(), + ..Default::default() + }); + + let inscription_id = InscriptionId::from(txid); + + server.mine_blocks(1); + + server.assert_response( + format!("/preview/{}", inscription_id), + StatusCode::OK, + &fs::read_to_string("templates/preview-unknown.html").unwrap(), + ); + } } diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index c9d9188b51..d823daeb4c 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -61,7 +61,7 @@ mod tests {
1
content
link
-
content size
+
content length
10 bytes
content type
text/plain;charset=utf-8
diff --git a/src/test.rs b/src/test.rs index 0756643280..35f099d172 100644 --- a/src/test.rs +++ b/src/test.rs @@ -100,8 +100,8 @@ pub(crate) fn tx_out(value: u64, address: Address) -> TxOut { } } -pub(crate) fn inscription(content_type: &str, content: impl AsRef<[u8]>) -> Inscription { - Inscription::new(Some(content_type.into()), Some(content.as_ref().into())) +pub(crate) fn inscription(content_type: &str, body: impl AsRef<[u8]>) -> Inscription { + Inscription::new(Some(content_type.into()), Some(body.as_ref().into())) } pub(crate) fn inscription_id(n: u32) -> InscriptionId { diff --git a/templates/inscription.html b/templates/inscription.html index fa6249f06c..f08293cd39 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -25,11 +25,11 @@

Inscription {{ self.number }}

sat
{{sat}}
%% } -%% if let Some(content_size) = self.inscription.content_size() { +%% if let Some(content_length) = self.inscription.content_length() {
content
link
-
content size
-
{{ content_size }} bytes
+
content length
+
{{ content_length }} bytes
%% } %% if let Some(content_type) = self.inscription.content_type() {
content type
diff --git a/tests/server.rs b/tests/server.rs index c1f99983b0..a4ea61dcca 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -60,7 +60,7 @@ fn inscription_page() {
10000
content
link
-
content size
+
content length
3 bytes
content type
text/plain;charset=utf-8
diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index bee2e50a0b..223771f956 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -29,7 +29,7 @@ fn inscriptions_can_be_sent() { format!("/inscription/{inscription}"), format!( ".*

Inscription 0

.*
.* -
content size
+
content length
3 bytes
content type
text/plain;charset=utf-8