Skip to content

Commit

Permalink
Fix preview for inscriptions with no body (ordinals#1287)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Jan 19, 2023
1 parent ea207d4 commit 75937ab
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 62 deletions.
80 changes: 41 additions & 39 deletions src/inscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>>,
body: Option<Vec<u8>>,
content_type: Option<Vec<u8>>,
}

impl Inscription {
#[cfg(test)]
pub(crate) fn new(content_type: Option<Vec<u8>>, content: Option<Vec<u8>>) -> Self {
Self {
content_type,
content,
}
pub(crate) fn new(content_type: Option<Vec<u8>>, body: Option<Vec<u8>>) -> Self {
Self { content_type, body }
}

pub(crate) fn from_transaction(tx: &Transaction) -> Option<Inscription> {
Expand All @@ -38,10 +35,10 @@ impl Inscription {
pub(crate) fn from_file(chain: Chain, path: impl AsRef<Path>) -> Result<Self, Error> {
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");
}
Expand All @@ -56,7 +53,7 @@ impl Inscription {
)?;

Ok(Self {
content: Some(content),
body: Some(body),
content_type: Some(content_type.into()),
})
}
Expand All @@ -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);
}
}
Expand All @@ -87,20 +84,28 @@ impl Inscription {
self.append_reveal_script_to_builder(builder).into_script()
}

pub(crate) fn media(&self) -> Option<Media> {
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<Vec<u8>> {
self.content
pub(crate) fn into_body(self) -> Option<Vec<u8>> {
self.body
}

pub(crate) fn content_size(&self) -> Option<usize> {
Some(self.content_bytes()?.len())
pub(crate) fn content_length(&self) -> Option<usize> {
Some(self.body()?.len())
}

pub(crate) fn content_type(&self) -> Option<&str> {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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() {
Expand All @@ -232,10 +237,7 @@ impl<'a> InscriptionParser<'a> {
}
}

return Ok(Some(Inscription {
content,
content_type,
}));
return Ok(Some(Inscription { body, content_type }));
}

Ok(None)
Expand Down Expand Up @@ -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,
}),
);
}
Expand All @@ -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",
Expand All @@ -409,15 +411,15 @@ 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", "")),
);
}

#[test]
fn valid_content_in_multiple_empty_pushes() {
fn valid_body_in_multiple_empty_pushes() {
assert_eq!(
InscriptionParser::parse(&envelope(&[
b"ord",
Expand Down Expand Up @@ -544,7 +546,7 @@ mod tests {
}

#[test]
fn no_content() {
fn empty_envelope() {
assert_eq!(
InscriptionParser::parse(&envelope(&[])),
Err(InscriptionError::NoInscription)
Expand Down Expand Up @@ -710,7 +712,7 @@ mod tests {
witness.push(
&Inscription {
content_type: None,
content: None,
body: None,
}
.append_reveal_script(script::Builder::new()),
);
Expand All @@ -721,7 +723,7 @@ mod tests {
InscriptionParser::parse(&witness).unwrap(),
Inscription {
content_type: None,
content: None,
body: None,
}
);
}
Expand All @@ -732,7 +734,7 @@ mod tests {
InscriptionParser::parse(&envelope(&[b"ord", &[3], &[0]])),
Ok(Inscription {
content_type: None,
content: None,
body: None,
}),
);
}
Expand Down
1 change: 1 addition & 0 deletions src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) enum Media {
Iframe,
Image,
Text,
Unknown,
}

impl Media {
Expand Down
77 changes: 62 additions & 15 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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()),
};
}

Expand Down Expand Up @@ -2052,4 +2055,48 @@ mod tests {
".*<title>Inscription 0</title>.*",
);
}

#[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(),
);
}
}
2 changes: 1 addition & 1 deletion src/templates/inscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ mod tests {
<dd>1</dd>
<dt>content</dt>
<dd><a href=/content/1{64}i1>link</a></dd>
<dt>content size</dt>
<dt>content length</dt>
<dd>10 bytes</dd>
<dt>content type</dt>
<dd>text/plain;charset=utf-8</dd>
Expand Down
4 changes: 2 additions & 2 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions templates/inscription.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ <h1>Inscription {{ self.number }}</h1>
<dt>sat</dt>
<dd><a href=/sat/{{sat}}>{{sat}}</a></dd>
%% }
%% if let Some(content_size) = self.inscription.content_size() {
%% if let Some(content_length) = self.inscription.content_length() {
<dt>content</dt>
<dd><a href=/content/{{self.inscription_id}}>link</a></dd>
<dt>content size</dt>
<dd>{{ content_size }} bytes</dd>
<dt>content length</dt>
<dd>{{ content_length }} bytes</dd>
%% }
%% if let Some(content_type) = self.inscription.content_type() {
<dt>content type</dt>
Expand Down
2 changes: 1 addition & 1 deletion tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fn inscription_page() {
<dd>10000</dd>
<dt>content</dt>
<dd><a href=/content/{inscription}>link</a></dd>
<dt>content size</dt>
<dt>content length</dt>
<dd>3 bytes</dd>
<dt>content type</dt>
<dd>text/plain;charset=utf-8</dd>
Expand Down
2 changes: 1 addition & 1 deletion tests/wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fn inscriptions_can_be_sent() {
format!("/inscription/{inscription}"),
format!(
".*<h1>Inscription 0</h1>.*<dl>.*
<dt>content size</dt>
<dt>content length</dt>
<dd>3 bytes</dd>
<dt>content type</dt>
<dd>text/plain;charset=utf-8</dd>
Expand Down

0 comments on commit 75937ab

Please sign in to comment.