Skip to content

Commit

Permalink
fix: add information about limitation and examples
Browse files Browse the repository at this point in the history
Serde derives for internally tagged and untagged unions are not supported.
The reason is, that this library tries to make it impossible to deserialize
a CID accidentally into bytes.

This limiation is now documented and examples on how to work around it are
also provided. An additional test that fails when trying to deserialize a
CID into bytes was also added.
  • Loading branch information
vmx committed Feb 13, 2024
1 parent 8311876 commit bd47a75
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ jobs:
shell: bash

- name: Test
run: cargo test --workspace
run: cargo test --all-targets --workspace
shell: bash
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ fn main() -> Result<(), Box<dyn Error>> {
}
```

Limitations
-----------

This library tries as much as it can to make it impossible to accidentally deserialize a CID into bytes. Due to that objective, there are certain limitations. The Serde attributes for [internally tagged and untagged enums](https://serde.rs/enum-representations.html) won't work, but will lead to errors. It means that you cannot automatically derive them, but you would need to implement such functionality manually with a custom deserializer. Examples of how to do that are in the [enum example](examples/enums.rs).


License
-------
Expand Down
145 changes: 145 additions & 0 deletions examples/enums.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/// Serde untagged (`#[serde(untagged)]`) and internaly tagged enums (`#[serde(tag = "tag")]`) are
/// not supported by CIDs. Here examples are provided on how to implement similar behaviour. This
/// file also contains an example for a kinded enum.
use std::convert::{TryFrom, TryInto};

use cid::Cid;
use libipld_core::ipld::Ipld;
use serde::{de, Deserialize};
use serde_bytes::ByteBuf;
use serde_ipld_dagcbor::from_slice;

/// The CID `bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy` encoded as CBOR
/// 42(h'00015512202C26B46B68FFC68FF99B453C1D30413413422D706483BFA0F98A5E886266E7AE')
const CBOR_CID_FIXTURE: [u8; 41] = [
0xd8, 0x2a, 0x58, 0x25, 0x00, 0x01, 0x55, 0x12, 0x20, 0x2c, 0x26, 0xb4, 0x6b, 0x68, 0xff, 0xc6,
0x8f, 0xf9, 0x9b, 0x45, 0x3c, 0x1d, 0x30, 0x41, 0x34, 0x13, 0x42, 0x2d, 0x70, 0x64, 0x83, 0xbf,
0xa0, 0xf9, 0x8a, 0x5e, 0x88, 0x62, 0x66, 0xe7, 0xae,
];

/// This enum shows how an internally tagged enum could be implemented.
#[derive(Debug, PartialEq)]
enum CidInInternallyTaggedEnum {
MyCid { cid: Cid },
}

// This manual deserializer implementation works as if you would derive `Deserialize` and add
// `#[serde(tag = "type")]` to the `CidInternallyTaggedEnum` enum.
impl<'de> de::Deserialize<'de> for CidInInternallyTaggedEnum {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Tagged {
r#type: String,
cid: Cid,
}

let Tagged { r#type, cid } = Deserialize::deserialize(deserializer)?;
if r#type == "MyCid" {
Ok(CidInInternallyTaggedEnum::MyCid { cid })
} else {
Err(de::Error::custom("No matching enum variant found"))
}
}
}

/// This enum shows how an untagged enum could be implemented.
#[derive(Deserialize)]
#[serde(tag = "t", content = "c")]
enum CidInAdjacentlyTaggedEnum {
MyCid(Cid),
}

/// This enum shows how an untagged enum could be implemented.
#[derive(Debug, PartialEq)]
enum CidInUntaggedEnum {
MyCid(Cid),
}

// This manual deserializer implementation works as if you would derive `Deserialize` and add
// `#[serde(untagged)]`.
impl<'de> de::Deserialize<'de> for CidInUntaggedEnum {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
Cid::deserialize(deserializer)
.map(CidInUntaggedEnum::MyCid)
.map_err(|_| de::Error::custom("No matching enum variant found"))
}
}

/// This enum shows how a kinded enum could be implemented.
#[derive(Debug, PartialEq)]
pub enum Kinded {
Bytes(ByteBuf),
Link(Cid),
}

impl TryFrom<Ipld> for Kinded {
type Error = ();

fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
match ipld {
Ipld::Bytes(bytes) => Ok(Self::Bytes(ByteBuf::from(bytes))),
Ipld::Link(cid) => Ok(Self::Link(cid)),
_ => Err(()),
}
}
}

impl<'de> de::Deserialize<'de> for Kinded {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
Ipld::deserialize(deserializer).and_then(|ipld| {
ipld.try_into()
.map_err(|_| de::Error::custom("No matching enum variant found"))
})
}
}

pub fn main() {
let cid: Cid = from_slice(&CBOR_CID_FIXTURE).unwrap();

// {"type": "MyCid", "cid": 42(h'00015512202C26B46B68FFC68FF99B453C1D30413413422D706483BFA0F98A5E886266E7AE')}
let cbor_internally_tagged_enum = [
&[
0xa2, 0x64, 0x74, 0x79, 0x70, 0x65, 0x65, 0x4d, 0x79, 0x43, 0x69, 0x64, 0x63, 0x63,
0x69, 0x64,
],
&CBOR_CID_FIXTURE[..],
]
.concat();
assert_eq!(
from_slice::<CidInInternallyTaggedEnum>(&cbor_internally_tagged_enum).unwrap(),
CidInInternallyTaggedEnum::MyCid { cid }
);

assert_eq!(
from_slice::<CidInUntaggedEnum>(&CBOR_CID_FIXTURE).unwrap(),
CidInUntaggedEnum::MyCid(cid)
);

assert_eq!(
from_slice::<Kinded>(&CBOR_CID_FIXTURE).unwrap(),
Kinded::Link(cid)
);

// The CID without the tag 42 prefix, so that it decodes as just bytes.
let cbor_bytes = &CBOR_CID_FIXTURE[2..];
let decoded_bytes: Kinded = from_slice(cbor_bytes).unwrap();
// The CBOR decoded bytes don't contain the prefix with the bytes type identifier and the
// length.
let bytes = cbor_bytes[2..].to_vec();
assert_eq!(decoded_bytes, Kinded::Bytes(ByteBuf::from(bytes)));
}

// Make it possible to run this example as test.
#[test]
fn test_main() {
main()
}
12 changes: 12 additions & 0 deletions tests/cid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ fn test_cid_not_as_bytes() {
.expect_err("shouldn't have parsed a tagged CID as a byte array");
from_slice::<serde_bytes::ByteBuf>(&cbor_cid[2..])
.expect("should have parsed an untagged CID as a byte array");

#[derive(Debug, Deserialize)]
struct NewType(ByteBuf);

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum BytesInEnum {
MyCid(NewType),
}

from_slice::<BytesInEnum>(&cbor_cid)
.expect_err("shouldn't have parsed a tagged CID as byte array");
}

/// Test whether a binary CID could be serialized if it isn't prefixed by tag 42. It should fail.
Expand Down

0 comments on commit bd47a75

Please sign in to comment.