Skip to main content

confers_macros/
lib.rs

1// Copyright (c) 2025 Kirky.X
2//
3// Licensed under the MIT License
4// See LICENSE file in the project root for full license information.
5
6//! Security warning constants and patterns
7const SENSITIVE_FIELD_PATTERNS: &[&str] = &[
8    "password",
9    "token",
10    "secret",
11    "key",
12    "credential",
13    "auth",
14    "private",
15    "cert",
16];
17
18const MAX_INPUT_LENGTH: usize = 10_000;
19
20/// Check if a field name suggests it contains sensitive data
21fn is_sensitive_field_name(field_name: &str) -> bool {
22    let lower = field_name.to_lowercase();
23    SENSITIVE_FIELD_PATTERNS
24        .iter()
25        .any(|pattern| lower.contains(pattern))
26}
27
28/// Check if a value appears to be sensitive (simple heuristic)
29fn is_sensitive_value(value: &str) -> bool {
30    // Check for common secret patterns
31    if value.len() >= 8 {
32        // High entropy check: mix of different character types
33        let has_uppercase = value.chars().any(|c| c.is_uppercase());
34        let has_lowercase = value.chars().any(|c| c.is_lowercase());
35        let has_digit = value.chars().any(|c| c.is_ascii_digit());
36        let has_special = value.chars().any(|c| !c.is_alphanumeric());
37
38        if has_uppercase && has_lowercase && has_digit && has_special {
39            return true;
40        }
41    }
42    false
43}
44
45/// Emit a security warning for hardcoded sensitive values
46fn emit_security_warning(field_name: &str, _value: &str) {
47    // Only log warning, don't fail - maintain backward compatibility
48    eprintln!(
49        "⚠️  SECURITY WARNING: Hardcoded sensitive value detected for field '{}'.\n\
50         Consider using environment variables instead to avoid embedding secrets in compiled code.\n\
51         Example: Use #[config({}_env = \"MY_SECRET_VAR\")] instead of #[config({} = \"...\")]",
52        field_name,
53        field_name.split("_").next().unwrap_or(field_name),
54        field_name
55    );
56}
57
58use darling::FromDeriveInput;
59use proc_macro::TokenStream;
60use proc_macro2::TokenStream as ProcMacro2TokenStream;
61use syn::{parse_macro_input, DeriveInput, Meta};
62
63mod codegen;
64mod confers_common;
65mod parse;
66
67fn has_serde_flatten(attrs: &Vec<syn::Attribute>) -> bool {
68    for attr in attrs {
69        if attr.path().is_ident("serde") {
70            if let syn::Meta::List(list) = &attr.meta {
71                let s = list.tokens.to_string();
72                if s.contains("flatten") {
73                    return true;
74                }
75            }
76        }
77    }
78    false
79}
80
81/// Properly unescape a Rust string literal content
82/// Handles escape sequences: \\\" -> ", \\\\ -> \\\, \n -> newline, \t -> tab
83fn unescape_rust_string(content: &str) -> String {
84    let mut result = String::with_capacity(content.len());
85    let mut chars = content.chars().peekable();
86
87    while let Some(c) = chars.next() {
88        if c == '\\' {
89            match chars.peek() {
90                Some(&'\\') => {
91                    result.push('\\');
92                    chars.next();
93                }
94                Some(&'"') => {
95                    result.push('"');
96                    chars.next();
97                }
98                Some(&'n') => {
99                    result.push('\n');
100                    chars.next();
101                }
102                Some(&'t') => {
103                    result.push('\t');
104                    chars.next();
105                }
106                Some(&'0') => {
107                    result.push('\0');
108                    chars.next();
109                }
110                _ => {
111                    result.push(c);
112                }
113            }
114        } else {
115            result.push(c);
116        }
117    }
118    result
119}
120
121fn has_validate_derive(input: &syn::DeriveInput) -> bool {
122    for attr in &input.attrs {
123        if attr.path().is_ident("derive") {
124            if let syn::Meta::List(list) = &attr.meta {
125                let tokens_str = list.tokens.to_string();
126                if tokens_str.contains("Validate") {
127                    return true;
128                }
129            }
130        }
131    }
132    false
133}
134
135/// Extract default value with input validation and length limits
136fn extract_default_value(tokens_str: &str) -> Option<(String, bool, bool)> {
137    // Security: Enforce input length limit to prevent DoS
138    if tokens_str.len() > MAX_INPUT_LENGTH {
139        eprintln!(
140            "⚠️  SECURITY WARNING: Input token length ({}) exceeds maximum allowed ({}). \
141             Potential denial of service attack detected.",
142            tokens_str.len(),
143            MAX_INPUT_LENGTH
144        );
145        return None;
146    }
147
148    if let Some(start) = tokens_str.find("default = ") {
149        let after_equals = &tokens_str[start + 10..];
150
151        if after_equals.starts_with('"') {
152            let after_first_quote = after_equals
153                .get(1..)
154                .expect("String should have at least one character after quote");
155            let mut i = 0;
156            let mut end_pos = None;
157
158            while i < after_first_quote.len() {
159                let c = after_first_quote.chars().nth(i).unwrap();
160                if c == '\\' && i + 1 < after_first_quote.len() {
161                    let next_char = after_first_quote.chars().nth(i + 1).unwrap();
162                    if next_char == '"' {
163                        i += 2;
164                        continue;
165                    }
166                }
167                if c == '"' {
168                    end_pos = Some(i);
169                    break;
170                }
171                i += 1;
172            }
173
174            if let Some(end) = end_pos {
175                let inner_value = &after_first_quote[..end];
176                // Check if this is already a .to_string() call
177                let already_wrapped = inner_value.contains(".to_string()");
178
179                let (value, wrapped) = if already_wrapped {
180                    // Extract the string part inside quotes before .to_string()
181                    // For input like "\"old_syntax\".to_string()", extract "old_syntax"
182                    let before_to_string = inner_value
183                        .strip_suffix(".to_string()")
184                        .unwrap_or(inner_value);
185
186                    // Manually extract and unescape the string content
187                    // before_to_string is something like \"old_syntax\" (with escaped inner quotes)
188                    // The format is: \"content\" where \" is an escaped quote
189                    // We need to strip the outer \"...\" and unescape inner \"
190
191                    // Check if content is wrapped in escaped quotes: \"...\"
192                    let content = if before_to_string.starts_with("\\\"")
193                        && before_to_string.ends_with("\\\"")
194                    {
195                        &before_to_string[2..before_to_string.len() - 2]
196                    } else if before_to_string.starts_with('"') && before_to_string.ends_with('"') {
197                        // Regular quotes
198                        before_to_string
199                            .strip_prefix('"')
200                            .and_then(|s| s.strip_suffix('"'))
201                            .unwrap_or(before_to_string)
202                    } else {
203                        before_to_string
204                    };
205
206                    // Use proper unescape function for any remaining escapes
207                    let unescaped = unescape_rust_string(content);
208
209                    (unescaped, true)
210                } else {
211                    // Parse the inner value as a string literal (simplified syntax)
212                    // For input like "./storage", wrapped should be false
213                    let parse_str = format!("\"{}\"", inner_value);
214                    if let Ok(lit_str) = syn::parse_str::<syn::LitStr>(&parse_str) {
215                        (lit_str.value(), false)
216                    } else {
217                        // Fallback: strip outer quotes and unescape
218                        let content = inner_value
219                            .strip_prefix('"')
220                            .and_then(|s| s.strip_suffix('"'))
221                            .unwrap_or(inner_value);
222                        let unescaped = unescape_rust_string(content);
223                        (unescaped, false)
224                    }
225                };
226                return Some((value, wrapped, true));
227            }
228        } else {
229            let value = after_equals.trim();
230            let already_wrapped = value.contains(".to_string()");
231            return Some((value.to_string(), already_wrapped, false));
232        }
233    }
234    None
235}
236
237fn process_meta_name_value(nv: &syn::MetaNameValue, opts: &mut parse::FieldOpts) {
238    let ident = nv.path.get_ident().map(|i| i.to_string());
239    let field_name = opts
240        .ident
241        .as_ref()
242        .map(|i| i.to_string())
243        .unwrap_or_default();
244    let is_sensitive_field = is_sensitive_field_name(&field_name);
245
246    match ident.as_deref() {
247        Some("description") => {
248            if let syn::Expr::Lit(syn::ExprLit {
249                lit: syn::Lit::Str(s),
250                ..
251            }) = &nv.value
252            {
253                opts.description = Some(s.value());
254            }
255        }
256        Some("default") => {
257            if opts.default.is_none() {
258                opts.default = Some(nv.value.clone());
259            }
260        }
261        Some("name") => {
262            if let syn::Expr::Lit(syn::ExprLit {
263                lit: syn::Lit::Str(s),
264                ..
265            }) = &nv.value
266            {
267                opts.name_config = Some(s.value());
268            }
269        }
270        Some("name_env") => {
271            if let syn::Expr::Lit(syn::ExprLit {
272                lit: syn::Lit::Str(s),
273                ..
274            }) = &nv.value
275            {
276                opts.name_env = Some(s.value());
277            }
278        }
279        Some("validate") => {
280            if let syn::Expr::Lit(syn::ExprLit {
281                lit: syn::Lit::Str(s),
282                ..
283            }) = &nv.value
284            {
285                opts.validate = Some(s.value());
286            } else if let syn::Expr::Lit(syn::ExprLit {
287                lit: syn::Lit::Bool(b),
288                ..
289            }) = &nv.value
290            {
291                if b.value {
292                    opts.validate = Some("true".to_string());
293                }
294            } else {
295                let s = quote::quote!(#nv.value).to_string();
296                opts.validate = Some(s);
297            }
298        }
299        Some("custom_validate") => {
300            if let syn::Expr::Lit(syn::ExprLit {
301                lit: syn::Lit::Str(s),
302                ..
303            }) = &nv.value
304            {
305                opts.custom_validate = Some(s.value());
306            }
307        }
308        Some("sensitive") => {
309            if let syn::Expr::Lit(syn::ExprLit {
310                lit: syn::Lit::Bool(b),
311                ..
312            }) = &nv.value
313            {
314                opts.sensitive = Some(b.value);
315            }
316        }
317        Some("remote") => {
318            if let syn::Expr::Lit(syn::ExprLit {
319                lit: syn::Lit::Str(s),
320                ..
321            }) = &nv.value
322            {
323                opts.remote = Some(s.value());
324            }
325        }
326        Some("remote_timeout") => {
327            if let syn::Expr::Lit(syn::ExprLit {
328                lit: syn::Lit::Str(s),
329                ..
330            }) = &nv.value
331            {
332                opts.remote_timeout = Some(s.value());
333            }
334        }
335        Some("remote_auth") => {
336            if let syn::Expr::Lit(syn::ExprLit {
337                lit: syn::Lit::Bool(b),
338                ..
339            }) = &nv.value
340            {
341                opts.remote_auth = Some(b.value);
342            }
343        }
344        Some("remote_username") => {
345            if let syn::Expr::Lit(syn::ExprLit {
346                lit: syn::Lit::Str(s),
347                ..
348            }) = &nv.value
349            {
350                opts.remote_username = Some(s.value());
351            }
352        }
353        Some("remote_password") => {
354            if let syn::Expr::Lit(syn::ExprLit {
355                lit: syn::Lit::Str(s),
356                ..
357            }) = &nv.value
358            {
359                let value = s.value();
360                // Security check: warn about hardcoded passwords
361                if is_sensitive_field || is_sensitive_value(&value) {
362                    emit_security_warning("remote_password", &value);
363                }
364                opts.remote_password = Some(value);
365            }
366        }
367        Some("remote_token") => {
368            if let syn::Expr::Lit(syn::ExprLit {
369                lit: syn::Lit::Str(s),
370                ..
371            }) = &nv.value
372            {
373                let value = s.value();
374                // Security check: warn about hardcoded tokens
375                if is_sensitive_field || is_sensitive_value(&value) {
376                    emit_security_warning("remote_token", &value);
377                }
378                opts.remote_token = Some(value);
379            }
380        }
381        Some("remote_tls") => {
382            if let syn::Expr::Lit(syn::ExprLit {
383                lit: syn::Lit::Bool(b),
384                ..
385            }) = &nv.value
386            {
387                opts.remote_tls = Some(b.value);
388            }
389        }
390        Some("remote_ca_cert") => {
391            if let syn::Expr::Lit(syn::ExprLit {
392                lit: syn::Lit::Str(s),
393                ..
394            }) = &nv.value
395            {
396                opts.remote_ca_cert = Some(s.value());
397            }
398        }
399        Some("remote_client_cert") => {
400            if let syn::Expr::Lit(syn::ExprLit {
401                lit: syn::Lit::Str(s),
402                ..
403            }) = &nv.value
404            {
405                opts.remote_client_cert = Some(s.value());
406            }
407        }
408        Some("remote_client_key") => {
409            if let syn::Expr::Lit(syn::ExprLit {
410                lit: syn::Lit::Str(s),
411                ..
412            }) = &nv.value
413            {
414                let value = s.value();
415                // Security check: warn about hardcoded private keys
416                if is_sensitive_field || is_sensitive_value(&value) {
417                    emit_security_warning("remote_client_key", &value);
418                }
419                opts.remote_client_key = Some(value);
420            }
421        }
422        _ => {}
423    }
424}
425
426fn parse_field_opts(field: &syn::Field) -> parse::FieldOpts {
427    let mut opts = parse::FieldOpts {
428        ident: field.ident.clone(),
429        ty: field.ty.clone(),
430        attrs: field.attrs.clone(),
431        description: None,
432        default: None,
433        flatten: false,
434        serde_flatten: false,
435        skip: false,
436        name_config: None,
437        name_env: None,
438        name_clap_long: None,
439        name_clap_short: None,
440        validate: None,
441        custom_validate: None,
442        sensitive: None,
443        remote: None,
444        remote_timeout: None,
445        remote_auth: None,
446        remote_username: None,
447        remote_password: None,
448        remote_token: None,
449        remote_tls: None,
450        remote_ca_cert: None,
451        remote_client_cert: None,
452        remote_client_key: None,
453    };
454
455    for attr in &field.attrs {
456        if !attr.path().is_ident("config") {
457            continue;
458        }
459
460        if let Meta::List(list) = &attr.meta {
461            let tokens_str = list.tokens.to_string();
462
463            if let Some((value, already_wrapped, is_string)) = extract_default_value(&tokens_str) {
464                if is_string {
465                    if already_wrapped {
466                        let lit = syn::LitStr::new(&value, proc_macro2::Span::call_site());
467                        let expr: syn::Expr = syn::parse_quote! { #lit };
468                        opts.default = Some(expr);
469                    } else {
470                        let wrapped_str = format!("\"{}\".to_string()", value);
471                        if let Ok(expr) = syn::parse_str::<syn::Expr>(&wrapped_str) {
472                            opts.default = Some(expr);
473                        }
474                    }
475                }
476
477                if !is_string {
478                    if already_wrapped {
479                        let wrapped_str = format!("{}.to_string()", value);
480                        if let Ok(expr) = syn::parse_str::<syn::Expr>(&wrapped_str) {
481                            opts.default = Some(expr);
482                        }
483                    }
484
485                    if !already_wrapped {
486                        if let Ok(expr) = syn::parse_str::<syn::Expr>(&value) {
487                            opts.default = Some(expr);
488                        }
489                    }
490                }
491            }
492
493            // Build tokens stream for parsing MetaNameValue
494            let tokens_stream: ProcMacro2TokenStream = list.tokens.clone().into_iter().collect();
495
496            // Try to parse as a single MetaNameValue (for simple cases like name_env = "value")
497            if let Ok(nv) = syn::parse2::<syn::MetaNameValue>(tokens_stream.clone()) {
498                process_meta_name_value(&nv, &mut opts);
499            } else {
500                // Try to parse individual MetaNameValue pairs
501                let mut current_nv_tokens = ProcMacro2TokenStream::new();
502                let mut expect_value = false;
503
504                for token in tokens_stream {
505                    if let proc_macro2::TokenTree::Ident(ident) = &token {
506                        let ident_str = ident.to_string();
507                        // Check if this is a keyword that should be treated as a flag
508                        if ident_str == "flatten" {
509                            opts.flatten = true;
510                            continue;
511                        } else if ident_str == "skip" {
512                            opts.skip = true;
513                            continue;
514                        }
515                    }
516
517                    if let proc_macro2::TokenTree::Punct(punct) = &token {
518                        if punct.as_char() == '=' && !expect_value {
519                            expect_value = true;
520                            continue;
521                        }
522                    }
523
524                    if expect_value {
525                        current_nv_tokens = ProcMacro2TokenStream::new();
526                        expect_value = false;
527                    }
528
529                    current_nv_tokens.extend(std::iter::once(token));
530
531                    // Try to parse when we have a complete MetaNameValue
532                    if current_nv_tokens.clone().into_iter().next().is_some() {
533                        // Check if we have an = sign in the stream
534                        let has_equals = current_nv_tokens.clone().into_iter().any(|t| {
535                            if let proc_macro2::TokenTree::Punct(p) = t {
536                                p.as_char() == '='
537                            } else {
538                                false
539                            }
540                        });
541
542                        if has_equals {
543                            if let Ok(nv) =
544                                syn::parse2::<syn::MetaNameValue>(current_nv_tokens.clone())
545                            {
546                                process_meta_name_value(&nv, &mut opts);
547                                current_nv_tokens = ProcMacro2TokenStream::new();
548                            }
549                        }
550                    }
551                }
552
553                // Handle remaining tokens in Group
554                for item in list.tokens.clone().into_iter() {
555                    if let proc_macro2::TokenTree::Group(group) = item {
556                        for inner in group.stream().into_iter() {
557                            if let Ok(nv) = syn::parse2::<syn::MetaNameValue>(
558                                ProcMacro2TokenStream::from(inner),
559                            ) {
560                                process_meta_name_value(&nv, &mut opts);
561                            }
562                        }
563                    }
564                }
565            }
566        }
567    }
568
569    opts
570}
571
572#[proc_macro_derive(Config, attributes(config))]
573pub fn derive_config(input: TokenStream) -> TokenStream {
574    let input = parse_macro_input!(input as DeriveInput);
575
576    let opts = match parse::ConfigOpts::from_derive_input(&input) {
577        Ok(val) => val,
578        Err(err) => return err.write_errors().into(),
579    };
580
581    let fields = match &input.data {
582        syn::Data::Struct(data) => {
583            let mut field_opts = Vec::new();
584            for field in &data.fields {
585                let mut f = parse_field_opts(field);
586
587                if has_serde_flatten(&field.attrs) {
588                    f.serde_flatten = true;
589                    f.flatten = true;
590                }
591
592                field_opts.push(f);
593            }
594            field_opts
595        }
596        _ => {
597            return syn::Error::new_spanned(input, "Config can only be derived for structs")
598                .to_compile_error()
599                .into();
600        }
601    };
602
603    let has_validate = has_validate_derive(&input);
604    codegen::generate_impl(&opts, &fields, has_validate).into()
605}