#cbor #binary #serialization #field-value #array-index

无 std minicbor-derive

派生 minicbor 的 DecodeEncode 特性

26 个版本 (14 个重大更改)

0.15.0 2024年4月17日
0.13.0 2022年12月17日
0.12.0 2022年6月19日
0.9.0 2022年2月20日
0.1.1 2020年3月29日

#13 in #array-index

Download history 18299/week @ 2024-04-17 16973/week @ 2024-04-24 17027/week @ 2024-05-01 16295/week @ 2024-05-08 18960/week @ 2024-05-15 16379/week @ 2024-05-22 19464/week @ 2024-05-29 16729/week @ 2024-06-05 16552/week @ 2024-06-12 17243/week @ 2024-06-19 17333/week @ 2024-06-26 14689/week @ 2024-07-03 18337/week @ 2024-07-10 16771/week @ 2024-07-17 14486/week @ 2024-07-24 13572/week @ 2024-07-31

66,165 每月下载量
用于 184 个crate(2 个直接使用)

BlueOak-1.0.0

120KB
2.5K SLoC

minicbor-derive

minicbor 的配套 crate,允许派生 minicbor::Encodeminicbor::Decode 特性。

文档

文档可在以下地址获取

许可证

本软件根据 Blue Oak Model License Version 1.0.0 许可。如果您有兴趣为此项目做出贡献,请首先阅读 CONTRIBUTING.md 文件。


lib.rs:

过程宏,用于派生 minicbor 的 EncodeDecodeCborLen 特性。

支持为 structenum 派生。编码针对前后兼容性进行了优化,整体方法受 Google 的 Protocol Buffers 影响。

目标是:理想情况下,类型的变化仍然允许不知道变化的旧软件解码更改后的类型值(向前兼容性),以及新软件解码由旧软件编码的类型值,这些类型值不包括对类型所做的更改(向后兼容性)。

为了实现这一目标,编码具有以下特征

  1. 编码不包含任何名称,即没有字段名称、类型名称或变体名称。相反,每个字段和每个构造函数都需要用(无符号)索引数字进行注释,例如 #[n(1)]

  2. 在解码过程中忽略未知字段。[^1]

  3. 可选类型在解码时如果其值不存在,则默认为 None

  4. 可选枚举在解码过程中遇到未知变体时默认为 None

项目 1 确保名称可以自由更改而无需考虑兼容性问题。项目 2 确保新字段不会影响旧软件。项目 3 确保新软件可以停止生成可选值。项目 4 确保枚举可以得到旧软件不知道的新变体。当我们提到“字段”时,指的是结构体和元组结构体的元素,以及枚举结构和枚举元组。此外,如果所有字段都是可选的,将单元变体转换为结构体或元组变体也是一种兼容的更改。

从上述内容可以明显看出,非可选字段需要永久存在,因此它们只有在经过仔细考虑后才能成为类型的一部分。

应强调,enum 本身不能以兼容的方式进行更改。未知变体会导致错误。只有在它们被声明为可选字段类型时,枚举的未知变体才会映射到 None。换句话说,只有结构体可以作为前向和后向兼容方式中的顶级类型,枚举不能。

示例

use minicbor::{Encode, Decode};

#[derive(Encode, Decode)]
struct Point {
    #[n(0)] x: f64,
    #[n(1)] y: f64
}

#[derive(Encode, Decode)]
struct ConvexHull {
    #[n(0)] left: Point,
    #[n(1)] right: Point,
    #[n(2)] points: Vec<Point>,
    #[n(3)] state: Option<State>
}

#[derive(Encode, Decode)]
enum State {
    #[n(0)] Start,
    #[n(1)] Search { #[n(0)] info: u64 }
}

在这个示例中,以下更改在双向都是兼容的:

  • 重命名每个标识符。

  • PointConvexHullState::StartState::Search 添加可选字段。

  • 如果 State 仅作为 ConvexHull 的一部分进行解码,则可以向 State 添加更多变体。直接解码 State 会产生 UnknownVariant 错误,因为这些新变体。

