initial version

This commit is contained in:
Robin Appelman 2025-06-20 23:39:52 +02:00
commit 932c20cd71
6 changed files with 3300 additions and 2 deletions

1
src/error.rs Normal file
View file

@ -0,0 +1 @@

View file

@ -1,3 +1,66 @@
fn main() {
println!("Hello, world!");
use clap::Parser;
use crate::product_info::ProductInfoFetcher;
use main_error::MainResult;
use serde::Serialize;
use crate::prefetch::prefetch;
mod product_info;
mod error;
mod prefetch;
#[derive(Debug, Parser)]
struct Args {
/// App to prefetch depots for
app_id: u32,
/// Branch to prefetch, defaults to "public"
#[clap(long, default_value_t = String::from("public"))]
branch: String,
/// Only prefetch the listed depot(s)
#[clap(long)]
depot: Vec<u32>,
}
#[tokio::main]
async fn main() -> MainResult {
let args = Args::parse();
let info_fetcher = ProductInfoFetcher::new().await?;
let depots = info_fetcher.fetch_depots(args.app_id).await?;
let manifests = depots.into_iter()
.filter(|(depot_id, _)| args.depot.is_empty() || args.depot.contains(depot_id))
.flat_map(|(depot_id, depot)| depot.branches.into_iter().map(move |(branch, manifest)| (depot_id, branch, manifest)))
.filter(|(_, branch, _)| &args.branch == branch)
.map(|(depot_id, _, manifest)| ManifestInput {
app_id: args.app_id,
depot_id,
manifest: manifest.gid,
});
let outputs = manifests.map(|input| {
prefetch(&input).map(|hash| ManifestOutput {
app_id: input.app_id,
depot_id: input.depot_id,
manifest: input.manifest,
hash,
})
}).collect::<Result<Vec<_>, _>>()?;
serde_json::to_writer_pretty(std::io::stdout(), &outputs)?;
Ok(())
}
struct ManifestInput {
app_id: u32,
depot_id: u32,
manifest: u64,
}
#[derive(Debug, Serialize)]
struct ManifestOutput {
app_id: u32,
depot_id: u32,
manifest: u64,
hash: String,
}

63
src/prefetch.rs Normal file
View file

@ -0,0 +1,63 @@
use crate::ManifestInput;
use std::process::Command;
use std::string::FromUtf8Error;
use thiserror::Error;
const EXPRESSION_TEMPLATE: &str = r#"
{
pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/9ba04bda9249d5d5e5238303c9755de5a49a79c5.tar.gz";
sha256 = "sha256-H8J4H2XCIMEJ5g6fZ179QfQvsc2dUqhqfBjC8RAHNRY=";
}) {}
}: let
steam-fetcher = pkgs.fetchFromGitHub {
owner = "nix-community";
repo = "steam-fetcher";
rev = "12f66eafb7862d91b3e30c14035f96a21941bd9c";
hash = "sha256-PkgC9jqoN6cJ8XYzTA2PlrWs7aPJkM3BGiTxNqax0cA=";
};
fetchSteam = pkgs.callPackage (steam-fetcher + "/fetch-steam") {};
in fetchSteam {
name = "steam-depot-APP_ID-DEPOT_ID";
appId = "APP_ID";
depotId = "DEPOT_ID";
manifestId = "MANIFEST_ID";
hash = "";
}
"#;
pub fn prefetch(manifest: &ManifestInput) -> Result<String, PrefetchError> {
let expression = EXPRESSION_TEMPLATE
.replace("APP_ID", &manifest.app_id.to_string())
.replace("DEPOT_ID", &manifest.depot_id.to_string())
.replace("MANIFEST_ID", &manifest.manifest.to_string());
let command = Command::new("nix-build")
.arg("-E")
.arg(&expression)
.arg("--no-out-link")
.output()
.map_err(PrefetchError::CommandFailed)?;
let stderr = String::from_utf8(command.stderr).map_err(PrefetchError::Utf8)?;
let Some(line) = stderr.lines().find(|line| line.contains("got:")) else {
return Err(PrefetchError::BuildFailed);
};
let Some(start) = line.find("sha256-") else {
return Err(PrefetchError::BuildFailed);
};
let Some(length) = line[start..].find("=") else {
return Err(PrefetchError::BuildFailed);
};
Ok(line[start..(start + length + 1)].to_string())
}
#[derive(Debug, Error)]
pub enum PrefetchError {
#[error("Error while running nix-build: {0:#}")]
CommandFailed(std::io::Error),
#[error("nix-build failed or didn't provide an output hash")]
BuildFailed,
#[error("nix-build returned non-utf8 output")]
Utf8(FromUtf8Error),
}

