File: install-mimic.rs

package info (click to toggle)
install-mimic 0.4.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 256 kB
  • sloc: perl: 385; ansic: 200; makefile: 109; sh: 51
file content (160 lines) | stat: -rw-r--r-- 4,594 bytes parent folder | download
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
/*-
 * SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
 * SPDX-License-Identifier: BSD-2-Clause
 */

use std::env;
use std::fs;
use std::io::ErrorKind;
use std::os::unix::fs::MetadataExt as _;
use std::path::Path;
use std::process::Command;

use anyhow::{bail, Context as _, Result};
use clap::Parser as _;
use clap_derive::Parser;

#[derive(Debug, Parser)]
#[clap(version)]
struct Cli {
    /// Display the features supported by the program.
    #[clap(long)]
    features: bool,

    /// Specify a reference file to obtain the information from.
    #[clap(short)]
    reffile: Option<String>,

    /// Verbose operation; display diagnostic output.
    #[clap(short, long)]
    verbose: bool,

    filenames: Vec<String>,
}

const VERSION_STR: &str = env!("CARGO_PKG_VERSION");

struct Config {
    filenames: Vec<String>,
    destination: String,
    refname: Option<String>,
    verbose: bool,
}

enum Mode {
    Handled,
    Install(Config),
}

#[expect(clippy::print_stdout, reason = "This is the purpose of this function")]
fn features() {
    println!("Features: install-mimic={VERSION_STR}");
}

fn install_mimic<SP: AsRef<Path>, DP: AsRef<Path>>(
    src: SP,
    dst: DP,
    refname: Option<&str>,
    verbose: bool,
) -> Result<()> {
    let src_path = src.as_ref().to_str().with_context(|| {
        format!(
            "Could not build a source path from {src}",
            src = src.as_ref().display()
        )
    })?;
    let dst_path = dst.as_ref().to_str().with_context(|| {
        format!(
            "Could not build a destination path from {dst}",
            dst = dst.as_ref().display()
        )
    })?;
    let filetoref = refname.map_or_else(|| dst_path.to_owned(), ToOwned::to_owned);
    let stat =
        fs::metadata(&filetoref).with_context(|| format!("Could not examine {filetoref}"))?;
    let user_id = stat.uid().to_string();
    let group_id = stat.gid().to_string();
    let mode = format!("{mode:o}", mode = stat.mode() & 0o7777);
    let prog_name = "install";
    let args = [
        "-c", "-o", &user_id, "-g", &group_id, "-m", &mode, "--", src_path, dst_path,
    ];
    let mut cmd = Command::new(prog_name);
    cmd.args(args);
    #[expect(clippy::print_stdout, reason = "This is the purpose of this function")]
    if verbose {
        println!("{prog_name} {args}", args = shell_words::join(args));
    }
    if !cmd.status().context("Could not run install")?.success() {
        bail!("Could not install {src_path} as {dst_path}");
    }
    Ok(())
}

fn parse_args() -> Result<Mode> {
    let opts = Cli::parse();
    if opts.features {
        features();
        return Ok(Mode::Handled);
    }

    let mut filenames = opts.filenames;
    let destination = filenames
        .pop()
        .context("No source or destination paths specified")?;
    if filenames.is_empty() {
        bail!("At least one source and one destination path must be specified");
    }
    Ok(Mode::Install(Config {
        filenames,
        destination,
        refname: opts.reffile,
        verbose: opts.verbose,
    }))
}

fn doit(cfg: &Config) -> Result<()> {
    let is_dir = match fs::metadata(&cfg.destination) {
        Err(err) if err.kind() == ErrorKind::NotFound => {
            if cfg.refname.is_none() {
                bail!(
                    "The destination path {dst} does not exist and no -r specified",
                    dst = cfg.destination
                );
            }
            false
        }
        Err(err) => {
            bail!("Could not examine {dst}: {err}", dst = cfg.destination);
        }
        Ok(data) => data.is_dir(),
    };
    if is_dir {
        let dstpath: &Path = cfg.destination.as_ref();
        for path in &cfg.filenames {
            let pathref: &Path = path.as_ref();
            let basename = pathref
                .file_name()
                .with_context(|| format!("Invalid source filename {path}"))?;
            install_mimic(
                path,
                dstpath.join(basename),
                cfg.refname.as_deref(),
                cfg.verbose,
            )?;
        }
        Ok(())
    } else {
        match *cfg.filenames {
            [ref source] => install_mimic(source, &cfg.destination, cfg.refname.as_deref(), cfg.verbose),
            _ => bail!("The destination path must be a directory if more than one source path is specified"),
        }
    }
}

fn main() -> Result<()> {
    match parse_args().context("Could not parse the command-line arguments")? {
        Mode::Handled => Ok(()),
        Mode::Install(cfg) => doit(&cfg),
    }
}