cargo_about/
lib.rs

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            // Users can't control the expression of external crates, some of which
15            // might use invalid identifiers, mostly the GNU licenses due to how annoying
16            // and divergent they are
17            allow_deprecated: true,
18            // Again, some crates might use completely invalid license names
19            allow_imprecise_license_names: true,
20            // We auto correct this extremely common error on crate load
21            allow_slash_as_or_operator: false,
22            // Invalid, but again have to handle invalid cases
23            allow_postfix_plus_on_gpl: true,
24            // There is no reason to allow unknown identifiers, the user has the
25            // LicenseRef- and AdditionRef- options available to them
26            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            //. Reasons this can fail:
37            // * Empty! The rust crate used to validate this field has a bug
38            // https://github.com/rust-lang-nursery/license-exprs/issues/23
39            // * It also just does basic lexing, so parens, duplicate operators,
40            // unpaired exceptions etc can all fail validation
41
42            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        // Fix the license field as cargo used to allow the
85        // invalid / separator
86        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    // The metadata command builder is weird and only allows you to specify
136    // one of these, but really you might need to do multiple of them
137    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    // Ignore faulty CRLF style newlines
217    // for line in buffer.split('\r') {
218    //     ctx.update(line.as_bytes());
219    // }
220
221    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        // Making the assumption that no one on MacOS or any of the *BSDs uses powershell...
258        return false;
259    }
260
261    // SAFETY: no invariants to uphold
262    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        // The API for this is extremely irritating, the struct and string buffer
331        // need to be the same :/
332        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}