Skip to main content

htb_cli/cli/
sherlocks.rs

1use clap::Subcommand;
2
3use crate::api::HtbClient;
4use crate::output::{self, OutputFormat};
5
6#[derive(Subcommand)]
7pub enum SherlockCommand {
8    /// List Sherlocks
9    List {
10        #[arg(long, help = "Filter by category")]
11        category: Option<String>,
12        #[arg(long, help = "Page number")]
13        page: Option<u32>,
14    },
15    /// List Sherlock categories
16    Categories,
17    /// Show Sherlock details
18    Info {
19        /// Sherlock slug
20        slug: String,
21    },
22    /// Download Sherlock files
23    Download {
24        /// Sherlock slug
25        slug: String,
26    },
27    /// Submit a task flag
28    Submit {
29        /// Sherlock ID
30        id: u64,
31        /// Task ID
32        task_id: u64,
33        /// The flag
34        flag: String,
35    },
36}
37
38pub async fn handle(
39    client: &HtbClient,
40    cmd: SherlockCommand,
41    format: OutputFormat,
42) -> anyhow::Result<()> {
43    match cmd {
44        SherlockCommand::List { category, page } => {
45            let result = client.sherlocks().list(page.unwrap_or(1), 100).await?;
46            let mut sherlocks = result.data;
47
48            if let Some(ref cat_filter) = category {
49                sherlocks.retain(|s| {
50                    s.category_name
51                        .as_ref()
52                        .is_some_and(|cn| cn.eq_ignore_ascii_case(cat_filter))
53                });
54            }
55
56            output::print_list(&sherlocks, format);
57            output::print_pagination(
58                result.meta.current_page,
59                result.meta.last_page,
60                result.meta.total,
61                format,
62            );
63        }
64
65        SherlockCommand::Categories => {
66            let categories = client.sherlocks().categories().await?;
67            output::print_list(&categories, format);
68        }
69
70        SherlockCommand::Info { slug } => {
71            let sherlock = client.sherlocks().info(&slug).await?;
72            let fields = vec![
73                ("ID", sherlock.id.to_string()),
74                ("Name", sherlock.name.clone()),
75                (
76                    "Difficulty",
77                    sherlock.difficulty.clone().unwrap_or_default(),
78                ),
79                (
80                    "Category",
81                    sherlock.category_name.clone().unwrap_or_default(),
82                ),
83                ("Solves", sherlock.solves.to_string()),
84                (
85                    "Rating",
86                    sherlock
87                        .rating
88                        .map(|r| format!("{r:.1}"))
89                        .unwrap_or_default(),
90                ),
91                (
92                    "Progress",
93                    sherlock
94                        .progress
95                        .map(|p| format!("{p}%"))
96                        .unwrap_or_else(|| "-".into()),
97                ),
98                ("State", sherlock.state.clone().unwrap_or_default()),
99            ];
100            output::print_detail(&sherlock, format, &fields);
101        }
102
103        SherlockCommand::Download { slug } => {
104            let sherlock = client.sherlocks().info(&slug).await?;
105            let url = client.sherlocks().download_link(sherlock.id).await?;
106            let bytes = client.challenges().download_file(&url).await?;
107            let filename = crate::sanitize_filename(&format!("{slug}.zip"), &format!("{slug}.zip"));
108            std::fs::write(&filename, &bytes)?;
109            output::print_message(&format!("Downloaded {} ({} bytes)", filename, bytes.len()));
110        }
111
112        SherlockCommand::Submit { id, task_id, flag } => {
113            let resp = client.sherlocks().submit_flag(id, task_id, &flag).await?;
114            output::print_message(&resp.message);
115        }
116    }
117    Ok(())
118}