支持的属性

#[n(...)]#[b(...)](或 #[cbor(n(...))]#[cbor(b(...))]

每个字段和变体都需要用索引数字进行注释,该数字将替代名称使用。对于编码,选择哪一个没有区别。对于解码,b 表示值从解码输入中借用,而 n 生成非借用值(但请参见下文隐式借用部分)。这意味着如果一个类型被注释为 #[b(...)],则其生命周期将限制在输入生命周期('bytes)。此外,如果类型是 Cow<'_, str>Cow<'_, minicbor::bytes::ByteSlice>Cow<'_, [u8]>,生成的代码将解码 strByteSlice[u8] 并构造一个 Cow::Borrowed 变体,这与 DecodeDecodeBytes 的常规 Cow 实现不同,后者生成所有权的值。

#[cbor(array)]

使用 CBOR 数组对注释的 struct、enum 或 enum 变体进行编码。当与 enum 一起使用时,它适用于其所有变体,但可以按变体覆盖。有关详细信息,请参阅CBOR 编码部分。

如果没有指定 #[cbor(array)]#[cbor(map)],则默认使用 #[cbor(array)]

#[cbor(map)]

使用 CBOR 映射来编码注解的结构体、枚举或枚举变体。当与枚举一起使用时,它应用于所有变体,但可以按每个变体进行覆盖。有关详细信息,请参阅CBOR 编码部分。

如果没有指定 #[cbor(array)]#[cbor(map)],则默认使用 #[cbor(array)]

#[cbor(index_only)]

不包含字段的枚举可能具有此属性。这会将编码更改为仅编码变体索引(有关详细信息,请参阅CBOR 编码部分)。

#[cbor(transparent)]

此属性可以附加到只有一个字段的结构体(即 newtypes)。如果存在,生成的 EncodeDecode 实现将仅将相应的 encodedecode 调用转发到内部类型,即结果 CBOR 表示将与内部类型相同。

#[cbor(skip)]

此属性可以附加到结构体和枚举的字段,以防止这些字段被编码。字段类型必须实现 Default,并在解码时使用 Default::default() 初始化字段。

#[cbor(tag(...))]

此属性可以附加到结构体、枚举及其字段。其参数是一个十进制无符号整数,它被编码为值的 CBOR 标签。解码也会尝试读取标签,否则失败。

#[cbor(decode_with= "<path>")]

当应用于类型 T 的字段时,由 <path> 表示的函数将用于解码 T。该函数需要与以下类型等效

use minicbor::decode::{Decoder, Error};

fn decode<'b, Ctx, T: 'b>(d: &mut Decoder<'b>, ctx: &mut Ctx) -> Result<T, Error> {
    todo!()
}

请注意,如果解码函数在其上下文参数中是通用的,则 derive 宏使用类型变量名称 Ctx

#[cbor(encode_with= "<path>")]

当应用于类型 T 的字段时,由 <path> 表示的函数将用于编码 T。该函数需要与以下类型等效

use minicbor::encode::{Encoder, Error, Write};

fn encode<Ctx, T, W: Write>(v: &T, e: &mut Encoder<W>, ctx: &mut Ctx) -> Result<(), Error<W::Error>> {
    todo!()
}

请注意,如果编码函数在其上下文参数中是通用的,则 derive 宏使用类型变量名称 Ctx

#[cbor(with= "<path>")]

结合 #[cbor(decode_with = "...")]#[cbor(encode_with = "...")]。在这里,<path> 表示一个包含名为 encodedecode 的函数的模块,这些函数满足 encode_withdecode_with 中提到的相应类型签名。如果也推导出 CborLen,则假定该模块包含一个名为 cbor_len 的函数,其签名与以下链接中描述的相匹配:#[cbor(cbor_len = "...")]

