1#![doc = include_str!("../README.md")]
2
3use krates::cm;
4use std::{cmp, fmt};
5
6pub mod generate;
7pub mod licenses;
8
9#[inline]
10pub fn parse_license_expression(license: &str) -> Result<spdx::Expression, spdx::ParseError> {
11 spdx::Expression::parse_mode(
12 license,
13 spdx::ParseMode {
14 allow_deprecated: true,
18 allow_imprecise_license_names: true,
20 allow_slash_as_or_operator: false,
22 allow_postfix_plus_on_gpl: true,
24 allow_unknown: false,
27 },
28 )
29}
30
31pub struct Krate(pub cm::Package);
32
33impl Krate {
34 fn get_license_expression(&self) -> licenses::LicenseInfo {
35 if let Some(license_field) = &self.0.license {
36 match parse_license_expression(license_field) {
43 Ok(validated) => licenses::LicenseInfo::Expr(validated),
44 Err(err) => {
45 log::error!("unable to parse license expression for '{self}': {err}");
46 licenses::LicenseInfo::Unknown
47 }
48 }
49 } else {
50 log::warn!("crate '{self}' doesn't have a license field");
51 licenses::LicenseInfo::Unknown
52 }
53 }
54}
55
56impl Ord for Krate {
57 #[inline]
58 fn cmp(&self, o: &Self) -> cmp::Ordering {
59 match self.0.name.cmp(&o.0.name) {
60 cmp::Ordering::Equal => self.0.version.cmp(&o.0.version),
61 o => o,
62 }
63 }
64}
65
66impl PartialOrd for Krate {
67 #[inline]
68 fn partial_cmp(&self, o: &Self) -> Option<cmp::Ordering> {
69 Some(self.cmp(o))
70 }
71}
72
73impl PartialEq for Krate {
74 #[inline]
75 fn eq(&self, o: &Self) -> bool {
76 self.cmp(o) == cmp::Ordering::Equal
77 }
78}
79
80impl Eq for Krate {}
81
82impl From<cm::Package> for Krate {
83 fn from(mut pkg: cm::Package) -> Self {
84 if let Some(lf) = &mut pkg.license {
87 *lf = lf.replace('/', " OR ");
88 }
89
90 Self(pkg)
91 }
92}
93
94impl krates::KrateDetails for Krate {
95 fn name(&self) -> &str {
96 &self.0.name
97 }
98
99 fn version(&self) -> &krates::semver::Version {
100 &self.0.version
101 }
102}
103
104impl fmt::Display for Krate {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "{} {}", self.0.name, self.0.version)
107 }
108}
109
110impl std::ops::Deref for Krate {
111 type Target = cm::Package;
112
113 fn deref(&self) -> &Self::Target {
114 &self.0
115 }
116}
117
118pub type Krates = krates::Krates<Krate>;
119
120#[allow(clippy::too_many_arguments)]
121pub fn get_all_crates(
122 cargo_toml: &krates::Utf8Path,
123 no_default_features: bool,
124 all_features: bool,
125 features: Vec<String>,
126 workspace: bool,
127 lock_opts: krates::LockOptions,
128 cfg: &licenses::config::Config,
129 target_overrdes: &[String],
130) -> anyhow::Result<Krates> {
131 let mut mdc = krates::Cmd::new();
132 mdc.manifest_path(cargo_toml);
133 mdc.lock_opts(lock_opts);
134
135 if no_default_features {
138 mdc.no_default_features();
139 }
140
141 if all_features {
142 mdc.all_features();
143 }
144
145 mdc.features(features);
146
147 let mut builder = krates::Builder::new();
148
149 if workspace {
150 builder.workspace(true);
151 }
152
153 if cfg.ignore_build_dependencies {
154 builder.ignore_kind(krates::DepKind::Build, krates::Scope::All);
155 }
156
157 if cfg.ignore_dev_dependencies {
158 builder.ignore_kind(krates::DepKind::Dev, krates::Scope::All);
159 }
160
161 if cfg.ignore_transitive_dependencies {
162 builder.ignore_kind(krates::DepKind::Normal, krates::Scope::NonWorkspace);
163 builder.ignore_kind(krates::DepKind::Dev, krates::Scope::NonWorkspace);
164 builder.ignore_kind(krates::DepKind::Build, krates::Scope::NonWorkspace);
165 }
166
167 if target_overrdes.is_empty() {
168 builder.include_targets(cfg.targets.iter().map(|triple| (triple.as_str(), vec![])));
169 } else {
170 builder.include_targets(
171 target_overrdes
172 .iter()
173 .map(|triple| (triple.as_str(), vec![])),
174 );
175 }
176
177 let graph = builder.build(mdc, |filtered: cm::Package| {
178 if let Some(src) = filtered.source {
179 if src.is_crates_io() {
180 log::debug!("filtered {} {}", filtered.name, filtered.version);
181 } else {
182 log::debug!("filtered {} {} {}", filtered.name, filtered.version, src);
183 }
184 } else {
185 log::debug!("filtered crate {} {}", filtered.name, filtered.version);
186 }
187 })?;
188
189 Ok(graph)
190}
191
192#[inline]
193pub fn to_hex(bytes: &[u8]) -> String {
194 let mut s = String::with_capacity(bytes.len() * 2);
195 const CHARS: &[u8] = b"0123456789abcdef";
196
197 for &byte in bytes {
198 s.push(CHARS[(byte >> 4) as usize] as char);
199 s.push(CHARS[(byte & 0xf) as usize] as char);
200 }
201
202 s
203}
204
205pub fn validate_sha256(buffer: &str, expected: &str) -> anyhow::Result<()> {
206 anyhow::ensure!(
207 expected.len() == 64,
208 "checksum '{expected}' length is {} instead of expected 64",
209 expected.len()
210 );
211
212 let mut ctx = ring::digest::Context::new(&ring::digest::SHA256);
213
214 ctx.update(buffer.as_bytes());
215
216 let content_digest = ctx.finish();
222 let digest = content_digest.as_ref();
223
224 for (ind, exp) in expected.as_bytes().chunks(2).enumerate() {
225 let mut cur = match exp[0] {
226 b'A'..=b'F' => exp[0] - b'A' + 10,
227 b'a'..=b'f' => exp[0] - b'a' + 10,
228 b'0'..=b'9' => exp[0] - b'0',
229 c => {
230 anyhow::bail!("invalid byte in checksum '{expected}' @ {ind}: {c}");
231 }
232 };
233
234 cur <<= 4;
235
236 cur |= match exp[1] {
237 b'A'..=b'F' => exp[1] - b'A' + 10,
238 b'a'..=b'f' => exp[1] - b'a' + 10,
239 b'0'..=b'9' => exp[1] - b'0',
240 c => {
241 anyhow::bail!("invalid byte in checksum '{expected}' @ {ind}: {c}");
242 }
243 };
244
245 if digest[ind] != cur {
246 anyhow::bail!("checksum mismatch, expected '{expected}'");
247 }
248 }
249
250 Ok(())
251}
252
253#[cfg(target_family = "unix")]
254#[allow(unsafe_code)]
255pub fn is_powershell_parent() -> bool {
256 if !cfg!(target_os = "linux") {
257 return false;
259 }
260
261 let mut parent_id = Some(unsafe { libc::getppid() });
263
264 while let Some(ppid) = parent_id {
265 let Ok(cmd) = std::fs::read_to_string(format!("/proc/{ppid}/cmdline")) else {
266 break;
267 };
268
269 let Some(proc) = cmd
270 .split('\0')
271 .next()
272 .and_then(|path| path.split('/').next_back())
273 else {
274 break;
275 };
276
277 if proc == "pwsh" {
278 return true;
279 }
280
281 let Ok(status) = std::fs::read_to_string(format!("/proc/{ppid}/status")) else {
282 break;
283 };
284
285 for line in status.lines() {
286 let Some(ppid) = line.strip_prefix("PPid:\t") else {
287 continue;
288 };
289
290 parent_id = ppid.parse().ok();
291 break;
292 }
293 }
294
295 false
296}
297
298#[cfg(target_family = "windows")]
299mod win_bindings;
300
301#[cfg(target_family = "windows")]
302#[allow(unsafe_code)]
303pub fn is_powershell_parent() -> bool {
304 use std::os::windows::ffi::OsStringExt as _;
305 use win_bindings::*;
306
307 struct NtHandle {
308 handle: isize,
309 }
310
311 impl Drop for NtHandle {
312 fn drop(&mut self) {
313 if self.handle != -1 {
314 unsafe {
315 nt_close(self.handle);
316 }
317 }
318 }
319 }
320
321 let mut handle = Some(NtHandle { handle: -1 });
322
323 unsafe {
324 let reset = |fname: &mut [u16]| {
325 let ustr = &mut *fname.as_mut_ptr().cast::<UnicodeString>();
326 ustr.length = 0;
327 ustr.maximum_length = MaxPath as _;
328 };
329
330 let mut file_name = [0u16; MaxPath as usize + std::mem::size_of::<UnicodeString>() / 2];
333
334 while let Some(ph) = handle {
335 let mut basic_info = std::mem::MaybeUninit::<ProcessBasicInformation>::uninit();
336 let mut length = 0;
337 if nt_query_information_process(
338 ph.handle,
339 Processinfoclass::ProcessBasicInformation,
340 basic_info.as_mut_ptr().cast(),
341 std::mem::size_of::<ProcessBasicInformation>() as _,
342 &mut length,
343 ) != StatusSuccess
344 {
345 break;
346 }
347
348 if length != std::mem::size_of::<ProcessBasicInformation>() as u32 {
349 break;
350 }
351
352 let basic_info = basic_info.assume_init();
353 reset(&mut file_name);
354
355 let ppid = basic_info.inherited_from_unique_process_id as isize;
356
357 if ppid == 0 || ppid == -1 {
358 break;
359 }
360
361 let mut parent_handle = -1;
362 let obj_attr = std::mem::zeroed();
363 let client_id = ClientId {
364 unique_process: ppid,
365 unique_thread: 0,
366 };
367 if nt_open_process(
368 &mut parent_handle,
369 ProcessAccessRights::ProcessQueryInformation,
370 &obj_attr,
371 &client_id,
372 ) != StatusSuccess
373 {
374 break;
375 }
376
377 handle = Some(NtHandle {
378 handle: parent_handle,
379 });
380
381 if nt_query_information_process(
382 parent_handle,
383 Processinfoclass::ProcessImageFileName,
384 file_name.as_mut_ptr().cast(),
385 (file_name.len() * 2) as _,
386 &mut length,
387 ) != StatusSuccess
388 {
389 break;
390 }
391
392 let ustr = &*file_name.as_ptr().cast::<UnicodeString>();
393 let os = std::ffi::OsString::from_wide(std::slice::from_raw_parts(
394 ustr.buffer,
395 (ustr.length >> 1) as usize,
396 ));
397
398 let path = std::path::Path::new(&os);
399 if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) {
400 if stem == "pwsh" || stem == "powershell" {
401 return true;
402 }
403 }
404 }
405
406 false
407 }
408}
409
410#[cfg(test)]
411mod test {
412 #[test]
413 #[ignore = "call when actually run from powershell"]
414 fn is_powershell_true() {
415 assert!(super::is_powershell_parent());
416 }
417
418 #[test]
419 #[ignore = "call when not actually run from powershell"]
420 fn is_powershell_false() {
421 assert!(!super::is_powershell_parent());
422 }
423}