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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
use std::path::PathBuf;
use clap::{Parser, Subcommand};
pub mod cache;
pub mod config;
pub mod filter;
pub mod paths;
mod commands {
pub mod analyze;
pub mod bpf;
pub mod check;
pub mod diff;
pub mod explain;
pub mod fix;
pub mod init;
pub mod list;
pub mod report;
pub mod summary;
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 = concat!(env!("CARGO_PKG_VERSION"), " (", env!("BUILD_GIT_SHA"), ")")
)]
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 .zig), 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,
/// Output as Markdown (suitable for CI step summaries or PR comment bots)
#[arg(long)]
markdown: bool,
/// Override the cache-line size in bytes (e.g. 128 for Apple Silicon or POWER)
#[arg(long, value_name = "BYTES")]
cache_line_size: Option<usize>,
/// Override the pointer/word size in bytes (e.g. 4 for 32-bit targets)
#[arg(long, value_name = "BYTES")]
word_size: Option<usize>,
/// Exit with a non-zero status code if any finding meets or exceeds this severity
/// (high, medium, or low). Useful for stricter CI gates.
#[arg(long, value_name = "SEVERITY")]
fail_on_severity: Option<filter::FailSeverity>,
/// Target architecture as a Rust triple or short name
/// (e.g. aarch64-apple-darwin, x86_64-unknown-linux-gnu, aarch64, wasm32).
/// Overrides the arch.override config value.
#[arg(long, value_name = "TRIPLE")]
target: Option<String>,
/// C++ standard library variant for type-size lookups.
/// Affects sizes of std::string, std::mutex, etc.
/// Choices: libstdc++ (GCC/Linux default), libc++ (Clang/macOS), msvc (Windows).
#[arg(long, value_name = "VARIANT")]
stdlib: Option<String>,
#[command(flatten)]
filter: filter::FilterArgs,
},
/// Show a project-level health summary: aggregate score, severity distribution,
/// worst files, and worst structs. Designed for large codebases where `analyze`
/// output is too verbose.
Summary {
/// Paths to analyze: source files, binaries, or directories
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
/// Number of worst files and structs to show (default: 5)
#[arg(long, value_name = "N", default_value = "5")]
top: usize,
/// Override the cache-line size in bytes
#[arg(long, value_name = "BYTES")]
cache_line_size: Option<usize>,
/// Override the pointer/word size in bytes
#[arg(long, value_name = "BYTES")]
word_size: Option<usize>,
/// Target architecture as a Rust triple or short name
#[arg(long, value_name = "TRIPLE")]
target: Option<String>,
#[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; exits 1 when any reorderings
/// are pending (like `git diff --exit-code`), so it can gate CI pipelines
#[arg(long)]
dry_run: bool,
/// Keep a .bak copy of the original file before rewriting
#[arg(long)]
backup: 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>,
},
/// Analyse eBPF object files or binaries that contain a .BTF section.
///
/// This is an alias for `padlock analyze` that accepts the same paths and
/// flags but prints a brief note reminding users that BTF-derived layouts
/// reflect the compiled types, not the source, and that false-sharing
/// findings for BPF map structs are directly actionable.
///
/// Example: padlock bpf my_prog.bpf.o --json
Bpf {
/// eBPF object files or binaries with a .BTF ELF section
#[arg(num_args = 1.., value_name = "PATH")]
paths: Vec<PathBuf>,
/// Output as JSON
#[arg(long)]
json: bool,
/// Output as SARIF
#[arg(long)]
sarif: bool,
/// Exit non-zero when any finding meets or exceeds this severity
#[arg(long, value_name = "SEVERITY")]
fail_on_severity: Option<filter::FailSeverity>,
#[command(flatten)]
filter: filter::FilterArgs,
},
/// Generate a .padlock.toml configuration template in the current directory
Init,
/// 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,
/// Target architecture as a Rust triple or short name
#[arg(long, value_name = "TRIPLE")]
target: Option<String>,
#[command(flatten)]
filter: filter::FilterArgs,
},
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Analyze {
paths,
json,
sarif,
markdown,
cache_line_size,
word_size,
fail_on_severity,
target,
stdlib,
filter,
} => commands::analyze::run(
&paths,
commands::analyze::AnalyzeOpts {
json,
sarif,
markdown,
cache_line_size,
word_size,
fail_on_severity,
target,
stdlib: stdlib.as_deref().and_then(parse_stdlib),
},
&filter,
),
Commands::Summary {
paths,
top,
cache_line_size,
word_size,
target,
filter,
} => commands::summary::run(&paths, top, cache_line_size, word_size, target, &filter),
Commands::Bpf {
paths,
json,
sarif,
fail_on_severity,
filter,
} => commands::bpf::run(&paths, json, sarif, fail_on_severity, &filter),
Commands::Init => commands::init::run(),
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,
backup,
filter,
} => commands::fix::run(&paths, dry_run, backup, filter.as_deref()),
Commands::Report {
paths,
json,
filter,
} => commands::analyze::run(
&paths,
commands::analyze::AnalyzeOpts {
json,
sarif: false,
markdown: false,
cache_line_size: None,
word_size: None,
fail_on_severity: None,
target: None,
stdlib: None,
},
&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,
target,
filter,
} => commands::check::run(
&paths,
baseline.as_deref(),
save_baseline,
json,
target,
&filter,
),
}
}
fn parse_stdlib(s: &str) -> Option<padlock_source::CppStdlib> {
match s.to_ascii_lowercase().replace(['-', '_', '+'], "").as_str() {
"libstdcpp" | "stdcpp" | "gcc" | "gnustl" => Some(padlock_source::CppStdlib::LibStdCpp),
"libcpp" | "libc" | "clang" => Some(padlock_source::CppStdlib::LibCpp),
"msvc" | "msstl" | "ms" => Some(padlock_source::CppStdlib::Msvc),
other => {
eprintln!(
"padlock: warning: unknown --stdlib '{other}', \
expected libstdc++, libc++, or msvc — using libstdc++ default"
);
None
}
}
}