///|
pub(all) struct InventoryRecord {
library : String
manifest : String
app : SteamApp
} derive(Eq, Debug)
///|
pub(all) struct InventoryIssue {
code : String
library : String
manifest : String?
message : String
} derive(Eq, Debug)
///|
pub(all) struct InventoryTotal {
known_size_bytes : Int64
unknown_size_count : Int
} derive(Eq, Debug)
///|
pub extend InventoryRecord with Eq::{equal, not_equal}
///|
pub extend InventoryRecord with @debug.Debug::{to_repr}
///|
pub extend InventoryIssue with Eq::{equal, not_equal}
///|
pub extend InventoryIssue with @debug.Debug::{to_repr}
///|
pub extend InventoryTotal with Eq::{equal, not_equal}
///|
pub extend InventoryTotal with @debug.Debug::{to_repr}
///|
/// Filter by Unicode lowercase name or identifier; sort by name, id or descending
/// size. Missing sizes sort last. Identifiers are compared as decimal integers.
pub fn select_inventory(
records : Array[InventoryRecord],
filter? : String = "",
sort? : String = "id",
) -> Array[InventoryRecord] raise SteamError {
if sort != "id" && sort != "name" && sort != "size" {
raise InvalidManifest("Unknown inventory sort")
}
let needle = filter.to_lower()
let result = records.filter(r => {
r.app.name.to_lower().contains(needle) || r.app.app_id.contains(needle)
})
result.sort_by((a, b) => {
let primary = match sort {
"name" => a.app.name.to_lower().compare(b.app.name.to_lower())
"size" =>
match (a.app.size_on_disk, b.app.size_on_disk) {
(Some(x), Some(y)) => y.compare(x)
(Some(_), None) => -1
(None, Some(_)) => 1
_ => 0
}
_ => 0
}
if primary != 0 {
primary
} else {
let by_id = decimal_compare(a.app.app_id, b.app.app_id)
if by_id != 0 {
by_id
} else {
let by_library = a.library.compare(b.library)
if by_library != 0 {
by_library
} else {
a.manifest.compare(b.manifest)
}
}
}
})
result
}
///|
fn decimal_compare(a : String, b : String) -> Int {
let x = a.trim_start(chars="0").to_owned()
let y = b.trim_start(chars="0").to_owned()
let length = x.length().compare(y.length())
if length != 0 {
length
} else {
x.compare(y)
}
}
///|
pub fn inventory_total(
records : Array[InventoryRecord],
) -> InventoryTotal raise SteamError {
let mut known = 0L
let mut unknown = 0
for record in records {
match record.app.size_on_disk {
Some(size) => {
if size < 0 || size > 9223372036854775807L - known {
raise InvalidManifest("Inventory size total exceeds Int64 range")
}
known += size
}
None => unknown += 1
}
}
{ known_size_bytes: known, unknown_size_count: unknown, }
}
///|
/// Check listed records against declared apps. Paths are compared exactly;
/// filesystem canonicalization and case rules belong to the host adapter.
pub fn check_inventory(
libraries : Array[SteamLibrary],
records : Array[InventoryRecord],
) -> Array[InventoryIssue] {
let issues : Array[InventoryIssue] = []
let paths : Map[String, Bool] = Map([])
let ids : Map[String, Bool] = Map([])
let present : Map[(String, String), Bool] = Map([])
for record in records {
let name = record.manifest
.replace_all(old="\\", new="/")
.split("/")
.last()
.unwrap()
.to_owned()
if name != "appmanifest_" + record.app.app_id + ".acf" {
issues.push({
code: "appid_mismatch",
library: record.library,
manifest: Some(record.manifest),
message: "Manifest filename does not match AppState appid",
})
}
if ids.contains(record.app.app_id) {
issues.push({
code: "duplicate_app",
library: record.library,
manifest: Some(record.manifest),
message: "App identifier occurs in multiple manifests: " +
record.app.app_id,
})
}
ids[record.app.app_id] = true
present[(record.library, record.app.app_id)] = true
}
for library in libraries {
if paths.contains(library.path) {
issues.push({
code: "duplicate_library",
library: library.path,
manifest: None,
message: "Library path occurs more than once",
})
continue
}
paths[library.path] = true
for id in library.app_ids {
if !present.contains((library.path, id)) {
issues.push({
code: "missing_manifest",
library: library.path,
manifest: None,
message: "Declared app has no readable manifest: " + id,
})
}
}
}
issues
}
///|
/// RFC 4180 quoting with CRLF records. All fields are quoted, so commas,
/// quotes and embedded line breaks remain unambiguous.
pub fn inventory_csv(records : Array[InventoryRecord]) -> String {
let builder = StringBuilder()
builder.write_string(
"\"app_id\",\"name\",\"install_dir\",\"size_on_disk\",\"build_id\",\"library\",\"manifest\"\r\n",
)
for record in records {
let app = record.app
let fields = [
app.app_id,
app.name,
app.install_dir,
match app.size_on_disk {
Some(n) => n.to_string()
None => ""
},
app.build_id.unwrap_or(""),
record.library,
record.manifest,
]
for i in 0.. 0 {
builder.write_string(",")
}
builder.write_string("\"")
builder.write_string(fields[i].replace_all(old="\"", new="\"\""))
builder.write_string("\"")
}
builder.write_string("\r\n")
}
builder.to_string()
}