124
src/product_info.rs Normal file
View file

@ -0,0 +1,124 @@
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use std::string::FromUtf8Error;
use steam_vent::proto::steammessages_clientserver_appinfo::{
CMsgClientPICSProductInfoRequest, CMsgClientPICSProductInfoResponse,
cmsg_client_picsproduct_info_request,
};
use steam_vent::{Connection, ConnectionError, ConnectionTrait, NetworkError, ServerList};
use thiserror::Error;
use vdf_reader::VdfError;
pub struct ProductInfoFetcher {
connection: Connection,
}
impl ProductInfoFetcher {
pub async fn new() -> Result<ProductInfoFetcher, ProductInfoError> {
let server_list = ServerList::discover()
.await
.map_err(ConnectionError::Discovery)?;
let connection = Connection::anonymous(&server_list).await?;
Ok(ProductInfoFetcher { connection })
}
pub async fn fetch_depots(
&self,
app_id: u32,
) -> Result<BTreeMap<u32, Depot>, ProductInfoError> {
let msg = CMsgClientPICSProductInfoRequest {
apps: vec![cmsg_client_picsproduct_info_request::AppInfo {
appid: Some(app_id),
only_public_obsolete: Some(true),
..Default::default()
}],
meta_data_only: Some(false),
single_response: Some(true),
..Default::default()
};
let response: CMsgClientPICSProductInfoResponse = self.connection.job(msg).await?;
let app_info = response.apps.into_iter().next().unwrap();
let mut buffer = app_info.buffer.unwrap_or_default();
while buffer.ends_with(&[0]) {
let _ = buffer.pop();
}
let raw_vdf = String::from_utf8(buffer)?;
let vdf: Response = vdf_reader::from_str(&raw_vdf)?;
assert_eq!(app_id, vdf.app_info.app_id);
Ok(vdf
.app_info
.depots
.depots
.into_iter()
.filter_map(|(depot_id, depot)| Some((u32::from_str(&depot_id).ok()?, depot)))
.collect())
}
}
#[derive(Debug, Deserialize)]
struct Response {
#[serde(rename = "appinfo")]
app_info: AppInfo,
}
#[derive(Debug, Deserialize)]
struct AppInfo {
#[serde(rename = "appid")]
app_id: u32,
depots: Depots,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct Depots {
overridescddb: bool,
branches: HashMap<String, Branch>,
#[serde(flatten)]
depots: BTreeMap<String, Depot>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct Depot {
#[serde(default)]
pub config: DepotConfig,
#[serde(rename = "manifests")]
pub branches: HashMap<String, Manifest>,
}
#[allow(dead_code)]
#[derive(Debug, Default, Deserialize)]
pub struct DepotConfig {
pub oslist: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct Manifest {
pub gid: u64,
pub size: u64,
pub download: u64,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct Branch {
timeupdated: u64,
buildid: u64,
}
#[derive(Debug, Error)]
pub enum ProductInfoError {
#[error("Error while connecting to steam: {0:#}")]
Connect(#[from] ConnectionError),
#[error("Error while fetching product info from steam: {0:#}")]
Steam(#[from] NetworkError),
#[error("Steam returned non-utf8 data")]
Utf8(#[from] FromUtf8Error),
#[error("Error while parsing product info: {0:#}")]
Vdf(#[from] VdfError),
}