ge_man_lib/
archive.rs

1//! Operations for GloriousEgroll's Proton and Wine release archives.
2//!
3//! This module defines operations that work with compressed archives from GloriousEgroll's Proton and Wine
4//! releases.
5use std::io;
6use std::io::Read;
7use std::path::{Path, PathBuf};
8
9use data_encoding::HEXLOWER;
10use flate2::read::GzDecoder;
11use tar::Archive;
12use xz2::read::XzDecoder;
13
14use crate::tag::TagKind;
15
16/// Compares a given checksum to the checksum generated from the provided compressed archive file.
17///
18/// Generates a checksum from the provided `compressed_archive` file and compares it to the `expected_sum`. This method
19/// attempts to split the expected sum by whitespaces before comparing it with the generated checksum from
20/// `compressed_archive`. This is done because GE releases provide checksums with the sha512sum tool which also outputs
21/// the file name additionally to the generated sum.
22///
23/// # Examples
24///
25/// Comparing checksums where `expected_sum` contains no file name.
26///
27/// ```ignore
28/// let file = std::fs::read("filename.txt").unwrap();
29/// let expected = "<checksum>";
30/// let is_matching = archive::checksums_match(file, expected);
31/// ```
32///
33/// Comparing checksums where `expected_sum` contains a file name.
34/// ```ignore
35/// let file = std::fs::read("filename.txt").unwrap();
36/// let expected = "<checksum> filename.txt";
37/// let is_matching = archive::checksums_match(file, expected);
38/// ```
39pub fn checksums_match(compressed_tar: &[u8], expected_sum: &[u8]) -> bool {
40    let expected_sum = String::from_utf8_lossy(expected_sum)
41        .split_whitespace()
42        .next()
43        .map(String::from)
44        .unwrap_or(String::new());
45
46    let digest = ring::digest::digest(&ring::digest::SHA512, compressed_tar);
47    let sum = HEXLOWER.encode(digest.as_ref());
48
49    expected_sum.eq(&sum)
50}
51
52/// Extracts a compressed archive for a tag kind into the given `extract_destination` and returns a `PathBuf` to the
53/// extracted location.
54///
55/// This method first decompresses `compressed_tar` with `flate2` or `xz2` and then extracts it with `tar`. The
56/// decompression algorithm to use is decided by the provided `kind`. If `kind` is a `TagKind::Proton`, gzip
57/// decompression is used. If `kind` is a `TagKind::Wine {..}` xz decompression is used.
58///
59/// The difference in decompression is due to the fact that Proton GE releases use gzip compression and Wine GE
60/// releases use xz compressions.
61///
62/// # Examples
63///
64/// Extracting a Proton GE release (Proton GE releases use GZIP for compression).
65///
66/// ```ignore
67/// # use gcm_lib::tag::TagKind;
68///
69/// let archive = std::fs::read("archive.tar.gz").unwrap();
70/// let kind = TagKind::Proton;
71/// let destination = PathBuf::from("/path/to/destination");
72/// let extracted_location = archive::extract_tar(&kind, archive, destination);
73/// ```
74///
75/// Extracting a Wine GE release (Wine GE releases use XZ for compression).
76///
77/// ```ignore
78/// # use gcm_lib::tag::TagKind;
79///
80/// let archive = std::fs::read("archive.tar.xz").unwrap();
81/// let kind = TagKind::wine();
82/// let destination = PathBuf::from("/path/to/destination");
83/// let extracted_location = archive::extract_tar(&kind, archive, destination);
84/// ```
85///
86/// # Errors
87///
88/// This method returns a `std::io::Error` when:
89/// * any standard library IO error is encountered
90/// * the `flat2` crate returns an error during decompression
91/// * the `xz2` crate returns an error during decompression
92/// * the `tar` crate returns an error during extraction
93pub fn extract_compressed(
94    kind: &TagKind,
95    compressed_tar: impl Read,
96    extract_destination: &Path,
97) -> Result<PathBuf, io::Error> {
98    let extracted_dst = match kind {
99        TagKind::Proton => {
100            let decoder = GzDecoder::new(compressed_tar);
101            extract_tar(decoder, extract_destination)?
102        }
103        TagKind::Wine { .. } => {
104            let decoder = XzDecoder::new(compressed_tar);
105            extract_tar(decoder, extract_destination)?
106        }
107    };
108
109    Ok(extracted_dst)
110}
111
112fn extract_tar(decoder: impl Read, extract_destination: &Path) -> Result<PathBuf, std::io::Error> {
113    let mut archive = Archive::new(decoder);
114
115    let mut iter = archive.entries()?;
116    let first_entry = &mut iter.next().unwrap()?;
117
118    let dir_name = first_entry.path().unwrap().into_owned();
119
120    first_entry.unpack_in(extract_destination)?;
121    for entry in iter {
122        let mut entry = entry?;
123        entry.unpack_in(extract_destination)?;
124    }
125
126    Ok(extract_destination.join(dir_name))
127}
128
129#[cfg(test)]
130mod checksum_tests {
131    use std::fs;
132
133    use super::*;
134
135    #[test]
136    fn check_if_equal_checksums_where_expected_sum_contains_file_name_match() {
137        let tar = fs::read("test_resources/assets/test.tar.gz").unwrap();
138        let checksum =
139            "f2ad7b96bb24ae5fa71398127927b22c8c11eba2d3578df5a47e6ad5b5a06b0c4c66d25cf53bed0d9ed0864b76aea73794cc4be7f01249f43b796f70d068f972  test.tar.gz";
140
141        let is_equal = checksums_match(&tar, checksum.as_bytes());
142        assert!(is_equal);
143    }
144
145    #[test]
146    fn check_if_equal_checksums_match() {
147        let tar = fs::read("test_resources/assets/test.tar.gz").unwrap();
148        let checksum =
149            "f2ad7b96bb24ae5fa71398127927b22c8c11eba2d3578df5a47e6ad5b5a06b0c4c66d25cf53bed0d9ed0864b76aea73794cc4be7f01249f43b796f70d068f972";
150
151        let is_equal = checksums_match(&tar, checksum.as_bytes());
152        assert!(is_equal);
153    }
154
155    #[test]
156    fn check_if_not_equal_checksums_do_not_match() {
157        let tar = fs::read("test_resources/assets/test.tar.gz").unwrap();
158        let checksum = "unreal-checksum";
159
160        let is_equal = checksums_match(&tar, checksum.as_bytes());
161        assert!(!is_equal);
162    }
163}
164
165#[cfg(test)]
166mod extraction_tests {
167    use std::fs::File;
168    use std::io;
169
170    use assert_fs::assert::PathAssert;
171    use assert_fs::fixture::PathChild;
172    use assert_fs::TempDir;
173    use test_case::test_case;
174
175    use super::*;
176
177    #[test]
178    fn extract_proton_ge_release_with_correct_tag_kind() {
179        let tmp_dir = TempDir::new().unwrap();
180
181        let archive = File::open("test_resources/assets/test.tar.gz").unwrap();
182        let kind = TagKind::Proton;
183
184        let dst = super::extract_compressed(&kind, archive, tmp_dir.path()).unwrap();
185
186        assert_eq!(dst, tmp_dir.join("test"));
187        tmp_dir
188            .child(&dst)
189            .assert(predicates::path::exists())
190            .child(dst.join("hello-world.txt"))
191            .assert(predicates::path::exists());
192        tmp_dir
193            .child(&dst)
194            .child(dst.join("nested"))
195            .assert(predicates::path::exists())
196            .child(dst.join("nested/nested.txt"));
197        tmp_dir
198            .child(&dst)
199            .child(dst.join("other-file.txt"))
200            .assert(predicates::path::exists());
201
202        tmp_dir.close().unwrap();
203    }
204
205    #[test]
206    fn extract_proton_ge_release_with_wrong_tag_kind() {
207        let tmp_dir = TempDir::new().unwrap();
208
209        let archive = File::open("test_resources/assets/test.tar.gz").unwrap();
210        let kind = TagKind::wine();
211
212        let result = extract_compressed(&kind, archive, tmp_dir.path());
213        assert!(result.is_err());
214
215        let err = result.unwrap_err();
216        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
217
218        assert!(tmp_dir.iter().size_hint().eq(&(0, None)));
219        tmp_dir.close().unwrap();
220    }
221
222    #[test_case(TagKind::wine(); "Running with Wine kind")]
223    #[test_case(TagKind::lol(); "Running with LoL kind")]
224    fn extract_wine_ge_release_with_correct_tag_kind(kind: TagKind) {
225        let tmp_dir = TempDir::new().unwrap();
226
227        let archive = File::open("test_resources/assets/test.tar.xz").unwrap();
228
229        let dst = super::extract_compressed(&kind, archive, tmp_dir.path()).unwrap();
230
231        assert_eq!(dst, tmp_dir.join("test"));
232        tmp_dir
233            .child(&dst)
234            .assert(predicates::path::exists())
235            .child(dst.join("hello-world.txt"))
236            .assert(predicates::path::exists());
237        tmp_dir
238            .child(&dst)
239            .child(dst.join("nested"))
240            .assert(predicates::path::exists())
241            .child(dst.join("nested/nested.txt"));
242        tmp_dir
243            .child(&dst)
244            .child(dst.join("other-file.txt"))
245            .assert(predicates::path::exists());
246
247        tmp_dir.close().unwrap();
248    }
249
250    #[test]
251    fn extract_wine_ge_release_with_wrong_tag_kind() {
252        let tmp_dir = TempDir::new().unwrap();
253        let kind = TagKind::Proton;
254
255        let archive = File::open("test_resources/assets/test.tar.xz").unwrap();
256        let result = super::extract_compressed(&kind, archive, tmp_dir.path());
257        assert!(result.is_err());
258
259        let err = result.unwrap_err();
260        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
261
262        assert!(tmp_dir.iter().size_hint().eq(&(0, None)));
263        tmp_dir.close().unwrap();
264    }
265}