#[cbor(nil= "<path>")]

仅在与 #[cbor(decode_with = "...")] 结合时有效。如果存在,<path> 表示创建类型 T 的 nil 类似值的函数。有关详细信息,请参阅 minicbor::Decode::nil。该函数需要与以下类型等效

fn nil<T>() -> Option<T> {
    todo!()
}

#[cbor(has_nil)]

仅在与 #[cbor(with = "...")] 结合时有效。如果存在,则该属性表示由 with 指示的模块还包含用于创建 nil 值和检查值是否为 nil 值的函数 nilis_nil

#[cbor(is_nil= "<path>")]

仅在与#[cbor(encode_with = "...")]结合时有效。如果存在,<path>表示一个函数,用于检查类型为T的值是否为nil-like值。有关详细信息,请参阅minicbor::Encode::is_nil。该函数需要与以下类型等效

fn is_nil<T>(v: &T) -> bool {
    todo!()
}

#[cbor(cbor_len= "<path>")]

仅当派生CborLen时适用。当应用于类型为T的字段时,由<path>表示的函数将用于计算字节数的CBOR长度。该函数需要与以下类型等效

fn cbor_len<Ctx, T>(val: &T, ctx: &mut Ctx) -> usize {
    todo!()
}

请注意,如果cbor_len函数在其上下文参数中是泛型的,则 derive 宏使用类型变量名 Ctx

#[cbor(decode_bound= "...")]

当应用于泛型字段时,此属性会覆盖由 minicbor-derive 为派生的 Decode 实现生成的任何隐式类型参数限制。

#[cbor(encode_bound= "...")]

当应用于泛型字段时,此属性会覆盖由 minicbor-derive 为派生的 Encode 实现生成的任何隐式类型参数限制。

#[cbor(bound)]

结合#[cbor(encode_bound = "...")]#[cbor(decode_bound = "...")],即该限制适用于派生的 EncodeDecode 实现。

#[cbor(context_bound= "...")]

当为具有约束泛型上下文类型参数的部分类型派生 EncodeDecode 时,可以使用此属性将所需的特征限制添加到上下文类型参数。该属性可以是重复的,也可以将限制列表为 '+' 分隔的值,例如 "A + B + C"。

示例

组合上下文。
use minicbor::{Encode, Decode};
use minicbor::decode::{self, Decoder};

// Some decodable type that uses a custom context.
struct A(u8);

// `A`'s context type.
struct AC { a: u8 }

impl AsMut<AC> for AC {
    fn as_mut(&mut self) -> &mut AC { self }
}

impl<'b, C: AsMut<AC>> Decode<'b, C> for A {
    fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result<Self, decode::Error> {
        Ok(A(ctx.as_mut().a))
    }
}

// Another decodable type that uses a different context.
struct B(u8);

// `B`'s context type.
struct BC { b: u8 }

impl AsMut<BC> for BC {
    fn as_mut(&mut self) -> &mut BC { self }
}

impl<'b, C: AsMut<BC>> Decode<'b, C> for B {
    fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result<Self, decode::Error> {
        Ok(B(ctx.as_mut().b))
    }
}

// Finally, a type that combines `A` and `B` and therefore also needs to provide
// a context that can be used by both of them.
#[derive(Decode)]
#[cbor(context_bound = "AsMut<AC> + AsMut<BC>")]
struct C {
    #[n(0)] a: A,
    #[n(1)] b: B
}

// The combined context type.
struct CC(AC, BC);

impl AsMut<AC> for CC {
    fn as_mut(&mut self) -> &mut AC {
        &mut self.0
    }
}

impl AsMut<BC> for CC {
    fn as_mut(&mut self) -> &mut BC {
        &mut self.1
    }
}

隐式借用

除了#[b(...)]的显式借用之外,以下类型从解码输入中隐式借用,这意味着它们的生存期受输入生存期的限制

  • &'_ str
  • &'_ minicbor::bytes::ByteSlice
  • Option<&'_ str>
  • Option<&'_ minicbor::bytes::ByteSlice>

