Skip to content

Commit

Permalink
Add PatchList parsing from Kawari
Browse files Browse the repository at this point in the history
This was originally from my Kawari project, but this belongs in here, so
we can share it between Kawari and Astra.
  • Loading branch information
redstrate committed Aug 23, 2024
1 parent 7b8c4c9 commit 979b8df
Show file tree
Hide file tree
Showing 2 changed files with 357 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,6 @@ pub mod stm;

/// Find existing installation directories
pub mod existing_dirs;

/// Reading patch lists
pub mod patchlist;
354 changes: 354 additions & 0 deletions src/patchlist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later

/// Represents a patch to be downloaded.
pub struct PatchEntry {
/// The URL of the patch file. Usually an HTTP URL.
pub url: String,
/// The version for this patch.
pub version: String,
/// How many bytes each SHA1 hash block is considering.
pub hash_block_size: i64,
/// Length of the patch file (in bytes.)
pub length: i64,
/// New size of the (game?) when the patch is installed.
pub size_on_disk: i64,
/// The list of SHA1 hashes.
pub hashes: Vec<String>,

// TODO: figure out what this is
unknown_a: i32,
// TODO: ditto
unknown_b: i32,
}

/// A list of patch files the client is requested to download, and install.
pub struct PatchList {
/// The id of the patch list.
// FIXME: this is most likely auto-generated, not set?
pub id: String,
/// Size of all the patch files by size (in bytes.)
pub patch_length: u64,
/// The content location, usually from an HTTP URL.
pub content_location: String,
/// The version that was requested from the server.
pub requested_version: String,
/// The list of patches.
pub patches: Vec<PatchEntry>
}

/// The kind of patch list.
/// This must be the kind of patch list you're parsing, or else the fields will filled in the wrong order.
#[derive(PartialEq)]
#[repr(C)]
pub enum PatchListType {
/// A boot patch list.
Boot,
/// A game patch ist.
Game
}

