cargo_mobile2/util/
ln.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display},
4    fs::remove_dir_all,
5    path::{Path, PathBuf},
6};
7
8use crate::DuctExpressionExt;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum LinkType {
12    Hard,
13    Symbolic,
14}
15
16impl Display for LinkType {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Hard => write!(f, "hard"),
20            Self::Symbolic => write!(f, "symbolic"),
21        }
22    }
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum Clobber {
27    Never,
28    FileOnly,
29    FileOrDirectory,
30}
31
32impl Display for Clobber {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Never => write!(f, "clobbering disabled"),
36            Self::FileOnly => write!(f, "file clobbering enabled"),
37            Self::FileOrDirectory => write!(f, "file and directory clobbering enabled"),
38        }
39    }
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum TargetStyle {
44    File,
45    Directory,
46}
47
48impl Display for TargetStyle {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::File => write!(f, "file"),
52            Self::Directory => write!(f, "directory"),
53        }
54    }
55}
56
57#[derive(Debug)]
58pub enum ErrorCause {
59    MissingFileName,
60    CommandFailed(std::io::Error),
61    IOError(std::io::Error),
62    SymlinkNotAllowed,
63}
64
65impl Display for ErrorCause {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Self::MissingFileName => {
69                write!(f, "Neither the source nor target contained a file name.",)
70            }
71            Self::CommandFailed(err) => write!(f, "`ln` command failed: {}", err),
72            Self::IOError(err) => write!(f, "IO error: {}", err),
73            Self::SymlinkNotAllowed => {
74                write!(
75                    f,
76                    r"
77Creation symbolic link is not allowed for this system.
78
79For Windows 10 or newer:
80You should use developer mode.
81See https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
82
83For Window 8.1 or older:
84You need `SeCreateSymbolicLinkPrivilege` security policy.
85See https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links"
86                )
87            }
88        }
89    }
90}
91
92#[derive(Debug)]
93pub struct Error {
94    link_type: LinkType,
95    force: Clobber,
96    source: PathBuf,
97    target: PathBuf,
98    target_style: TargetStyle,
99    cause: ErrorCause,
100}
101
102impl Error {
103    pub fn new(
104        link_type: LinkType,
105        force: Clobber,
106        source: PathBuf,
107        target: PathBuf,
108        target_style: TargetStyle,
109        cause: ErrorCause,
110    ) -> Self {
111        Self {
112            link_type,
113            force,
114            source,
115            target,
116            target_style,
117            cause,
118        }
119    }
120}
121
122impl Display for Error {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(
125            f,
126            "Failed to create a {} link from {:?} to {} {:?} ({}): {}",
127            self.link_type, self.source, self.target_style, self.target, self.force, self.cause
128        )
129    }
130}
131
132impl std::error::Error for Error {}
133
134#[derive(Clone, Debug)]
135pub struct Call<'a> {
136    link_type: LinkType,
137    force: Clobber,
138    source: &'a Path,
139    target: &'a Path,
140    target_override: Cow<'a, Path>,
141    target_style: TargetStyle,
142}
143
144impl<'a> Call<'a> {
145    pub fn new(
146        link_type: LinkType,
147        force: Clobber,
148        source: &'a Path,
149        target: &'a Path,
150        target_style: TargetStyle,
151    ) -> Result<Self, Error> {
152        let target_override = if let TargetStyle::Directory = target_style {
153            // If the target is a directory, then the link name has to come from
154            // the last component of the source.
155            if let Some(file_name) = source.file_name() {
156                Cow::Owned(target.join(file_name))
157            } else {
158                return Err(Error {
159                    link_type,
160                    force,
161                    source: source.to_owned(),
162                    target: target.to_owned(),
163                    target_style,
164                    cause: ErrorCause::MissingFileName,
165                });
166            }
167        } else {
168            Cow::Borrowed(target)
169        };
170        Ok(Self {
171            link_type,
172            force,
173            source,
174            target,
175            target_override,
176            target_style,
177        })
178    }
179
180    pub fn exec(self) -> Result<(), Error> {
181        let mut args = vec!["-n" /* don't follow symlinks */];
182        if let LinkType::Symbolic = self.link_type {
183            args.push("-s");
184        }
185        match self.force {
186            Clobber::FileOnly => {
187                args.push("-f");
188            }
189            Clobber::FileOrDirectory => {
190                if self.target_override.is_dir() {
191                    remove_dir_all(self.target)
192                        .map_err(|err| self.make_error(ErrorCause::IOError(err)))?;
193                }
194                args.push("-f");
195            }
196            _ => (),
197        }
198        let source = self.source.to_string_lossy();
199        let target_override = self.target_override.as_ref().to_string_lossy();
200        args.push(&source);
201        args.push(&target_override);
202        duct::cmd("ln", args)
203            .dup_stdio()
204            .run()
205            .map_err(|err| self.make_error(ErrorCause::CommandFailed(err)))?;
206        Ok(())
207    }
208
209    fn make_error(&self, cause: ErrorCause) -> Error {
210        Error {
211            link_type: self.link_type,
212            force: self.force,
213            source: self.source.to_owned(),
214            target: self.target.to_owned(),
215            target_style: self.target_style,
216            cause,
217        }
218    }
219}
220
221pub fn force_symlink(
222    source: impl AsRef<Path>,
223    target: impl AsRef<Path>,
224    target_style: TargetStyle,
225) -> Result<(), Error> {
226    Call::new(
227        LinkType::Symbolic,
228        Clobber::FileOrDirectory,
229        source.as_ref(),
230        target.as_ref(),
231        target_style,
232    )?
233    .exec()
234}
235
236pub fn force_symlink_relative(
237    abs_source: impl AsRef<Path>,
238    abs_target: impl AsRef<Path>,
239    target_style: TargetStyle,
240) -> Result<(), Error> {
241    let (abs_source, abs_target) = (abs_source.as_ref(), abs_target.as_ref());
242    let rel_source = super::relativize_path(abs_source, abs_target);
243    if target_style == TargetStyle::Directory && rel_source.file_name().is_none() {
244        if let Some(file_name) = abs_source.file_name() {
245            force_symlink(rel_source, abs_target.join(file_name), TargetStyle::File)
246        } else {
247            Err(Error {
248                link_type: LinkType::Symbolic,
249                force: Clobber::FileOrDirectory,
250                source: rel_source,
251                target: abs_target.to_owned(),
252                target_style,
253                cause: ErrorCause::MissingFileName,
254            })
255        }
256    } else {
257        force_symlink(rel_source, abs_target, target_style)
258    }
259}