Skip to main content

prototext_core/serialize/common/
format.rs

1// SPDX-FileCopyrightText: 2025 - 2026 Frederic Ruget <fred@atlant.is> <fred@s3ns.io> (GitHub: @douzebis)
2// SPDX-FileCopyrightText: 2025 - 2026 Thales Cloud Sécurisé
3//
4// SPDX-License-Identifier: MIT
5
6use crate::decoder::{ProtoTextContent, ProtoTextField};
7
8use super::escape::{escape_bytes, escape_string};
9use super::floats::{format_double_protoc, format_float_protoc};
10use super::scalars::{
11    format_bool_protoc, format_enum_protoc, format_fixed32_protoc, format_fixed64_protoc,
12    format_int32_protoc, format_int64_protoc, format_sfixed32_protoc, format_sfixed64_protoc,
13    format_sint32_protoc, format_sint64_protoc, format_uint32_protoc, format_uint64_protoc,
14    format_wire_fixed32_protoc, format_wire_fixed64_protoc, format_wire_varint_protoc,
15};
16
17// ── format_protoc_value ───────────────────────────────────────────────────────
18//
19// Mirrors `format_protoc_value()` in common.py.
20// Returns `None` for fields that should be skipped (INVALID_*, packed repeats,
21// untyped BYTES/message when handling is done as nested).
22// `include_wire_types`: true for protoc, false for protoc_meticulous.
23
24pub fn format_protoc_value(field: &ProtoTextField, include_wire_types: bool) -> Option<String> {
25    match &field.content {
26        // Always skip invalid fields
27        ProtoTextContent::InvalidTagType(_)
28        | ProtoTextContent::InvalidVarint(_)
29        | ProtoTextContent::InvalidFixed64(_)
30        | ProtoTextContent::InvalidFixed32(_)
31        | ProtoTextContent::InvalidBytesLength(_)
32        | ProtoTextContent::TruncatedBytes(_)
33        | ProtoTextContent::InvalidPackedRecords(_)
34        | ProtoTextContent::InvalidString(_)
35        | ProtoTextContent::InvalidGroupEnd(_) => None,
36
37        // Generic wire types: only in protoc (include_wire_types=true)
38        ProtoTextContent::WireVarint(v) => {
39            if include_wire_types {
40                Some(format_wire_varint_protoc(*v))
41            } else {
42                None
43            }
44        }
45        ProtoTextContent::WireFixed64(v) => {
46            if include_wire_types {
47                Some(format_wire_fixed64_protoc(*v))
48            } else {
49                None
50            }
51        }
52        ProtoTextContent::WireFixed32(v) => {
53            if include_wire_types {
54                Some(format_wire_fixed32_protoc(*v))
55            } else {
56                None
57            }
58        }
59        ProtoTextContent::WireBytes(b) => {
60            if include_wire_types {
61                Some(format!("\"{}\"", escape_bytes(b)))
62            } else {
63                None
64            }
65        }
66
67        // Typed VARINT
68        ProtoTextContent::Int64(v) => Some(format_int64_protoc(*v)),
69        ProtoTextContent::Uint64(v) => Some(format_uint64_protoc(*v)),
70        ProtoTextContent::Int32(v) => Some(format_int32_protoc(*v)),
71        ProtoTextContent::Uint32(v) => Some(format_uint32_protoc(*v)),
72        ProtoTextContent::Bool(v) => Some(format_bool_protoc(*v).to_owned()),
73        ProtoTextContent::Enum(v) => Some(format_enum_protoc(*v)),
74        ProtoTextContent::Sint32(v) => Some(format_sint32_protoc(*v)),
75        ProtoTextContent::Sint64(v) => Some(format_sint64_protoc(*v)),
76
77        // Typed FIXED64
78        ProtoTextContent::Double(v) => Some(format_double_protoc(*v)),
79        ProtoTextContent::PFixed64(v) => Some(format_fixed64_protoc(*v)),
80        ProtoTextContent::Sfixed64(v) => Some(format_sfixed64_protoc(*v)),
81
82        // Typed FIXED32
83        ProtoTextContent::Float(v) => Some(format_float_protoc(*v)),
84        ProtoTextContent::PFixed32(v) => Some(format_fixed32_protoc(*v)),
85        ProtoTextContent::Sfixed32(v) => Some(format_sfixed32_protoc(*v)),
86
87        // Length-delimited scalars
88        ProtoTextContent::StringVal(s) => Some(format!("\"{}\"", escape_string(s))),
89        ProtoTextContent::BytesVal(b) => Some(format!("\"{}\"", escape_bytes(b))),
90
91        // Nested (handled as child block, not here)
92        ProtoTextContent::MessageVal(_)
93        | ProtoTextContent::Group(_)
94        | ProtoTextContent::WireGroup(_) => None,
95
96        // Packed repeated: render as "[val1, val2, ...]" (matching Python's protoc output)
97        ProtoTextContent::Doubles(vs) => Some(format!(
98            "[{}]",
99            vs.iter()
100                .map(|v| format_double_protoc(*v))
101                .collect::<Vec<_>>()
102                .join(", ")
103        )),
104        ProtoTextContent::Floats(vs) => Some(format!(
105            "[{}]",
106            vs.iter()
107                .map(|v| format_float_protoc(*v))
108                .collect::<Vec<_>>()
109                .join(", ")
110        )),
111        ProtoTextContent::Int64s(vs) => Some(format!(
112            "[{}]",
113            vs.iter()
114                .map(|v| format_int64_protoc(*v))
115                .collect::<Vec<_>>()
116                .join(", ")
117        )),
118        ProtoTextContent::Uint64s(vs) => Some(format!(
119            "[{}]",
120            vs.iter()
121                .map(|v| format_uint64_protoc(*v))
122                .collect::<Vec<_>>()
123                .join(", ")
124        )),
125        ProtoTextContent::Int32s(vs) => Some(format!(
126            "[{}]",
127            vs.iter()
128                .map(|v| format_int32_protoc(*v))
129                .collect::<Vec<_>>()
130                .join(", ")
131        )),
132        ProtoTextContent::Fixed64s(vs) => Some(format!(
133            "[{}]",
134            vs.iter()
135                .map(|v| format_fixed64_protoc(*v))
136                .collect::<Vec<_>>()
137                .join(", ")
138        )),
139        ProtoTextContent::Fixed32s(vs) => Some(format!(
140            "[{}]",
141            vs.iter()
142                .map(|v| format_fixed32_protoc(*v))
143                .collect::<Vec<_>>()
144                .join(", ")
145        )),
146        ProtoTextContent::Bools(vs) => Some(format!(
147            "[{}]",
148            vs.iter()
149                .map(|v| format_bool_protoc(*v))
150                .collect::<Vec<_>>()
151                .join(", ")
152        )),
153        ProtoTextContent::Uint32s(vs) => Some(format!(
154            "[{}]",
155            vs.iter()
156                .map(|v| format_uint32_protoc(*v))
157                .collect::<Vec<_>>()
158                .join(", ")
159        )),
160        ProtoTextContent::Enums(vs) => Some(format!(
161            "[{}]",
162            vs.iter()
163                .map(|v| format_enum_protoc(*v))
164                .collect::<Vec<_>>()
165                .join(", ")
166        )),
167        ProtoTextContent::Sfixed32s(vs) => Some(format!(
168            "[{}]",
169            vs.iter()
170                .map(|v| format_sfixed32_protoc(*v))
171                .collect::<Vec<_>>()
172                .join(", ")
173        )),
174        ProtoTextContent::Sfixed64s(vs) => Some(format!(
175            "[{}]",
176            vs.iter()
177                .map(|v| format_sfixed64_protoc(*v))
178                .collect::<Vec<_>>()
179                .join(", ")
180        )),
181        ProtoTextContent::Sint32s(vs) => Some(format!(
182            "[{}]",
183            vs.iter()
184                .map(|v| format_sint32_protoc(*v))
185                .collect::<Vec<_>>()
186                .join(", ")
187        )),
188        ProtoTextContent::Sint64s(vs) => Some(format!(
189            "[{}]",
190            vs.iter()
191                .map(|v| format_sint64_protoc(*v))
192                .collect::<Vec<_>>()
193                .join(", ")
194        )),
195
196        ProtoTextContent::Unset => None,
197    }
198}
199
200// ── is_nested / is_invalid helpers ───────────────────────────────────────────
201
202#[inline]
203pub fn is_nested(content: &ProtoTextContent) -> bool {
204    matches!(
205        content,
206        ProtoTextContent::MessageVal(_)
207            | ProtoTextContent::Group(_)
208            | ProtoTextContent::WireGroup(_)
209    )
210}
211
212#[inline]
213pub fn is_invalid(content: &ProtoTextContent) -> bool {
214    matches!(
215        content,
216        ProtoTextContent::InvalidTagType(_)
217            | ProtoTextContent::InvalidVarint(_)
218            | ProtoTextContent::InvalidFixed64(_)
219            | ProtoTextContent::InvalidFixed32(_)
220            | ProtoTextContent::InvalidBytesLength(_)
221            | ProtoTextContent::TruncatedBytes(_)
222            | ProtoTextContent::InvalidPackedRecords(_)
223            | ProtoTextContent::InvalidString(_)
224            | ProtoTextContent::InvalidGroupEnd(_)
225    )
226}
227
228// ── Modifier lines ────────────────────────────────────────────────────────────
229//
230// Mirrors `get_modifier_strings(include_type=True)` in common.py.
231
232pub struct Modifier {
233    pub text: String,
234}
235
236pub fn get_modifiers(field: &ProtoTextField) -> Vec<Modifier> {
237    let mut out = Vec::new();
238
239    if let Some(v) = field.tag_overhang_count {
240        out.push(Modifier {
241            text: format!("tag_overhang_count: {}", v),
242        });
243    }
244    if field.tag_is_out_of_range {
245        out.push(Modifier {
246            text: "tag_is_out_of_range: true".to_owned(),
247        });
248    }
249    if let Some(v) = field.value_overhang_count {
250        out.push(Modifier {
251            text: format!("value_overhang_count: {}", v),
252        });
253    }
254    if let Some(v) = field.length_overhang_count {
255        out.push(Modifier {
256            text: format!("length_overhang_count: {}", v),
257        });
258    }
259    if let Some(v) = field.missing_bytes_count {
260        out.push(Modifier {
261            text: format!("missing_bytes_count: {}", v),
262        });
263    }
264    if let Some(v) = field.mismatched_group_end {
265        out.push(Modifier {
266            text: format!("mismatched_group_end: {}", v),
267        });
268    }
269    if field.open_ended_group {
270        out.push(Modifier {
271            text: "open_ended_group: true".to_owned(),
272        });
273    }
274    if let Some(v) = field.end_tag_overhang_count {
275        out.push(Modifier {
276            text: format!("end_tag_overhang_count: {}", v),
277        });
278    }
279    if field.end_tag_is_out_of_range {
280        out.push(Modifier {
281            text: "end_tag_is_out_of_range: true".to_owned(),
282        });
283    }
284    if field.proto2_has_type_mismatch {
285        out.push(Modifier {
286            text: "proto2_has_type_mismatch: true".to_owned(),
287        });
288    }
289    if !field.records_overhung_count.is_empty() {
290        let vals: Vec<String> = field
291            .records_overhung_count
292            .iter()
293            .map(|v| v.to_string())
294            .collect();
295        out.push(Modifier {
296            text: format!("records_overhung_count: [{}]", vals.join(", ")),
297        });
298    }
299    out
300}