impl PatchList {
pub fn from_string(patch_type: PatchListType, encoded: &str) -> Self {
let mut patches = vec![];

let mut patch_length = 0;
if let Some(patch_length_index) = encoded.find("X-Patch-Length: ") {
let rest_of_string = &encoded[patch_length_index..];
if let Some(end_of_number_index) = rest_of_string.find("\r\n") {
let patch_length_parse: Result<u64, _> = rest_of_string[0..end_of_number_index].parse();
if let Ok(p) = patch_length_parse {
patch_length = p;
}
}
};

let parts: Vec<_> = encoded.split("\r\n").collect();
for i in 5..parts.len() - 2 {
let patch_parts: Vec<_> = parts[i].split('\t').collect();

if patch_type == PatchListType::Boot {
patches.push(PatchEntry {
url: patch_parts[5].parse().unwrap(),
version: patch_parts[4].parse().unwrap(),
hash_block_size: 0,
length: patch_parts[0].parse().unwrap(),
size_on_disk: patch_parts[1].parse().unwrap(),
hashes: vec![],
unknown_a: 0,
unknown_b: 0,
});
} else {
patches.push(PatchEntry {
url: patch_parts[8].parse().unwrap(),
version: patch_parts[4].parse().unwrap(),
hash_block_size: patch_parts[6].parse().unwrap(),
length: patch_parts[0].parse().unwrap(),
size_on_disk: patch_parts[1].parse().unwrap(),
hashes: patch_parts[7].split(',').map(|x| x.to_string()).collect(),
unknown_a: 0,
unknown_b: 0,
});
}
}

Self {
id: "".to_string(),
content_location: "".to_string(),
requested_version: "".to_string(),
patch_length: patch_length,
patches,
}
}

pub fn to_string(&self, patch_type: PatchListType) -> String {
let mut str = String::new();

// header
str.push_str("--");
str.push_str(&self.id);
str.push_str("\r\n");
str.push_str("Content-Type: application/octet-stream\r\n");
str.push_str(&format!("Content-Location: {}\r\n", self.content_location));

let mut total_patch_size = 0;
for patch in &self.patches {
total_patch_size += patch.length;
}

str.push_str(&format!("X-Patch-Length: {}\r\n", total_patch_size));
str.push_str("\r\n");

for patch in &self.patches {
// length
str.push_str(&patch.length.to_string());
str.push('\t');

// TODO: unknown value, but i *suspect* is the size of the game on disk once this patch is applied.
// which would make sense for the launcher to check for
str.push_str(&patch.size_on_disk.to_string());
str.push('\t');

// TODO: totally unknown
str.push_str(&patch.unknown_a.to_string());
str.push('\t');

// TODO: unknown too
str.push_str(&patch.unknown_b.to_string());
str.push('\t');

// version (e.g. 2023.09.15.0000.0000)
str.push_str(&patch.version);
str.push('\t');

if patch_type == PatchListType::Game {
// hash type
// TODO: does this need to be configurable?
str.push_str("sha1");
str.push('\t');

// hash block size
str.push_str(&patch.hash_block_size.to_string());
str.push('\t');

// hashes
str.push_str(&patch.hashes[0]);
for hash in &patch.hashes[1..] {
str.push(',');
str.push_str(&hash);
}
str.push('\t');
}

// url
str.push_str(&patch.url);
str.push_str("\r\n");
}

str.push_str("--");
str.push_str(&self.id);
str.push_str("--\r\n");

str
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_boot_patch_parsing() {
let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http\r\nX-Patch-Length: \
22221335\r\n\r\n22221335\t69674819\t19\t18\t2023.09.14.0000.0001\thttp://patch-dl.ffxiv.com/boot/2b5cbc63/\
D2023.09.14.0000.0001.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";

let patch_list = PatchList::from_string(PatchListType::Boot, test_case);
assert_eq!(patch_list.patches.len(), 1);
assert_eq!(patch_list.patches[0].version, "2023.09.14.0000.0001");
assert_eq!(patch_list.patches[0].url, "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch");
}

#[test]
fn test_game_patch_parsing() {
let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http\r\nX-Patch-Length: \
1664916486\r\n\r\n1479062470\t44145529682\t71\t11\t2023.09.15.0000.0000\tsha1\t50000000\t1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c,\
950725418366c965d824228bf20f0496f81e0b9a,cabef48f7bf00fbf18b72843bdae2f61582ad264,53608de567b52f5fdb43fdb8b623156317e26704,\
f0bc06cabf9ff6490f36114b25f62619d594dbe8,3c5e4b962cd8445bd9ee29011ecdb331d108abd8,88e1a2a322f09de3dc28173d4130a2829950d4e0,\
1040667917dc99b9215dfccff0e458c2e8a724a8,149c7e20e9e3e376377a130e0526b35fd7f43df2,1bb4e33807355cdf46af93ce828b6e145a9a8795,\
a79daff43db488f087da8e22bb4c21fd3a390f3c,6b04fadb656d467fb8318eba1c7f5ee8f030d967,a6641e1c894db961a49b70fda2b0d6d87be487a7,\
edf419de49f42ef19bd6814f8184b35a25e9e977,c1525c4df6001b66b575e2891db0284dc3a16566,01b7628095b07fa3c9c1aed2d66d32d118020321,\
991b137ea0ebb11bd668f82149bc2392a4cbcf52,ad3f74d4fca143a6cf507fc859544a4bcd501d85,936a0f1711e273519cae6b2da0d8b435fe6aa020,\
023f19d8d8b3ecaaf865e3170e8243dd437a384c,2d9e934de152956961a849e81912ca8d848265ca,8e32f9aa76c95c60a9dbe0967aee5792b812d5ec,\
dee052b9aa1cc8863efd61afc63ac3c2d56f9acc,fa81225aea53fa13a9bae1e8e02dea07de6d7052,59b24693b1b62ea1660bc6f96a61f7d41b3f7878,\
349b691db1853f6c0120a8e66093c763ba6e3671,4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50,de94175c4db39a11d5334aefc7a99434eea8e4f9,\
55dd7215f24441d6e47d1f9b32cebdb041f2157f,2ca09db645cfeefa41a04251dfcb13587418347a\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
D2023.09.15.0000.0000.patch\r\n61259063\t44145955874\t71\t11\t2023.09.21.0000.0001\tsha1\t50000000\t88c9bbfe2af4eea7b56384baeeafd59afb47ddeb,\
095c26e87b4d25505845515c389dd22dd429ea7e\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
D2023.09.21.0000.0001.patch\r\n63776300\t44146911186\t71\t11\t2023.09.23.0000.0001\tsha1\t50000000\tc8fc6910be12d10b39e4e6ae980d4c219cfe56a1,\
5c0199b7147a47f620a2b50654a87c9b0cbcf43b\thttp://patch-dl.ffxiv.com/game/4e9a232b/
D2023.09.23.0000.0001.patch\r\n32384649\t44146977234\t71\t11\t2023.09.26.0000.\
0000\tsha1\t50000000\t519a5e46edb67ba6edb9871df5eb3991276da254\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
D2023.09.26.0000.0000.patch\r\n28434004\t44147154898\t71\t11\t2023.09.28.0000.\
0000\tsha1\t50000000\ta08e8a071b615b0babc45a09979ab6bc70affe14\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
D2023.09.28.0000.0000.patch\r\n82378953\t5854598228\t30\t4\t2023.07.26.0000.0001\tsha1\t50000000\t07d9fecb3975028fdf81166aa5a4cca48bc5a4b0,\
c10677985f809df93a739ed7b244d90d37456353\thttp://patch-dl.ffxiv.com/game/ex1/6b936f08/\
D2023.07.26.0000.0001.patch\r\n29384945\t5855426508\t30\t4\t2023.09.23.0000.0001\tsha1\t50000000\tcf4970957e846e6cacfdad521252de18afc7e29b\thttp:/\
/patch-dl.ffxiv.com/game/ex1/6b936f08/\
D2023.09.23.0000.0001.patch\r\n136864\t5855426508\t30\t4\t2023.09.26.0000.0000\tsha1\t50000000\t3ca5e0160e1cedb7a2801048408b247095f432ea\thttp://\
patch-dl.ffxiv.com/game/ex1/6b936f08/\
D2023.09.26.0000.0000.patch\r\n126288\t5855426508\t30\t4\t2023.09.28.0000.0000\tsha1\t50000000\t88d201defb32366004c88b236d03278f95d9b442\thttp://\
patch-dl.ffxiv.com/game/ex1/6b936f08/\
D2023.09.28.0000.0000.patch\r\n49352444\t7620831756\t24\t4\t2023.09.23.0000.0001\tsha1\t50000000\t2a05a452281d119241f222f4eae43266a22560fe\thttp:/\
/patch-dl.ffxiv.com/game/ex2/f29a3eb2/\
D2023.09.23.0000.0001.patch\r\n301600\t7620831756\t24\t4\t2023.09.26.0000.0000\tsha1\t50000000\t215de572fe51bca45f83e19d719f52220818bc39\thttp://\
patch-dl.ffxiv.com/game/ex2/f29a3eb2/\
D2023.09.26.0000.0000.patch\r\n385096\t7620831756\t24\t4\t2023.09.28.0000.0000\tsha1\t50000000\t427c3fd61f2ca79698ecd6dc34e3457f6d8c01cd\thttp://\
patch-dl.ffxiv.com/game/ex2/f29a3eb2/\
D2023.09.28.0000.0000.patch\r\n60799419\t9737248724\t26\t4\t2023.09.23.0000.0001\tsha1\t50000000\tab064df7aec0526e8306a78e51adbbba8b129c3f,\
4e1bb58a987b3157c16f59821bc67b1464a301e5\thttp://patch-dl.ffxiv.com/game/ex3/859d0e24/\
D2023.09.23.0000.0001.patch\r\n555712\t9737248724\t26\t4\t2023.09.26.0000.0000\tsha1\t50000000\tdb6b1f34b0b58ca0d0621ff6cebcc63e7eb692c5\thttp://\
patch-dl.ffxiv.com/game/ex3/859d0e24/\
D2023.09.26.0000.0000.patch\r\n579560\t9737248724\t26\t4\t2023.09.28.0000.0000\tsha1\t50000000\t67b5d62ee8202fe045c763deea38c136c5324195\thttp://\
patch-dl.ffxiv.com/game/ex3/859d0e24/\
D2023.09.28.0000.0000.patch\r\n867821466\t12469514712\t40\t4\t2023.09.15.0000.0001\tsha1\t50000000\t08f6164685b4363d719d09a8d0ef0910b48ec4a1,\
05819ea5182885b59f0dfb981ecab159f46d7343,6ab6106ce4153cac5edb26ad0aabc6ba718ee500,3c552f54cc3c101d9f1786156c1cbd9880a7c02f,\
e0d425f6032da1ceb60ff9ca14a10e5e89f23218,245402087bf6d535cb08bbc189647e8f56301722,a3a0630bb4ddd36b532be0e0a8982dbce1bb9053,\
eca8a1394db1e84b9ec11a99bd970e6335326da5,40546e6d37cc5ea21d26c2e00a11f46a13d99a77,f41f312ad72ee65dc97645c8f7426c35970187ca,\
e5a9966528ecab5a51059de3d5cd83a921c73a1c,c03855127d135d22c65e34e615eddbe6f38769e9,befab30a77c14743f53b20910b564bb6a97dfe86,\
dcce8ea707f03606b583d51d947a4cf99b52635e,c4a33a8c51a047706b65887bed804ec6c2c29016,a17bc8bd8709c2a0c5725c134101132d3536e67d,\
d2b277de55a65697d80cfe8ee46199a8d7482c30,6b97cc2862c6f8f5d279be5f28cc13ed011763e5\thttp://patch-dl.ffxiv.com/game/ex4/1bf99b87/\
D2023.09.15.0000.0001.patch\r\n17717567\t12471821656\t40\t4\t2023.09.23.0000.0001\tsha1\t50000000\tda144d5c1c173ef1d98e4e7b558414ae53bcd392\thttp:\
//patch-dl.ffxiv.com/game/ex4/1bf99b87/\
D2023.09.23.0000.0001.patch\r\n3117253\t12471859944\t40\t4\t2023.09.26.0000.0000\tsha1\t50000000\t7acdab61e99d69ffa53e9136f65a1a1d3b33732b\thttp:/\
/patch-dl.ffxiv.com/game/ex4/1bf99b87/\
D2023.09.26.0000.0000.patch\r\n4676853\t12471859944\t40\t4\t2023.09.28.0000.0000\tsha1\t50000000\tce26ccb2115af612ccd4c42c1a27ef2ec925c81e\thttp:/\
/patch-dl.ffxiv.com/game/ex4/1bf99b87/D2023.09.28.0000.0000.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";

let patch_list = PatchList::from_string(PatchListType::Game, test_case);
assert_eq!(patch_list.patches.len(), 19);
assert_eq!(patch_list.patches[5].version, "2023.07.26.0000.0001");
assert_eq!(patch_list.patches[5].url, "http://patch-dl.ffxiv.com/game/ex1/6b936f08/D2023.07.26.0000.0001.patch");
}

#[test]
fn test_boot_patch_output() {
let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http\r\nX-Patch-Length: \
22221335\r\n\r\n22221335\t69674819\t19\t18\t2023.09.14.0000.0001\thttp://patch-dl.ffxiv.com/boot/2b5cbc63/\
D2023.09.14.0000.0001.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";

let patch_list = PatchList {
id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
requested_version: "D2023.04.28.0000.0001".to_string(),
content_location: "ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http".to_string(),
patches: vec![
PatchEntry {
url: "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch".to_string(),
version: "2023.09.14.0000.0001".to_string(),
hash_block_size: 0,
length: 22221335,
size_on_disk: 69674819,
hashes: vec![],
unknown_a: 19,
unknown_b: 18
}
],
patch_length: 22221335,
};

assert_eq!(patch_list.to_string(PatchListType::Boot), test_case);
}

#[test]
fn test_game_patch_output() {
let test_case = "--477D80B1_38BC_41d4_8B48_5273ADB89CAC\r\nContent-Type: application/octet-stream\r\nContent-Location: \
ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http\r\nX-Patch-Length: \
1479062470\r\n\r\n1479062470\t44145529682\t71\t11\t2023.09.15.0000.0000\tsha1\t50000000\t1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c,\
950725418366c965d824228bf20f0496f81e0b9a,cabef48f7bf00fbf18b72843bdae2f61582ad264,53608de567b52f5fdb43fdb8b623156317e26704,\
f0bc06cabf9ff6490f36114b25f62619d594dbe8,3c5e4b962cd8445bd9ee29011ecdb331d108abd8,88e1a2a322f09de3dc28173d4130a2829950d4e0,\
1040667917dc99b9215dfccff0e458c2e8a724a8,149c7e20e9e3e376377a130e0526b35fd7f43df2,1bb4e33807355cdf46af93ce828b6e145a9a8795,\
a79daff43db488f087da8e22bb4c21fd3a390f3c,6b04fadb656d467fb8318eba1c7f5ee8f030d967,a6641e1c894db961a49b70fda2b0d6d87be487a7,\
edf419de49f42ef19bd6814f8184b35a25e9e977,c1525c4df6001b66b575e2891db0284dc3a16566,01b7628095b07fa3c9c1aed2d66d32d118020321,\
991b137ea0ebb11bd668f82149bc2392a4cbcf52,ad3f74d4fca143a6cf507fc859544a4bcd501d85,936a0f1711e273519cae6b2da0d8b435fe6aa020,\
023f19d8d8b3ecaaf865e3170e8243dd437a384c,2d9e934de152956961a849e81912ca8d848265ca,8e32f9aa76c95c60a9dbe0967aee5792b812d5ec,\
dee052b9aa1cc8863efd61afc63ac3c2d56f9acc,fa81225aea53fa13a9bae1e8e02dea07de6d7052,59b24693b1b62ea1660bc6f96a61f7d41b3f7878,\
349b691db1853f6c0120a8e66093c763ba6e3671,4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50,de94175c4db39a11d5334aefc7a99434eea8e4f9,\
55dd7215f24441d6e47d1f9b32cebdb041f2157f,2ca09db645cfeefa41a04251dfcb13587418347a\thttp://patch-dl.ffxiv.com/game/4e9a232b/\
D2023.09.15.0000.0000.patch\r\n--477D80B1_38BC_41d4_8B48_5273ADB89CAC--\r\n";

let patch_list = PatchList {
id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
requested_version: "2023.07.26.0000.0000".to_string(),
content_location: "ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http".to_string(),
patches: vec![
PatchEntry {
url: "http://patch-dl.ffxiv.com/game/4e9a232b/D2023.09.15.0000.0000.patch".to_string(),
version: "2023.09.15.0000.0000".to_string(),
hash_block_size: 50000000,
length: 1479062470,
size_on_disk: 44145529682,
unknown_a: 71,
unknown_b: 11,
hashes: vec![
"1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c".to_string(),
"950725418366c965d824228bf20f0496f81e0b9a".to_string(),
"cabef48f7bf00fbf18b72843bdae2f61582ad264".to_string(),
"53608de567b52f5fdb43fdb8b623156317e26704".to_string(),
"f0bc06cabf9ff6490f36114b25f62619d594dbe8".to_string(),
"3c5e4b962cd8445bd9ee29011ecdb331d108abd8".to_string(),
"88e1a2a322f09de3dc28173d4130a2829950d4e0".to_string(),
"1040667917dc99b9215dfccff0e458c2e8a724a8".to_string(),
"149c7e20e9e3e376377a130e0526b35fd7f43df2".to_string(),
"1bb4e33807355cdf46af93ce828b6e145a9a8795".to_string(),
"a79daff43db488f087da8e22bb4c21fd3a390f3c".to_string(),
"6b04fadb656d467fb8318eba1c7f5ee8f030d967".to_string(),
"a6641e1c894db961a49b70fda2b0d6d87be487a7".to_string(),
"edf419de49f42ef19bd6814f8184b35a25e9e977".to_string(),
"c1525c4df6001b66b575e2891db0284dc3a16566".to_string(),
"01b7628095b07fa3c9c1aed2d66d32d118020321".to_string(),
"991b137ea0ebb11bd668f82149bc2392a4cbcf52".to_string(),
"ad3f74d4fca143a6cf507fc859544a4bcd501d85".to_string(),
"936a0f1711e273519cae6b2da0d8b435fe6aa020".to_string(),
"023f19d8d8b3ecaaf865e3170e8243dd437a384c".to_string(),
"2d9e934de152956961a849e81912ca8d848265ca".to_string(),
"8e32f9aa76c95c60a9dbe0967aee5792b812d5ec".to_string(),
"dee052b9aa1cc8863efd61afc63ac3c2d56f9acc".to_string(),
"fa81225aea53fa13a9bae1e8e02dea07de6d7052".to_string(),
"59b24693b1b62ea1660bc6f96a61f7d41b3f7878".to_string(),
"349b691db1853f6c0120a8e66093c763ba6e3671".to_string(),
"4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50".to_string(),
"de94175c4db39a11d5334aefc7a99434eea8e4f9".to_string(),
"55dd7215f24441d6e47d1f9b32cebdb041f2157f".to_string(),
"2ca09db645cfeefa41a04251dfcb13587418347a".to_string()
],
}
],
patch_length: 1479062470,
};

assert_eq!(patch_list.to_string(PatchListType::Game), test_case);
}
}

0 comments on commit 979b8df

Please sign in to comment.