java_classpaths/
lib.rs

1//! Allows for file system like access to java like classpaths
2//!
3
4use std::{io, vec};
5use std::collections::{vec_deque, VecDeque};
6use std::convert::Infallible;
7use std::ffi::{OsStr, OsString};
8use std::fmt::{Display, Formatter, Write};
9use std::fs::File;
10use std::io::{ErrorKind, Read};
11use std::ops::{Add, AddAssign};
12use std::path::{Path, PathBuf};
13use std::str::FromStr;
14
15use cfg_if::cfg_if;
16use static_assertions::assert_impl_all;
17use url::Url;
18use zip::result::ZipError;
19use zip::ZipArchive;
20
21cfg_if! {
22    if #[cfg(windows)] {
23        /// The separator between different entries on the classpath. This is different depending on the os.
24        /// In general, the separator on unix is `:`, while on windows it's `;`
25        pub const CLASSPATH_SEPARATOR: char = ';';
26    } else if #[cfg(unix)] {
27        /// The separator between different entries on the classpath. This is different depending on the os.
28        /// In general, the separator on unix is `:`, while on windows it's `;`
29        pub const CLASSPATH_SEPARATOR: char = ':';
30    }
31}
32
33/// A classpath in java
34#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
35pub struct Classpath {
36    paths: VecDeque<PathBuf>,
37}
38
39impl Classpath {
40    /// Creates an empty classpath
41    pub fn new() -> Self {
42        Default::default()
43    }
44
45    /// Checks if the given classpath is empty
46    pub fn is_empty(&self) -> bool {
47        self.paths.is_empty()
48    }
49
50    /// Gets the number of entries in the classpath
51    pub fn len(&self) -> usize {
52        self.paths.len()
53    }
54
55    /// Converts this classpath into a usable classpath for java
56    ///
57    /// # Example
58    /// ```
59    /// # use std::ffi::OsString;
60    /// # use java_classpaths::{Classpath, CLASSPATH_SEPARATOR};
61    /// let cp = Classpath::from_iter(["file1", "file2", "file3"]);
62    /// assert_eq!(cp.as_os_string(), OsString::from(format!("file1{0}file2{0}file3", CLASSPATH_SEPARATOR)));
63    /// ```
64    pub fn as_os_string(&self) -> OsString {
65        self.paths.iter().fold(OsString::new(), |mut accum, path| {
66            if accum.is_empty() {
67                path.clone().into_os_string()
68            } else {
69                accum
70                    .write_char(CLASSPATH_SEPARATOR)
71                    .expect("couldn't add separator");
72                accum.push(path);
73                accum
74            }
75        })
76    }
77
78    /// Attempts to get a resource on the classpath.
79    ///
80    /// Paths will be interpreted with only `/` as a separator. A leading `/` is ignored.
81    ///
82    /// # Return
83    /// Will return `None` is path is not on classpath. Otherwise, `Some(Result)` is returned
84    /// where the resource exists. The result is `Ok` if the inner is actually readable.
85    ///
86    /// # Example
87    /// ```no_run
88    /// # use std::str::FromStr;
89    /// # use java_classpaths::Classpath;
90    /// let cp = Classpath::from_str("run.jar");
91    /// let resource = cp.get("META-INF/MANIFEST").expect("manifest not found");
92    /// ```
93    pub fn get<P: AsRef<str>>(&self, path: P) -> Option<io::Result<Resource>> {
94        let stripped = path.as_ref().trim_start_matches("/");
95        for entry in self {
96            if entry.is_dir() {
97                if let Some(ret) = Self::get_in_dir(entry, stripped) {
98                    return Some(ret);
99                }
100            } else {
101                let ext = entry.extension();
102                match ext.and_then(|os| os.to_str()) {
103                    Some("jar") | Some("zip") => {
104                        match Self::get_in_archive(
105                            entry, stripped
106                        ) {
107                            Ok(Some(resource)) => {
108                                return Some(Ok(resource))
109                            }
110                            Ok(None) => { }
111                            Err(e) => {
112                                return Some(Err(e))
113                            }
114                        }
115                    }
116                    _ => { }
117                }
118            }
119        }
120
121        None
122    }
123
124    fn get_in_archive(archive_path: &Path, entry_path: &str) -> io::Result<Option<Resource>> {
125        let archive_file = File::open(archive_path)?;
126        let mut archive = ZipArchive::new(archive_file)
127            .map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string()))?;
128
129        let out = match archive.by_name(entry_path) {
130            Ok(mut entry) => {
131                let mut buffer = vec![];
132                entry.read_to_end(&mut buffer)?;
133                Ok(Some(
134                    Resource {
135                        kind: ResourceKind::ArchiveEntry(VecDeque::from(buffer)),
136                        url: Url::parse(
137                            &format!("jar:file:{archive}!{entry_path}", archive = archive_path.to_str().unwrap())
138                        ).unwrap()
139                    }
140                ))
141            }
142            Err(err) => {
143                match err {
144                    ZipError::FileNotFound => {
145                        Ok(None)
146                    }
147                    e => {
148                        Err(io::Error::new(ErrorKind::InvalidData, e))
149                    }
150                }
151            }
152        };
153        out
154    }
155
156    fn get_in_dir(dir: &Path, entry: &str) -> Option<io::Result<Resource>> {
157        let full_path = dir.join(entry);
158        if full_path.exists() {
159            Some(
160                File::open(&full_path)
161                    .and_then(|file| {
162                        Url::from_file_path(&full_path)
163                            .map_err(|()| {
164                                io::Error::new(
165                                    io::ErrorKind::NotFound,
166                                    format!("{:?} is not valid as a url", full_path),
167                                )
168                            })
169                            .map(|url| (file, url))
170                    })
171                    .map(|(file, url)| Resource {
172                        kind: ResourceKind::Real(file),
173                        url,
174                    }),
175            )
176        } else {
177            None
178        }
179    }
180}
181
182/// Classpath manipulation methods
183impl Classpath {
184    /// Pushes a new entry to this classpath, at the front.
185    pub fn push_front<P: AsRef<Path>>(&mut self, path: P) {
186        self.paths.push_front(path.as_ref().to_path_buf());
187    }
188
189    /// Pushes a new entry to this classpath, at the back.
190    pub fn push_back<P: AsRef<Path>>(&mut self, path: P) {
191        self.paths.push_back(path.as_ref().to_path_buf());
192    }
193
194    /// Joins two classpaths together, with the `self` classpath being at the front and the `other`
195    /// classpath at the back.
196    pub fn join(self, other: Self) -> Self {
197        let mut paths = self.paths;
198        paths.extend(other.paths);
199        Self { paths }
200    }
201}
202
203impl Display for Classpath {
204    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
205        write!(f, "{:?}", self.as_os_string())
206    }
207}
208
209impl<P> FromIterator<P> for Classpath
210where
211    P: AsRef<Path>,
212{
213    fn from_iter<T: IntoIterator<Item = P>>(iter: T) -> Self {
214        Self {
215            paths: iter.into_iter().map(|p| p.as_ref().to_path_buf()).collect(),
216        }
217    }
218}
219
220impl From<Vec<PathBuf>> for Classpath {
221    fn from(vec: Vec<PathBuf>) -> Self {
222        Self::from_iter(vec)
223    }
224}
225
226impl From<&Path> for Classpath {
227    /// Tries to create a classpath where the given path is a *single* entry.
228    fn from(path: &Path) -> Self {
229        let mut output = Self::new();
230        output.push_front(path);
231        output
232    }
233}
234
235impl From<PathBuf> for Classpath {
236    /// Tries to create a classpath where the given path is a *single* entry.
237    fn from(path: PathBuf) -> Self {
238        let mut output = Self::new();
239        output.push_front(path);
240        output
241    }
242}
243
244impl From<&str> for Classpath {
245    /// Tries to create a classpath where the given path is a *single* entry.
246    fn from(path: &str) -> Self {
247        let mut output = Self::new();
248        output.push_front(path);
249        output
250    }
251}
252
253impl From<&OsStr> for Classpath {
254    /// Tries to create a classpath where the given path is a *single* entry.
255    fn from(path: &OsStr) -> Self {
256        let mut output = Self::new();
257        output.push_front(path);
258        output
259    }
260}
261
262impl FromStr for Classpath {
263    type Err = Infallible;
264
265    /// Attempts to parse a classpath, with entries seperated by the Os's classpath separator
266    fn from_str(s: &str) -> Result<Self, Self::Err> {
267        Ok(Self::from_iter(s.split(CLASSPATH_SEPARATOR)))
268    }
269}
270
271impl IntoIterator for Classpath {
272    type Item = PathBuf;
273    type IntoIter = vec_deque::IntoIter<PathBuf>;
274
275    fn into_iter(self) -> Self::IntoIter {
276        self.paths.into_iter()
277    }
278}
279
280impl<'a> IntoIterator for &'a Classpath {
281    type Item = &'a Path;
282    type IntoIter = vec::IntoIter<&'a Path>;
283
284    fn into_iter(self) -> Self::IntoIter {
285        self.paths
286            .iter()
287            .map(|s| s.as_path())
288            .collect::<Vec<_>>()
289            .into_iter()
290    }
291}
292
293impl<P: AsRef<Path>> Extend<P> for Classpath {
294    fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
295        self.paths
296            .extend(iter.into_iter().map(|p| p.as_ref().to_path_buf()))
297    }
298}
299
300impl Add for Classpath {
301    type Output = Self;
302
303    fn add(self, rhs: Self) -> Self::Output {
304        self.join(rhs)
305    }
306}
307
308impl AddAssign for Classpath {
309    fn add_assign(&mut self, rhs: Self) {
310        self.extend(rhs)
311    }
312}
313
314
315
316/// A classpath resource. This is some readable entry available on the classpath
317#[derive(Debug)]
318pub struct Resource {
319    kind: ResourceKind,
320    url: Url,
321}
322
323impl Resource {
324    /// Gets the url of the resource as it would appear in java.
325    pub fn url(&self) -> &Url {
326        &self.url
327    }
328}
329
330assert_impl_all!(Resource: io::Read);
331
332impl io::Read for Resource {
333    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
334        self.kind.read(buf)
335    }
336}
337
338#[derive(Debug)]
339enum ResourceKind {
340    Real(File),
341    ArchiveEntry(VecDeque<u8>),
342}
343
344assert_impl_all!(ResourceKind: io::Read);
345
346impl io::Read for ResourceKind {
347    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
348        match self {
349            ResourceKind::Real(file) => file.read(buf),
350            ResourceKind::ArchiveEntry(old_buf) => {
351                old_buf.read(buf)
352            }
353        }
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use std::ffi::OsString;
360
361    use crate::{Classpath, CLASSPATH_SEPARATOR};
362
363    #[test]
364    fn as_path() {
365        let cp = Classpath::from_iter(["path1", "path2"]);
366        let as_path = cp.as_os_string();
367        assert_eq!(
368            as_path,
369            OsString::from(format!("path1{}path2", CLASSPATH_SEPARATOR))
370        );
371    }
372
373    #[test]
374    fn join() {
375        let cp1 = Classpath::from("path1");
376        let cp2 = Classpath::from("path2");
377        assert_eq!(cp1.join(cp2), Classpath::from_iter(["path1", "path2"]));
378    }
379
380    #[test]
381    fn add_classpaths() {
382        let mut cp = Classpath::new();
383        cp = cp + Classpath::from("path1");
384        cp += Classpath::from_iter(["path2", "path3"]);
385        assert_eq!(cp, Classpath::from_iter(["path1", "path2", "path3"]));
386    }
387
388    #[test]
389    fn from_string() {
390        let classpath: Classpath = format!("path1{}path2", CLASSPATH_SEPARATOR)
391            .parse()
392            .unwrap();
393        assert_eq!(classpath, Classpath::from_iter(["path1", "path2"]))
394    }
395}