1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
use std::path::PathBuf;
use clap::{Parser, Subcommand};
pub mod config;
pub mod filter;
pub mod paths;
mod commands {
pub mod analyze;
pub mod check;
pub mod diff;
pub mod explain;
pub mod fix;
pub mod list;
pub mod report;
pub mod watch;
}
mod output {
pub mod terminal;
}
#[derive(Parser)]
#[command(
name = "padlock",
about = "Struct memory layout analyzer for C, C++, Rust, and Go",
version
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Analyze one or more files or directories for struct layout issues
Analyze {
/// Paths to analyze: source files (.c .cpp .rs .go), binaries, or directories
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
/// Output as JSON
#[arg(long)]
json: bool,
/// Output as SARIF (for CI / GitHub Code Scanning annotations)
#[arg(long)]
sarif: bool,
#[command(flatten)]
filter: filter::FilterArgs,
},
/// List all structs found in one or more files with basic stats
List {
/// Paths to analyze: source files, binaries, or directories
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
#[command(flatten)]
filter: filter::FilterArgs,
},
/// Show a diff of original vs optimal field ordering
Diff {
/// Source files or directories to diff (binaries not supported)
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
/// Include only structs whose names match this regex pattern
#[arg(long, short = 'F', value_name = "PATTERN")]
filter: Option<String>,
},
/// Apply automatic field reordering to source files in-place
Fix {
/// Source files or directories to fix (binaries not supported)
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
/// Show the diff without writing any files
#[arg(long)]
dry_run: bool,
/// Include only structs whose names match this regex pattern
#[arg(long, short = 'F', value_name = "PATTERN")]
filter: Option<String>,
},
/// Generate a full report (alias for analyze)
Report {
/// Paths to analyze: source files, binaries, or directories
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
#[arg(long)]
json: bool,
#[command(flatten)]
filter: filter::FilterArgs,
},
/// Watch a file or directory and re-analyse on every change
Watch {
/// Path to watch (source file, binary, or directory)
path: PathBuf,
/// Output results as JSON on each refresh
#[arg(long)]
json: bool,
},
/// Show a visual field-by-field memory layout table for each struct
Explain {
/// Paths to analyze: source files, binaries, or directories
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
/// Include only structs whose names match this regex pattern
#[arg(long, short = 'F', value_name = "PATTERN")]
filter: Option<String>,
},
/// Compare current layout findings against a saved baseline; fail only on regressions
Check {
/// Paths to analyze: source files, binaries, or directories
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
/// Path to baseline JSON file (created with --save-baseline)
#[arg(long, value_name = "FILE")]
baseline: Option<PathBuf>,
/// Save current findings as the new baseline instead of comparing
#[arg(long)]
save_baseline: bool,
/// Output comparison result as JSON
#[arg(long)]
json: bool,
#[command(flatten)]
filter: filter::FilterArgs,
},
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Analyze {
paths,
json,
sarif,
filter,
} => commands::analyze::run(&paths, json, sarif, &filter),
Commands::List { paths, filter } => commands::list::run(&paths, &filter),
Commands::Diff { paths, filter } => commands::diff::run(&paths, filter.as_deref()),
Commands::Fix {
paths,
dry_run,
filter,
} => commands::fix::run(&paths, dry_run, filter.as_deref()),
Commands::Report {
paths,
json,
filter,
} => commands::analyze::run(&paths, json, false, &filter),
Commands::Watch { path, json } => commands::watch::run(&path, json),
Commands::Explain { paths, filter } => {
commands::explain::run(&paths, filter.as_deref())
}
Commands::Check {
paths,
baseline,
save_baseline,
json,
filter,
} => commands::check::run(&paths, baseline.as_deref(), save_baseline, json, &filter),
}
}