-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
info: implement appinfo and packageinfo (needs binary vdf)
- Loading branch information
Showing
8 changed files
with
314 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]> | ||
// SPDX-License-Identifier: EUPL-1.2 | ||
|
||
import Foundation | ||
|
||
extension Data { | ||
func read<T>(fromByteOffset offset: Int = 0, as type: T.Type) -> T { | ||
return withUnsafeBytes { rawBuffer in | ||
return rawBuffer.load(fromByteOffset: offset, as: type) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]> | ||
// SPDX-License-Identifier: EUPL-1.2 | ||
|
||
import Foundation | ||
|
||
class DataCursor { | ||
public var data: Data | ||
public var index: Data.Index | ||
|
||
init(_ data: Data) { | ||
self.data = data | ||
index = self.data.startIndex | ||
} | ||
|
||
func readBytes(count: Int) throws -> Data { | ||
guard index >= 0 else { | ||
throw DataCursorError.outOfBounds | ||
} | ||
|
||
guard count + index <= data.count else { | ||
throw DataCursorError.outOfBounds | ||
} | ||
|
||
let value = data[index...(index + count)] | ||
index = index + count | ||
return value | ||
} | ||
|
||
func readAsciiString() throws -> String { | ||
guard index >= 0 else { | ||
throw DataCursorError.outOfBounds | ||
} | ||
|
||
let size = data[index...].firstIndex { byte in | ||
byte == 0 | ||
} | ||
|
||
guard let size = size else { | ||
throw DataCursorError.nonNullTerminatedString | ||
} | ||
|
||
guard let str = String(data: try readBytes(count: size), encoding: .ascii) else { | ||
throw DataCursorError.nonNullTerminatedString | ||
} | ||
|
||
index = index + 1 | ||
|
||
return str | ||
} | ||
|
||
func readUnicodeString() throws -> String { | ||
guard index >= 0 else { | ||
throw DataCursorError.outOfBounds | ||
} | ||
|
||
let size = data[index...].firstIndex { byte in | ||
byte == 0 | ||
} | ||
|
||
guard let size = size else { | ||
throw DataCursorError.nonNullTerminatedString | ||
} | ||
|
||
guard let str = String(data: try readBytes(count: size), encoding: .unicode) else { | ||
throw DataCursorError.nonNullTerminatedString | ||
} | ||
|
||
index = index + 1 | ||
|
||
return str | ||
} | ||
|
||
func read<T>(as type: T.Type) throws -> T { | ||
guard index >= 0 else { | ||
throw DataCursorError.outOfBounds | ||
} | ||
|
||
let size = MemoryLayout<T>.size | ||
guard size + index <= data.count else { | ||
throw DataCursorError.outOfBounds | ||
} | ||
|
||
let value = data.withUnsafeBytes { rawBuffer in | ||
return rawBuffer.load(fromByteOffset: index, as: type) | ||
} | ||
|
||
index = index + size | ||
|
||
return value | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]> | ||
// SPDX-License-Identifier: EUPL-1.2 | ||
|
||
enum DataCursorError: Error { | ||
case outOfBounds | ||
case nonNullTerminatedString | ||
} | ||
|
||
public enum SteamAppInfoError: Error { | ||
case unsupported | ||
} | ||
|
||
public enum TextVDFError: Error { | ||
case unexpectedToken | ||
case unterminatedString | ||
case truncated | ||
case insertingIntoRootValue | ||
case missingKey | ||
case missingValue | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]> | ||
// SPDX-License-Identifier: EUPL-1.2 | ||
|
||
import Foundation | ||
|
||
/// The state a given app is in. | ||
public enum SteamAppState: UInt32 { | ||
case invalid | ||
case prerelease | ||
case released | ||
} | ||
|
||
/// Stored data of a particular steam app. | ||
public struct SteamAppData { | ||
public let appId: UInt32 | ||
public let state: SteamAppState | ||
public let lastUpdated: UInt32 | ||
public let contentId: UInt64 | ||
public let textHash: Data | ||
public let changeId: UInt32 | ||
public let hash: Data | ||
public let vdf: ValveKeyValue | ||
|
||
internal init?(version: Int, data: DataCursor, stringTable: [String]?) throws { | ||
appId = try data.read(as: UInt32.self) | ||
if appId == 0xFFFF_FFFF || appId == 0 { | ||
return nil | ||
} | ||
|
||
let size = Data.Index(try data.read(as: UInt32.self)) | ||
let end = data.index + size | ||
|
||
state = SteamAppState(rawValue: try data.read(as: UInt32.self)) ?? .invalid | ||
lastUpdated = try data.read(as: UInt32.self) | ||
contentId = try data.read(as: UInt64.self) | ||
textHash = try data.readBytes(count: 20) | ||
changeId = try data.read(as: UInt32.self) | ||
if version >= 28 { | ||
hash = try data.readBytes(count: 20) | ||
} else { | ||
hash = textHash | ||
} | ||
vdf = ValveKeyValue(ValveKeyValueNode("")) // todo: read binary object | ||
|
||
data.index = end | ||
} | ||
} | ||
|
||
/// appinfo.vdf file format. | ||
public struct SteamAppInfo { | ||
public let version: Int | ||
public let universe: SteamUniverse | ||
public let apps: [SteamAppData] | ||
|
||
public init(version: Int, data: Data) throws { | ||
let cursor = DataCursor(data) | ||
let version = try data.read(as: UInt32.self) | ||
guard (version & 0xFFFFFF) == 0x75644 else { | ||
throw SteamAppInfoError.unsupported | ||
} | ||
|
||
self.version = Int(version >> 24) | ||
|
||
guard self.version >= 27 && self.version <= 29 else { | ||
throw SteamAppInfoError.unsupported | ||
} | ||
|
||
guard let universe = SteamUniverse(rawValue: Int(try cursor.read(as: UInt32.self))) else { | ||
throw SteamAppInfoError.unsupported | ||
} | ||
|
||
self.universe = universe | ||
|
||
var stringTable: [String]? = nil | ||
if version >= 29 { | ||
let stringTableOffset = try cursor.read(as: UInt64.self) | ||
let stringTableCursor = DataCursor(data[stringTableOffset...]) | ||
let stringCount = Int(try stringTableCursor.read(as: UInt32.self)) | ||
var table = Array(repeating: "", count: stringCount) | ||
for index in 0...stringCount { | ||
table[index] = try stringTableCursor.readUnicodeString() | ||
} | ||
stringTable = table | ||
} | ||
|
||
var apps: [SteamAppData] = [] | ||
while let app = try? SteamAppData(version: self.version, data: cursor, stringTable: stringTable) { | ||
apps.append(app) | ||
} | ||
self.apps = apps | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,17 @@ | ||
// SPDX-FileCopyrightText: 2024 Legiayayana <[email protected]> | ||
// SPDX-License-Identifier: EUPL-1.2 | ||
|
||
/// Structure for 64-bit SteamIDs | ||
|
||
/// Structure for 64-bit SteamIDs. | ||
public struct SteamID { | ||
public var rawValue: UInt | ||
|
||
public var description: String { | ||
type == .pending ? "STEAM_ID_PENDING" : "STEAM_\(universe.rawValue):\(live ? 1 : 0):\(clientID)" | ||
} | ||
|
||
public var steam3: String { | ||
let letter = switch type { | ||
let letter = | ||
switch type { | ||
case .invalid: "I" | ||
case .profile: "U" | ||
case .multiseat: "M" | ||
|
@@ -23,7 +23,7 @@ public struct SteamID { | |
case .chat: "c" | ||
case .peer: "p" | ||
case .anonymousUser: "a" | ||
} | ||
} | ||
|
||
return "[\(letter):\(universe.rawValue):\(accountID)]" | ||
} | ||
|
@@ -35,68 +35,74 @@ public struct SteamID { | |
public var live: Bool { | ||
get { | ||
(rawValue & 1) == 1 | ||
} set { | ||
} | ||
set { | ||
rawValue = (rawValue & ~1) | (newValue ? 1 : 0) | ||
} | ||
} | ||
|
||
public var clientID: Int { | ||
get { | ||
Int((rawValue >> 1) & 0x7FFFFFFF) | ||
} set { | ||
rawValue = (rawValue & ~(0x7FFFFFFF << 1)) | (UInt(newValue & 0x7FFFFFFF) << 1) | ||
Int((rawValue >> 1) & 0x7FFF_FFFF) | ||
} | ||
set { | ||
rawValue = (rawValue & ~(0x7FFF_FFFF << 1)) | (UInt(newValue & 0x7FFF_FFFF) << 1) | ||
} | ||
} | ||
|
||
public var accountID: UInt { | ||
get { | ||
UInt(rawValue & 0xFFFFFFFF) | ||
} set { | ||
rawValue = (rawValue & ~0xFFFFFFFF) | UInt(newValue & 0xFFFFFFFF) | ||
UInt(rawValue & 0xFFFF_FFFF) | ||
} | ||
set { | ||
rawValue = (rawValue & ~0xFFFF_FFFF) | UInt(newValue & 0xFFFF_FFFF) | ||
} | ||
} | ||
|
||
public var instanceID: UInt { | ||
get { | ||
UInt((rawValue >> 32) & 0xFFFFF) | ||
} set { | ||
} | ||
set { | ||
rawValue = (rawValue & ~(0xFFFFF << 32)) | (UInt(newValue & 0xFFFFF) << 32) | ||
} | ||
} | ||
|
||
public var type: SteamAccountType { | ||
get { | ||
SteamAccountType(rawValue: Int((rawValue >> 52) & 0xF)) ?? .invalid | ||
} set { | ||
} | ||
set { | ||
rawValue = (rawValue & ~(0xF << 52)) | (UInt(newValue.rawValue & 0xF) << 52) | ||
} | ||
} | ||
|
||
public var universe: SteamUniverse { | ||
get { | ||
SteamUniverse(rawValue: Int((rawValue >> 56) & 0xFF)) ?? .invalid | ||
} set { | ||
} | ||
set { | ||
rawValue = (rawValue & ~(0xFF << 56)) | (UInt(newValue.rawValue & 0xFF) << 56) | ||
} | ||
} | ||
|
||
init() { | ||
public init() { | ||
rawValue = 0 | ||
} | ||
|
||
init(_ value: UInt) { | ||
public init(_ value: UInt) { | ||
rawValue = value | ||
} | ||
|
||
init(accountID: UInt, instanceID: UInt = 1, type: SteamAccountType = .profile, universe: SteamUniverse = .steam) { | ||
public init(accountID: UInt, instanceID: UInt = 1, type: SteamAccountType = .profile, universe: SteamUniverse = .steam) { | ||
self.rawValue = 0 | ||
self.accountID = accountID | ||
self.instanceID = instanceID | ||
self.type = type | ||
self.universe = universe | ||
} | ||
|
||
init(clientID: Int, live: Bool, instanceID: UInt = 1, type: SteamAccountType = .profile, universe: SteamUniverse = .steam) { | ||
public init(clientID: Int, live: Bool, instanceID: UInt = 1, type: SteamAccountType = .profile, universe: SteamUniverse = .steam) { | ||
self.rawValue = 0 | ||
self.live = live | ||
self.clientID = clientID | ||
|
Oops, something went wrong.