physis/
patchlist.rs

1// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4/// Represents a patch to be downloaded.
5#[derive(Debug)]
6pub struct PatchEntry {
7    /// The URL of the patch file. Usually an HTTP URL.
8    pub url: String,
9    /// The version for this patch.
10    pub version: String,
11    /// How many bytes each SHA1 hash block is considering.
12    pub hash_block_size: i64,
13    /// Length of the patch file (in bytes.)
14    pub length: i64,
15    /// New size of the (game?) when the patch is installed.
16    pub size_on_disk: i64,
17    /// The list of SHA1 hashes.
18    pub hashes: Vec<String>,
19
20    // TODO: figure out what this is
21    pub unknown_a: i32,
22    // TODO: ditto
23    pub unknown_b: i32,
24}
25
26/// A list of patch files the client is requested to download, and install.
27#[derive(Debug)]
28pub struct PatchList {
29    /// The id of the patch list.
30    // FIXME: this is most likely auto-generated, not set?
31    pub id: String,
32    /// Size of the **ffxiv** repository patches only! In bytes.
33    pub patch_length: u64,
34    /// The content location, usually from an HTTP URL.
35    pub content_location: String,
36    /// The version that was requested from the server.
37    pub requested_version: String,
38    /// The list of patches.
39    pub patches: Vec<PatchEntry>,
40}
41
42/// The kind of patch list.
43/// This must be the kind of patch list you're parsing, or else the fields will filled in the wrong order.
44#[derive(PartialEq)]
45#[repr(C)]
46pub enum PatchListType {
47    /// A boot patch list.
48    Boot,
49    /// A game patch ist.
50    Game,
51}
52
53impl PatchList {
54    pub fn from_string(patch_type: PatchListType, encoded: &str) -> Self {
55        let mut patches = vec![];
56
57        let mut patch_length = 0;
58        if let Some(patch_length_index) = encoded.find("X-Patch-Length: ") {
59            let rest_of_string = &encoded[patch_length_index + 16..];
60            if let Some(end_of_number_index) = rest_of_string.find("\r\n") {
61                let patch_length_parse: Result<u64, _> =
62                    rest_of_string[0..end_of_number_index].parse();
63                if let Ok(p) = patch_length_parse {
64                    patch_length = p;
65                }
66            }
67        };
68
69        let mut content_location = String::default();
70        if let Some(patch_length_index) = encoded.find("Content-Location: ") {
71            let rest_of_string = &encoded[patch_length_index + 18..];
72            if let Some(end_of_number_index) = rest_of_string.find("\r\n") {
73                content_location = rest_of_string[0..end_of_number_index].to_string();
74            }
75        };
76
77        let parts: Vec<_> = encoded.split("\r\n").collect();
78        for i in 5..parts.len() - 2 {
79            let patch_parts: Vec<_> = parts[i].split('\t').collect();
80
81            if patch_type == PatchListType::Boot {
82                patches.push(PatchEntry {
83                    url: patch_parts[5].parse().unwrap(),
84                    version: patch_parts[4].parse().unwrap(),
85                    hash_block_size: 0,
86                    length: patch_parts[0].parse().unwrap(),
87                    size_on_disk: patch_parts[1].parse().unwrap(),
88                    hashes: vec![],
89                    unknown_a: 0,
90                    unknown_b: 0,
91                });
92            } else {
93                patches.push(PatchEntry {
94                    url: patch_parts[8].parse().unwrap(),
95                    version: patch_parts[4].parse().unwrap(),
96                    hash_block_size: patch_parts[6].parse().unwrap(),
97                    length: patch_parts[0].parse().unwrap(),
98                    size_on_disk: patch_parts[1].parse().unwrap(),
99                    hashes: patch_parts[7].split(',').map(|x| x.to_string()).collect(),
100                    unknown_a: patch_parts[2].parse().unwrap(),
101                    unknown_b: patch_parts[3].parse().unwrap(),
102                });
103            }
104        }
105
106        Self {
107            id: "".to_string(),
108            content_location,
109            requested_version: "".to_string(),
110            patch_length,
111            patches,
112        }
113    }
114
115    pub fn to_string(&self, patch_type: PatchListType) -> String {
116        let mut str = String::new();
117
118        // header
119        str.push_str("--");
120        str.push_str(&self.id);
121        str.push_str("\r\n");
122        str.push_str("Content-Type: application/octet-stream\r\n");
123        str.push_str(&format!("Content-Location: {}\r\n", self.content_location));
124
125        let mut total_patch_size = 0;
126        for patch in &self.patches {
127            total_patch_size += patch.length;
128        }
129
130        str.push_str(&format!("X-Patch-Length: {}\r\n", total_patch_size));
131        str.push_str("\r\n");
132
133        for patch in &self.patches {
134            // length
135            str.push_str(&patch.length.to_string());
136            str.push('\t');
137
138            // TODO: unknown value, but i *suspect* is the size of the game on disk once this patch is applied.
139            // which would make sense for the launcher to check for
140            str.push_str(&patch.size_on_disk.to_string());
141            str.push('\t');
142
143            // TODO: totally unknown
144            str.push_str(&patch.unknown_a.to_string());
145            str.push('\t');
146
147            // TODO: unknown too
148            str.push_str(&patch.unknown_b.to_string());
149            str.push('\t');
150
151            // version (e.g. 2023.09.15.0000.0000)
152            str.push_str(&patch.version);
153            str.push('\t');
154
155            if patch_type == PatchListType::Game {
156                // hash type
157                // TODO: does this need to be configurable?
158                str.push_str("sha1");
159                str.push('\t');
160
161                // hash block size
162                str.push_str(&patch.hash_block_size.to_string());
163                str.push('\t');
164
165                // hashes
166                str.push_str(&patch.hashes[0]);
167                for hash in &patch.hashes[1..] {
168                    str.push(',');
169                    str.push_str(hash);
170                }
171                str.push('\t');
172            }
173
174            // url
175            str.push_str(&patch.url);
176            str.push_str("\r\n");
177        }
178
179        str.push_str("--");
180        str.push_str(&self.id);
181        str.push_str("--\r\n");
182
183        str
184    }
185
186    /// Size of all the patch files by size (in bytes.)
187    pub fn total_size_downloaded(&self) -> i64 {
188        let mut size = 0;
189        for patch in &self.patches {
190            size += patch.length;
191        }
192
193        return size;
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_boot_patch_parsing() {
203        let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
204        ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http\r\nX-Patch-Length: \
205        22221335\r\n\r\n22221335\t69674819\t19\t18\t2023.09.14.0000.0001\thttp://patch-dl.ffxiv.com/boot/2b5cbc63/\
206        D2023.09.14.0000.0001.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";
207
208        let patch_list = PatchList::from_string(PatchListType::Boot, test_case);
209        assert_eq!(patch_list.patches.len(), 1);
210        assert_eq!(patch_list.patches[0].version, "2023.09.14.0000.0001");
211        assert_eq!(
212            patch_list.patches[0].url,
213            "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch"
214        );
215        assert_eq!(patch_list.patches[0].size_on_disk, 69674819);
216        assert_eq!(patch_list.patch_length, 22221335);
217    }
218
219    #[test]
220    fn test_game_patch_parsing() {
221        let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
222        ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http\r\nX-Patch-Length: \
223        1664916486\r\n\r\n1479062470\t44145529682\t71\t11\t2023.09.15.0000.0000\tsha1\t50000000\t1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c,\
224        950725418366c965d824228bf20f0496f81e0b9a,cabef48f7bf00fbf18b72843bdae2f61582ad264,53608de567b52f5fdb43fdb8b623156317e26704,\
225        f0bc06cabf9ff6490f36114b25f62619d594dbe8,3c5e4b962cd8445bd9ee29011ecdb331d108abd8,88e1a2a322f09de3dc28173d4130a2829950d4e0,\
226        1040667917dc99b9215dfccff0e458c2e8a724a8,149c7e20e9e3e376377a130e0526b35fd7f43df2,1bb4e33807355cdf46af93ce828b6e145a9a8795,\
227        a79daff43db488f087da8e22bb4c21fd3a390f3c,6b04fadb656d467fb8318eba1c7f5ee8f030d967,a6641e1c894db961a49b70fda2b0d6d87be487a7,\
228        edf419de49f42ef19bd6814f8184b35a25e9e977,c1525c4df6001b66b575e2891db0284dc3a16566,01b7628095b07fa3c9c1aed2d66d32d118020321,\
229        991b137ea0ebb11bd668f82149bc2392a4cbcf52,ad3f74d4fca143a6cf507fc859544a4bcd501d85,936a0f1711e273519cae6b2da0d8b435fe6aa020,\
230        023f19d8d8b3ecaaf865e3170e8243dd437a384c,2d9e934de152956961a849e81912ca8d848265ca,8e32f9aa76c95c60a9dbe0967aee5792b812d5ec,\
231        dee052b9aa1cc8863efd61afc63ac3c2d56f9acc,fa81225aea53fa13a9bae1e8e02dea07de6d7052,59b24693b1b62ea1660bc6f96a61f7d41b3f7878,\
232        349b691db1853f6c0120a8e66093c763ba6e3671,4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50,de94175c4db39a11d5334aefc7a99434eea8e4f9,\
233        55dd7215f24441d6e47d1f9b32cebdb041f2157f,2ca09db645cfeefa41a04251dfcb13587418347a\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
234        D2023.09.15.0000.0000.patch\r\n61259063\t44145955874\t71\t11\t2023.09.21.0000.0001\tsha1\t50000000\t88c9bbfe2af4eea7b56384baeeafd59afb47ddeb,\
235        095c26e87b4d25505845515c389dd22dd429ea7e\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
236        D2023.09.21.0000.0001.patch\r\n63776300\t44146911186\t71\t11\t2023.09.23.0000.0001\tsha1\t50000000\tc8fc6910be12d10b39e4e6ae980d4c219cfe56a1,\
237        5c0199b7147a47f620a2b50654a87c9b0cbcf43b\thttp://patch-dl.ffxiv.com/game/4e9a232b/
238        D2023.09.23.0000.0001.patch\r\n32384649\t44146977234\t71\t11\t2023.09.26.0000.\
239        0000\tsha1\t50000000\t519a5e46edb67ba6edb9871df5eb3991276da254\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
240        D2023.09.26.0000.0000.patch\r\n28434004\t44147154898\t71\t11\t2023.09.28.0000.\
241        0000\tsha1\t50000000\ta08e8a071b615b0babc45a09979ab6bc70affe14\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
242        D2023.09.28.0000.0000.patch\r\n82378953\t5854598228\t30\t4\t2023.07.26.0000.0001\tsha1\t50000000\t07d9fecb3975028fdf81166aa5a4cca48bc5a4b0,\
243        c10677985f809df93a739ed7b244d90d37456353\thttp://patch-dl.ffxiv.com/game/ex1/6b936f08/\
244        D2023.07.26.0000.0001.patch\r\n29384945\t5855426508\t30\t4\t2023.09.23.0000.0001\tsha1\t50000000\tcf4970957e846e6cacfdad521252de18afc7e29b\thttp:/\
245        /patch-dl.ffxiv.com/game/ex1/6b936f08/\
246        D2023.09.23.0000.0001.patch\r\n136864\t5855426508\t30\t4\t2023.09.26.0000.0000\tsha1\t50000000\t3ca5e0160e1cedb7a2801048408b247095f432ea\thttp://\
247        patch-dl.ffxiv.com/game/ex1/6b936f08/\
248        D2023.09.26.0000.0000.patch\r\n126288\t5855426508\t30\t4\t2023.09.28.0000.0000\tsha1\t50000000\t88d201defb32366004c88b236d03278f95d9b442\thttp://\
249        patch-dl.ffxiv.com/game/ex1/6b936f08/\
250        D2023.09.28.0000.0000.patch\r\n49352444\t7620831756\t24\t4\t2023.09.23.0000.0001\tsha1\t50000000\t2a05a452281d119241f222f4eae43266a22560fe\thttp:/\
251        /patch-dl.ffxiv.com/game/ex2/f29a3eb2/\
252        D2023.09.23.0000.0001.patch\r\n301600\t7620831756\t24\t4\t2023.09.26.0000.0000\tsha1\t50000000\t215de572fe51bca45f83e19d719f52220818bc39\thttp://\
253        patch-dl.ffxiv.com/game/ex2/f29a3eb2/\
254        D2023.09.26.0000.0000.patch\r\n385096\t7620831756\t24\t4\t2023.09.28.0000.0000\tsha1\t50000000\t427c3fd61f2ca79698ecd6dc34e3457f6d8c01cd\thttp://\
255        patch-dl.ffxiv.com/game/ex2/f29a3eb2/\
256        D2023.09.28.0000.0000.patch\r\n60799419\t9737248724\t26\t4\t2023.09.23.0000.0001\tsha1\t50000000\tab064df7aec0526e8306a78e51adbbba8b129c3f,\
257        4e1bb58a987b3157c16f59821bc67b1464a301e5\thttp://patch-dl.ffxiv.com/game/ex3/859d0e24/\
258        D2023.09.23.0000.0001.patch\r\n555712\t9737248724\t26\t4\t2023.09.26.0000.0000\tsha1\t50000000\tdb6b1f34b0b58ca0d0621ff6cebcc63e7eb692c5\thttp://\
259        patch-dl.ffxiv.com/game/ex3/859d0e24/\
260        D2023.09.26.0000.0000.patch\r\n579560\t9737248724\t26\t4\t2023.09.28.0000.0000\tsha1\t50000000\t67b5d62ee8202fe045c763deea38c136c5324195\thttp://\
261        patch-dl.ffxiv.com/game/ex3/859d0e24/\
262        D2023.09.28.0000.0000.patch\r\n867821466\t12469514712\t40\t4\t2023.09.15.0000.0001\tsha1\t50000000\t08f6164685b4363d719d09a8d0ef0910b48ec4a1,\
263        05819ea5182885b59f0dfb981ecab159f46d7343,6ab6106ce4153cac5edb26ad0aabc6ba718ee500,3c552f54cc3c101d9f1786156c1cbd9880a7c02f,\
264        e0d425f6032da1ceb60ff9ca14a10e5e89f23218,245402087bf6d535cb08bbc189647e8f56301722,a3a0630bb4ddd36b532be0e0a8982dbce1bb9053,\
265        eca8a1394db1e84b9ec11a99bd970e6335326da5,40546e6d37cc5ea21d26c2e00a11f46a13d99a77,f41f312ad72ee65dc97645c8f7426c35970187ca,\
266        e5a9966528ecab5a51059de3d5cd83a921c73a1c,c03855127d135d22c65e34e615eddbe6f38769e9,befab30a77c14743f53b20910b564bb6a97dfe86,\
267        dcce8ea707f03606b583d51d947a4cf99b52635e,c4a33a8c51a047706b65887bed804ec6c2c29016,a17bc8bd8709c2a0c5725c134101132d3536e67d,\
268        d2b277de55a65697d80cfe8ee46199a8d7482c30,6b97cc2862c6f8f5d279be5f28cc13ed011763e5\thttp://patch-dl.ffxiv.com/game/ex4/1bf99b87/\
269        D2023.09.15.0000.0001.patch\r\n17717567\t12471821656\t40\t4\t2023.09.23.0000.0001\tsha1\t50000000\tda144d5c1c173ef1d98e4e7b558414ae53bcd392\thttp:\
270        //patch-dl.ffxiv.com/game/ex4/1bf99b87/\
271        D2023.09.23.0000.0001.patch\r\n3117253\t12471859944\t40\t4\t2023.09.26.0000.0000\tsha1\t50000000\t7acdab61e99d69ffa53e9136f65a1a1d3b33732b\thttp:/\
272        /patch-dl.ffxiv.com/game/ex4/1bf99b87/\
273        D2023.09.26.0000.0000.patch\r\n4676853\t12471859944\t40\t4\t2023.09.28.0000.0000\tsha1\t50000000\tce26ccb2115af612ccd4c42c1a27ef2ec925c81e\thttp:/\
274        /patch-dl.ffxiv.com/game/ex4/1bf99b87/D2023.09.28.0000.0000.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";
275
276        let patch_list = PatchList::from_string(PatchListType::Game, test_case);
277        assert_eq!(patch_list.patches.len(), 19);
278        assert_eq!(patch_list.patches[5].version, "2023.07.26.0000.0001");
279        assert_eq!(
280            patch_list.patches[5].url,
281            "http://patch-dl.ffxiv.com/game/ex1/6b936f08/D2023.07.26.0000.0001.patch"
282        );
283        assert_eq!(patch_list.patches[5].size_on_disk, 5854598228);
284        assert_eq!(patch_list.patch_length, 1664916486);
285    }
286
287    #[test]
288    fn test_boot_patch_output() {
289        let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
290        ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http\r\nX-Patch-Length: \
291        22221335\r\n\r\n22221335\t69674819\t19\t18\t2023.09.14.0000.0001\thttp://patch-dl.ffxiv.com/boot/2b5cbc63/\
292        D2023.09.14.0000.0001.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";
293
294        let patch_list = PatchList {
295            id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
296            requested_version: "D2023.04.28.0000.0001".to_string(),
297            content_location: "ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http".to_string(),
298            patches: vec![PatchEntry {
299                url: "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch"
300                    .to_string(),
301                version: "2023.09.14.0000.0001".to_string(),
302                hash_block_size: 0,
303                length: 22221335,
304                size_on_disk: 69674819,
305                hashes: vec![],
306                unknown_a: 19,
307                unknown_b: 18,
308            }],
309            patch_length: 22221335,
310        };
311
312        assert_eq!(patch_list.to_string(PatchListType::Boot), test_case);
313    }
314
315    #[test]
316    fn test_game_patch_output() {
317        let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
318        ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http\r\nX-Patch-Length: \
319        1479062470\r\n\r\n1479062470\t44145529682\t71\t11\t2023.09.15.0000.0000\tsha1\t50000000\t1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c,\
320        950725418366c965d824228bf20f0496f81e0b9a,cabef48f7bf00fbf18b72843bdae2f61582ad264,53608de567b52f5fdb43fdb8b623156317e26704,\
321        f0bc06cabf9ff6490f36114b25f62619d594dbe8,3c5e4b962cd8445bd9ee29011ecdb331d108abd8,88e1a2a322f09de3dc28173d4130a2829950d4e0,\
322        1040667917dc99b9215dfccff0e458c2e8a724a8,149c7e20e9e3e376377a130e0526b35fd7f43df2,1bb4e33807355cdf46af93ce828b6e145a9a8795,\
323        a79daff43db488f087da8e22bb4c21fd3a390f3c,6b04fadb656d467fb8318eba1c7f5ee8f030d967,a6641e1c894db961a49b70fda2b0d6d87be487a7,\
324        edf419de49f42ef19bd6814f8184b35a25e9e977,c1525c4df6001b66b575e2891db0284dc3a16566,01b7628095b07fa3c9c1aed2d66d32d118020321,\
325        991b137ea0ebb11bd668f82149bc2392a4cbcf52,ad3f74d4fca143a6cf507fc859544a4bcd501d85,936a0f1711e273519cae6b2da0d8b435fe6aa020,\
326        023f19d8d8b3ecaaf865e3170e8243dd437a384c,2d9e934de152956961a849e81912ca8d848265ca,8e32f9aa76c95c60a9dbe0967aee5792b812d5ec,\
327        dee052b9aa1cc8863efd61afc63ac3c2d56f9acc,fa81225aea53fa13a9bae1e8e02dea07de6d7052,59b24693b1b62ea1660bc6f96a61f7d41b3f7878,\
328        349b691db1853f6c0120a8e66093c763ba6e3671,4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50,de94175c4db39a11d5334aefc7a99434eea8e4f9,\
329        55dd7215f24441d6e47d1f9b32cebdb041f2157f,2ca09db645cfeefa41a04251dfcb13587418347a\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
330        D2023.09.15.0000.0000.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";
331
332        let patch_list = PatchList {
333            id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
334            requested_version: "2023.07.26.0000.0000".to_string(),
335            content_location: "ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http".to_string(),
336            patches: vec![PatchEntry {
337                url: "http://patch-dl.ffxiv.com/game/4e9a232b/D2023.09.15.0000.0000.patch"
338                    .to_string(),
339                version: "2023.09.15.0000.0000".to_string(),
340                hash_block_size: 50000000,
341                length: 1479062470,
342                size_on_disk: 44145529682,
343                unknown_a: 71,
344                unknown_b: 11,
345                hashes: vec![
346                    "1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c".to_string(),
347                    "950725418366c965d824228bf20f0496f81e0b9a".to_string(),
348                    "cabef48f7bf00fbf18b72843bdae2f61582ad264".to_string(),
349                    "53608de567b52f5fdb43fdb8b623156317e26704".to_string(),
350                    "f0bc06cabf9ff6490f36114b25f62619d594dbe8".to_string(),
351                    "3c5e4b962cd8445bd9ee29011ecdb331d108abd8".to_string(),
352                    "88e1a2a322f09de3dc28173d4130a2829950d4e0".to_string(),
353                    "1040667917dc99b9215dfccff0e458c2e8a724a8".to_string(),
354                    "149c7e20e9e3e376377a130e0526b35fd7f43df2".to_string(),
355                    "1bb4e33807355cdf46af93ce828b6e145a9a8795".to_string(),
356                    "a79daff43db488f087da8e22bb4c21fd3a390f3c".to_string(),
357                    "6b04fadb656d467fb8318eba1c7f5ee8f030d967".to_string(),
358                    "a6641e1c894db961a49b70fda2b0d6d87be487a7".to_string(),
359                    "edf419de49f42ef19bd6814f8184b35a25e9e977".to_string(),
360                    "c1525c4df6001b66b575e2891db0284dc3a16566".to_string(),
361                    "01b7628095b07fa3c9c1aed2d66d32d118020321".to_string(),
362                    "991b137ea0ebb11bd668f82149bc2392a4cbcf52".to_string(),
363                    "ad3f74d4fca143a6cf507fc859544a4bcd501d85".to_string(),
364                    "936a0f1711e273519cae6b2da0d8b435fe6aa020".to_string(),
365                    "023f19d8d8b3ecaaf865e3170e8243dd437a384c".to_string(),
366                    "2d9e934de152956961a849e81912ca8d848265ca".to_string(),
367                    "8e32f9aa76c95c60a9dbe0967aee5792b812d5ec".to_string(),
368                    "dee052b9aa1cc8863efd61afc63ac3c2d56f9acc".to_string(),
369                    "fa81225aea53fa13a9bae1e8e02dea07de6d7052".to_string(),
370                    "59b24693b1b62ea1660bc6f96a61f7d41b3f7878".to_string(),
371                    "349b691db1853f6c0120a8e66093c763ba6e3671".to_string(),
372                    "4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50".to_string(),
373                    "de94175c4db39a11d5334aefc7a99434eea8e4f9".to_string(),
374                    "55dd7215f24441d6e47d1f9b32cebdb041f2157f".to_string(),
375                    "2ca09db645cfeefa41a04251dfcb13587418347a".to_string(),
376                ],
377            }],
378            patch_length: 1479062470,
379        };
380
381        assert_eq!(patch_list.to_string(PatchListType::Game), test_case);
382    }
383}