Skip to content

Commit

Permalink
info: implement appinfo and packageinfo (needs binary vdf)
Browse files Browse the repository at this point in the history
  • Loading branch information
yretenai committed Dec 19, 2024
1 parent c43274d commit cc72ce1
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 40 deletions.
12 changes: 12 additions & 0 deletions Sources/Starvalve/BinaryExtensions.swift
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)
}
}
}
91 changes: 91 additions & 0 deletions Sources/Starvalve/CursoredData.swift
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
}
}
9 changes: 0 additions & 9 deletions Sources/Starvalve/Format/TextVDF.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@ extension CharacterSet {
}
}

public enum TextVDFError: Error {
case unexpectedToken
case unterminatedString
case truncated
case insertingIntoRootValue
case missingKey
case missingValue
}

class TextVDFLexer {
private let content: String
private var index: String.Index
Expand Down
20 changes: 20 additions & 0 deletions Sources/Starvalve/StarvalveErrors.swift
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
}
92 changes: 92 additions & 0 deletions Sources/Starvalve/SteamAppInfo.swift
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
}
}
46 changes: 26 additions & 20 deletions Sources/Starvalve/SteamID.swift
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"
Expand All @@ -23,7 +23,7 @@ public struct SteamID {
case .chat: "c"
case .peer: "p"
case .anonymousUser: "a"
}
}

return "[\(letter):\(universe.rawValue):\(accountID)]"
}
Expand All @@ -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
Expand Down
Loading

0 comments on commit cc72ce1

Please sign in to comment.