Skip to main content

htb_cli/models/
machine.rs

1use serde::{Deserialize, Serialize};
2
3use super::deserialize_bool_or_null;
4use crate::output::Tabular;
5
6#[derive(Debug, Deserialize, Serialize)]
7#[serde(rename_all = "camelCase")]
8pub struct Machine {
9    pub id: u64,
10    pub name: String,
11    pub os: String,
12    #[serde(default)]
13    pub points: u32,
14    #[serde(default)]
15    pub rating: Option<f64>,
16    #[serde(default)]
17    pub difficulty: Option<u32>,
18    #[serde(default)]
19    pub difficulty_text: Option<String>,
20    #[serde(default)]
21    pub user_owns_count: u32,
22    #[serde(default)]
23    pub root_owns_count: u32,
24    #[serde(default)]
25    pub auth_user_in_user_owns: Option<bool>,
26    #[serde(default)]
27    pub auth_user_in_root_owns: Option<bool>,
28    #[serde(default, deserialize_with = "deserialize_bool_or_null")]
29    pub todo: bool,
30    #[serde(default)]
31    pub state: Option<String>,
32    #[serde(default)]
33    pub ip: Option<String>,
34    #[serde(default, deserialize_with = "deserialize_bool_or_null")]
35    pub free: bool,
36    #[serde(default)]
37    pub release_date: Option<String>,
38    #[serde(default)]
39    pub play_info: Option<MachinePlayInfo>,
40    #[serde(default)]
41    pub first_creator: Option<Creator>,
42    #[serde(default)]
43    pub labels: Vec<Label>,
44}
45
46#[derive(Debug, Deserialize, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct MachinePlayInfo {
49    #[serde(default)]
50    pub is_spawned: Option<bool>,
51    #[serde(default)]
52    pub is_spawning: Option<bool>,
53    #[serde(default, deserialize_with = "deserialize_bool_or_null")]
54    pub is_active: bool,
55    #[serde(default)]
56    pub active_player_count: Option<u32>,
57    #[serde(default)]
58    pub expires_at: Option<String>,
59}
60
61#[derive(Debug, Deserialize, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Creator {
64    pub id: u64,
65    pub name: String,
66    #[serde(default)]
67    pub avatar: Option<String>,
68}
69
70#[derive(Debug, Deserialize, Serialize)]
71pub struct Label {
72    pub color: String,
73    pub name: String,
74}
75
76#[derive(Debug, Deserialize)]
77pub struct ActiveVmResponse {
78    pub info: Option<ActiveVmInfo>,
79}
80
81#[derive(Debug, Deserialize, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ActiveVmInfo {
84    pub id: u64,
85    pub name: String,
86    #[serde(default)]
87    pub avatar: Option<String>,
88    #[serde(default)]
89    pub expires_at: Option<String>,
90    #[serde(default, rename = "type")]
91    pub vm_type: Option<String>,
92    #[serde(default)]
93    pub lab_server: Option<String>,
94}
95
96#[derive(Debug, Deserialize)]
97pub struct MachineProfileResponse {
98    pub info: Machine,
99}
100
101#[derive(Debug, Deserialize)]
102pub struct TodoListResponse {
103    pub info: Vec<Machine>,
104}
105
106impl Tabular for Machine {
107    fn headers() -> Vec<&'static str> {
108        vec![
109            "ID",
110            "Name",
111            "OS",
112            "Difficulty",
113            "Rating",
114            "Points",
115            "State",
116            "User",
117            "Root",
118        ]
119    }
120
121    fn row(&self) -> Vec<String> {
122        let user_own = match self.auth_user_in_user_owns {
123            Some(true) => "✓",
124            _ => "",
125        };
126        let root_own = match self.auth_user_in_root_owns {
127            Some(true) => "✓",
128            _ => "",
129        };
130        vec![
131            self.id.to_string(),
132            self.name.clone(),
133            self.os.clone(),
134            self.difficulty_text.clone().unwrap_or_default(),
135            self.rating.map(|r| format!("{r:.1}")).unwrap_or_default(),
136            self.points.to_string(),
137            self.state.clone().unwrap_or_default(),
138            user_own.to_string(),
139            root_own.to_string(),
140        ]
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::models::Paginated;
148
149    #[test]
150    fn deserialize_machines_list() {
151        let json = include_str!("../../tests/fixtures/v5-machines-list.json");
152        let result: Paginated<Machine> = serde_json::from_str(json).unwrap();
153        assert_eq!(result.data.len(), 15);
154        let cap = &result.data[0];
155        assert_eq!(cap.name, "Cap");
156        assert_eq!(cap.os, "Linux");
157        assert_eq!(cap.difficulty_text.as_deref(), Some("Easy"));
158        assert_eq!(cap.auth_user_in_user_owns, Some(true));
159    }
160
161    #[test]
162    fn deserialize_machines_list_live() {
163        let json = include_str!("../../tests/fixtures/v5-machines-list-live.json");
164        let result: Paginated<Machine> = serde_json::from_str(json).unwrap();
165        assert_eq!(result.data.len(), 3);
166        assert_eq!(result.data[0].name, "TestMachine-1");
167        assert_eq!(result.data[0].os, "Linux");
168        assert_eq!(result.data[0].difficulty, Some(28));
169        assert!(result.data[0].free);
170        assert_eq!(result.data[0].state.as_deref(), Some("retired_free"));
171        assert!(result.data[0].labels.iter().any(|l| l.name == "STAFF PICK"));
172        assert!(result.data[0].first_creator.is_some());
173    }
174
175    #[test]
176    fn deserialize_active_vm_null() {
177        let json = include_str!("../../tests/fixtures/v5-virtual-machine-active.json");
178        let result: ActiveVmResponse = serde_json::from_str(json).unwrap();
179        assert!(result.info.is_none());
180    }
181}