1#![deny(unsafe_code)]
2#![deny(missing_docs)]
3#![deny(clippy::all)]
4#![deny(clippy::pedantic)]
5
6use proc_macro::{Delimiter, Group, TokenStream, TokenTree};
13
14#[proc_macro_derive(Base64Secret)]
33pub fn derive_base64_secret(input: TokenStream) -> TokenStream {
34 match expand_base64_secret(input) {
35 Ok(tokens) => tokens,
36 Err(error) => compile_error(&error),
37 }
38}
39
40fn expand_base64_secret(input: TokenStream) -> Result<TokenStream, String> {
41 let tokens = input.into_iter().collect::<Vec<_>>();
42 let struct_index = find_struct_index(&tokens)?;
43 let name = struct_name(&tokens, struct_index)?;
44 let field_group = tuple_field_group(&tokens, struct_index)?;
45 let length = array_field_length(&field_group)?;
46
47 let expanded = format!(
48 r#"
49impl {name} {{
50 /// Parses strict standard padded Base64 into this fixed-size newtype.
51 ///
52 /// Decoding uses `base64_ng::ct::STANDARD` and stages plaintext privately
53 /// until the full input has been accepted.
54 pub fn from_base64(input: &[u8]) -> Result<Self, ::base64_ng::DecodeError> {{
55 let required = ::base64_ng::ct::STANDARD.decoded_len(input)?;
56 if required != {length} {{
57 return Err(::base64_ng::DecodeError::InvalidLength);
58 }}
59
60 let mut output: [u8; {length}] = [0u8; {length}];
61 let mut staging: [u8; {length}] = [0u8; {length}];
62 let written = match ::base64_ng::ct::STANDARD
63 .decode_slice_staged_clear_tail(input, &mut output, &mut staging)
64 {{
65 Ok(written) => written,
66 Err(error) => {{
67 ::base64_ng::clear_bytes(&mut output);
68 ::base64_ng::clear_bytes(&mut staging);
69 return Err(error);
70 }}
71 }};
72
73 if written != {length} {{
74 ::base64_ng::clear_bytes(&mut output);
75 ::base64_ng::clear_bytes(&mut staging);
76 return Err(::base64_ng::DecodeError::InvalidLength);
77 }}
78
79 ::base64_ng::clear_bytes(&mut staging);
80 Ok(Self(output))
81 }}
82
83 /// Parses strict standard padded Base64 text into this fixed-size newtype.
84 pub fn from_base64_str(input: &str) -> Result<Self, ::base64_ng::DecodeError> {{
85 Self::from_base64(input.as_bytes())
86 }}
87
88 /// Encodes this value as strict standard padded Base64.
89 pub fn encode_base64<const CAP: usize>(
90 &self,
91 ) -> Result<::base64_ng::EncodedBuffer<CAP>, ::base64_ng::EncodeError> {{
92 ::base64_ng::STANDARD.encode_buffer::<CAP>(&self.0)
93 }}
94
95 /// Returns the wrapped bytes.
96 #[must_use]
97 pub fn as_bytes(&self) -> &[u8] {{
98 &self.0
99 }}
100
101 /// Returns the wrapped bytes mutably.
102 ///
103 /// Callers that mutate secret bytes are responsible for preserving any
104 /// application-level invariants attached to this newtype.
105 pub fn as_mut_bytes(&mut self) -> &mut [u8] {{
106 &mut self.0
107 }}
108
109 /// Compares two values with a fixed-width, constant-time-oriented scan.
110 ///
111 /// The final equality result is public. This inherits the caveats of
112 /// `base64_ng::constant_time_eq_fixed_width`.
113 #[must_use]
114 pub fn constant_time_eq(&self, other: &Self) -> bool {{
115 ::base64_ng::constant_time_eq_fixed_width(&self.0, &other.0)
116 }}
117}}
118
119impl ::core::convert::TryFrom<&[u8]> for {name} {{
120 type Error = ::base64_ng::DecodeError;
121
122 fn try_from(input: &[u8]) -> Result<Self, Self::Error> {{
123 Self::from_base64(input)
124 }}
125}}
126
127impl ::core::convert::TryFrom<&str> for {name} {{
128 type Error = ::base64_ng::DecodeError;
129
130 fn try_from(input: &str) -> Result<Self, Self::Error> {{
131 Self::from_base64_str(input)
132 }}
133}}
134
135impl ::core::str::FromStr for {name} {{
136 type Err = ::base64_ng::DecodeError;
137
138 fn from_str(input: &str) -> Result<Self, Self::Err> {{
139 Self::from_base64_str(input)
140 }}
141}}
142
143impl ::core::convert::From<[u8; {length}]> for {name} {{
144 fn from(bytes: [u8; {length}]) -> Self {{
145 Self(bytes)
146 }}
147}}
148
149impl ::core::convert::AsRef<[u8]> for {name} {{
150 fn as_ref(&self) -> &[u8] {{
151 self.as_bytes()
152 }}
153}}
154
155impl ::core::fmt::Debug for {name} {{
156 fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {{
157 formatter
158 .debug_struct(::core::stringify!({name}))
159 .field("bytes", &"<redacted>")
160 .field("len", &{length})
161 .finish()
162 }}
163}}
164
165impl ::core::ops::Drop for {name} {{
166 fn drop(&mut self) {{
167 ::base64_ng::clear_bytes(&mut self.0);
168 }}
169}}
170"#
171 );
172
173 expanded
174 .parse()
175 .map_err(|error| format!("failed to generate Base64Secret impl: {error}"))
176}
177
178fn find_struct_index(tokens: &[TokenTree]) -> Result<usize, String> {
179 tokens
180 .iter()
181 .position(|token| matches!(token, TokenTree::Ident(ident) if ident.to_string() == "struct"))
182 .ok_or_else(|| "Base64Secret can only be derived for tuple structs".to_owned())
183}
184
185fn struct_name(tokens: &[TokenTree], struct_index: usize) -> Result<String, String> {
186 tokens
187 .get(struct_index + 1)
188 .and_then(|token| match token {
189 TokenTree::Ident(ident) => Some(ident.to_string()),
190 _ => None,
191 })
192 .ok_or_else(|| "Base64Secret could not find a struct name".to_owned())
193}
194
195fn tuple_field_group(tokens: &[TokenTree], struct_index: usize) -> Result<Group, String> {
196 tokens
197 .iter()
198 .skip(struct_index + 2)
199 .find_map(|token| match token {
200 TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
201 Some(group.clone())
202 }
203 TokenTree::Group(group) if group.delimiter() == Delimiter::Brace => None,
204 _ => None,
205 })
206 .ok_or_else(|| {
207 "Base64Secret supports only tuple structs like `struct Key([u8; 32]);`".to_owned()
208 })
209}
210
211fn array_field_length(group: &Group) -> Result<String, String> {
212 let mut field_tokens = strip_field_visibility(group.stream().into_iter().collect::<Vec<_>>())?;
213 if field_tokens
214 .iter()
215 .any(|token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == ','))
216 {
217 return Err("Base64Secret supports exactly one tuple field".to_owned());
218 }
219
220 if field_tokens.len() != 1 {
221 return Err("Base64Secret field must be exactly `[u8; N]`".to_owned());
222 }
223
224 let Some(TokenTree::Group(array_group)) = field_tokens.pop() else {
225 return Err("Base64Secret field must be a `[u8; N]` byte array".to_owned());
226 };
227 if array_group.delimiter() != Delimiter::Bracket {
228 return Err("Base64Secret field must be a `[u8; N]` byte array".to_owned());
229 }
230
231 let array_tokens = array_group.stream().into_iter().collect::<Vec<_>>();
232 parse_array_length(&array_tokens)
233}
234
235fn strip_field_visibility(mut tokens: Vec<TokenTree>) -> Result<Vec<TokenTree>, String> {
236 if matches!(tokens.first(), Some(TokenTree::Ident(ident)) if ident.to_string() == "pub") {
237 tokens.remove(0);
238 if matches!(tokens.first(), Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis)
239 {
240 tokens.remove(0);
241 }
242 }
243 if tokens
244 .iter()
245 .any(|token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == '#'))
246 {
247 return Err("Base64Secret tuple fields may not have field attributes".to_owned());
248 }
249 Ok(tokens)
250}
251
252fn parse_array_length(tokens: &[TokenTree]) -> Result<String, String> {
253 let semicolon = tokens
254 .iter()
255 .position(|token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == ';'))
256 .ok_or_else(|| "Base64Secret field must be `[u8; N]`".to_owned())?;
257
258 let element = tokens[..semicolon]
259 .iter()
260 .map(TokenTree::to_string)
261 .collect::<String>();
262 if element != "u8" {
263 return Err("Base64Secret field element type must be `u8`".to_owned());
264 }
265
266 let length = tokens[semicolon + 1..]
267 .iter()
268 .map(TokenTree::to_string)
269 .collect::<Vec<_>>()
270 .join(" ");
271 if length.is_empty() {
272 return Err("Base64Secret field length is missing".to_owned());
273 }
274 Ok(length)
275}
276
277fn compile_error(message: &str) -> TokenStream {
278 let source = format!("compile_error!({message:?});");
279 match source.parse() {
280 Ok(tokens) => tokens,
281 Err(_) => TokenStream::new(),
282 }
283}