关于 &[u8] 呢?

&[u8]&[T] 的特例。由于 Rust 中 trait 实现特殊化的缺乏,为字节切片提供优化支持变得困难。泛型 [T] 实现 Encode 生成一个 T 数组。为了专门对 CBOR 字节进行编码和解码,minicbor 提供了 ByteSliceByteArrayByteVec 类型。此外,在派生时,可以使用 encode_withdecode_withwith 属性与 &[u8] 一起使用,例如。

use minicbor::{Encode, Decode};

#[derive(Encode, Decode)]
struct Foo<'a> {
    #[cbor(n(0), with = "minicbor::bytes")]
    field0: &'a [u8],

    #[n(1)]
    #[cbor(encode_with = "minicbor::bytes::encode")]
    #[cbor(decode_with = "minicbor::bytes::decode")]
    field1: &'a [u8],

    #[cbor(n(2), with = "minicbor::bytes")]
    field2: Option<&'a [u8]>,

    #[cbor(n(3), with = "minicbor::bytes")]
    field3: Vec<u8>,

    #[cbor(n(4), with = "minicbor::bytes")]
    field4: [u8; 16]
}

CBOR 编码

由派生的 Encode 实现产生的 CBOR 值具有以下格式。

结构体

数组编码

默认情况下,或者如果一个结构体有 #[cbor(array)] 属性,它将被表示为 CBOR 数组。其索引数字由字段值在这个数组中的位置表示。任何索引数字之间的空隙将用 CBOR NULL 值和同样以 NULL 结束的 Option 填充。

<<struct-as-array encoding>> =
    `array(n)`
        item_0
        item_1
        ...
        item_n

映射编码

如果一个结构体有 #[cbor(map)] 属性附加,那么它将被表示为具有与数字索引值对应的 CBOR 映射。

<<struct-as-map encoding>> =
    `map(n)`
        `0` item_0
        `1` item_1
        ...
         n  item_n

值为 None 的可选字段不会进行编码。

枚举

除非为没有字段的枚举使用了 #[cbor(index_only)] 属性,否则每个枚举变体都将编码为包含两个元素的数组。第一个元素是变体索引,第二个是实际的变体值。否则,如果没有字段的枚举具有 index_only 属性,则仅编码变体索引。

<<enum encoding>> =
    | `array(2)` n <<struct-as-array encoding>> ; if #[cbor(array)]
    | `array(2)` n <<struct-as-map encoding>>   ; if #[cbor(map)]
    | n                                         ; if #[cbor(index_only)]

使用哪种编码?

映射编码需要在编码中显式表示索引,这至少每字段值多消耗一个字节,而数组编码不需要编码索引。另一方面,缺失的值,即 None 和索引之间的空隙,在映射中不会进行编码,但需要显式地用数组作为 NULL 编码,每个 NULL 需要1个字节。因此,选择哪种编码取决于应编码的类型本质。

  • 密集类型 是只包含少量 Option 或假设其 OptionSome 的类型。它们最好以数组的形式编码。

  • 稀疏类型 是包含许多 Option 的类型,它们的 Option 通常为 None。它们最好以映射的形式编码。

在选择编码时,应考虑类型未来的变化,因为它们可能随着时间的推移将密集类型转换为稀疏类型。这也适用于 #[cbor(index_only)],该编码仅应与不期望其变体中具有字段的枚举一起使用。

[^1]: 使用 Decoder::skip 忽略 CBOR 项目。此方法需要 "alloc" 功能才能为所有可能的 CBOR 项目工作。如果没有 "alloc",则无法跳过常规映射或数组中的不定映射或数组。如果发生此类组合,并且 Decoder::skip 未使用 "alloc" 功能编译,则会返回解码错误。

依赖项

~250–690KB
~16K SLoC