prototext_core/lib.rs
1// SPDX-FileCopyrightText: 2025-2026 Frederic Ruget <fred@atlant.is> (GitHub: @douzebis)
2// SPDX-FileCopyrightText: 2025-2026 THALES CLOUD SECURISE SAS
3//
4// SPDX-License-Identifier: MIT
5
6pub mod decoder;
7pub mod helpers;
8pub mod instantiate;
9pub mod schema;
10pub mod serialize;
11
12pub use schema::{decode_pool, schema_from_pool, ParsedSchema, SchemaError};
13pub use serialize::render_text::is_prototext_text;
14
15// ── Public API types ──────────────────────────────────────────────────────────
16
17/// Options controlling how a protobuf binary payload is rendered as text.
18#[derive(Debug, Clone)]
19pub struct RenderOpts {
20 /// When `true`, always treat the input as raw protobuf binary.
21 /// When `false`, auto-detect: if the payload already carries the
22 /// `#@ prototext:` header it is returned unchanged (zero-copy fast path).
23 pub assume_binary: bool,
24 /// Emit inline comments with schema field names and types.
25 pub include_annotations: bool,
26 /// Indentation step in spaces.
27 pub indent: usize,
28 /// When `true` (default), expand `google.protobuf.Any` fields inline
29 /// using the type resolved from `type_url` (spec 0089).
30 pub expand_any: bool,
31}
32
33impl Default for RenderOpts {
34 fn default() -> Self {
35 RenderOpts {
36 assume_binary: false,
37 include_annotations: false,
38 indent: 1,
39 expand_any: true,
40 }
41 }
42}
43
44/// Errors that can occur while decoding or encoding a protobuf payload.
45#[non_exhaustive]
46#[derive(Debug)]
47pub enum CodecError {
48 /// The input bytes could not be decoded as a protobuf wire payload.
49 DecodeFailed(String),
50 /// The input bytes could not be decoded as a textual prototext payload.
51 TextDecodeFailed(String),
52 /// The input does not carry the `#@ prototext:` header required by `encode`.
53 NotPrototext,
54}
55
56impl std::fmt::Display for CodecError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 CodecError::DecodeFailed(msg) => write!(f, "decode failed: {msg}"),
60 CodecError::TextDecodeFailed(msg) => write!(f, "text decode failed: {msg}"),
61 CodecError::NotPrototext => write!(
62 f,
63 "input is not prototext (missing '#@ prototext:' header); \
64 use 'prototext decode' to produce encodable output (annotations on by default)"
65 ),
66 }
67 }
68}
69
70impl std::error::Error for CodecError {}
71
72// ── Public API functions ──────────────────────────────────────────────────────
73
74/// Decode a raw protobuf binary payload and render it as protoc-style text.
75///
76/// When `opts.assume_binary` is `false` and the data already carries the
77/// `#@ prototext:` header, it is first encoded back to binary so that the
78/// schema-aware decoder can re-render it (e.g. with a different schema or
79/// annotation settings). With `assume_binary: true` the data is always
80/// treated as raw binary wire bytes.
81pub fn render_as_text(
82 data: &[u8],
83 schema: Option<&ParsedSchema>,
84 opts: RenderOpts,
85) -> Result<Vec<u8>, CodecError> {
86 let binary;
87 let wire = if !opts.assume_binary && serialize::render_text::is_prototext_text(data) {
88 binary = serialize::encode_text::encode_text_to_binary(data);
89 binary.as_slice()
90 } else {
91 data
92 };
93 Ok(serialize::render_text::decode_and_render(
94 wire,
95 schema,
96 opts.include_annotations,
97 opts.indent,
98 opts.expand_any,
99 ))
100}
101
102/// Encode a textual prototext payload back to raw protobuf binary wire bytes.
103///
104/// When `opts.assume_binary` is `true`, or the input does not carry the
105/// `#@ prototext:` header, the bytes are returned unchanged (pass-through).
106/// When the input carries the header, it is decoded from text to binary.
107pub fn render_as_bytes(data: &[u8], opts: RenderOpts) -> Result<Vec<u8>, CodecError> {
108 if opts.assume_binary || !serialize::render_text::is_prototext_text(data) {
109 Ok(data.to_vec())
110 } else {
111 Ok(serialize::encode_text::encode_text_to_binary(data))
112 }
113}
114
115/// Parse a compiled `.pb` descriptor into a `ParsedSchema`.
116///
117/// Re-exported from `schema` for convenience so callers only need to import
118/// from the crate root.
119pub fn parse_schema(schema_bytes: &[u8], root_message: &str) -> Result<ParsedSchema, SchemaError> {
120 schema::parse_schema(schema_bytes, root_message)
121}