Skip to content

Commit

Permalink
feat: added usb stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
zleyyij committed Feb 7, 2024
1 parent 5698fd7 commit f1b75cb
Show file tree
Hide file tree
Showing 5 changed files with 25,874 additions and 12 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,28 @@ cargo test
By default, the application listens on port `3000`, although that can be configured by setting the `HWAPI_PORT` environment variable or by passing `-p`/`--port`.

## Endpoints
To interact with the cpu api, submit a `GET` request to `/api/cpus/?name=[CPU_NAME]`, where `[CPU_NAME]` is the HTTP encoded name of the cpu.

### CPU
To interact with the CPU API, submit a `GET` request to `/api/cpus/?name=[CPU_NAME]`, where `[CPU_NAME]` is the URL encoded name of the cpu.

This endpoint does not guarantee the correctness of the model returned, it will always attempt to return a model.

Here's an example curl request:
```
curl "http://localhost:3000/api/cpus/?name=Intel%20Core%20i9-9900k"
```

### USB
To interact with the USB API, submit a `GET` request to `/api/cpus/?name=[USB_IDENTIFIER_STRING]`, where `[USB_IDENTIFIER_STRING]` is a valid USB encoded string.

The endpoint will return a structure that looks like this:
```json
{
"vendor": "string | null",
"device": "string | null",
}
```

Here's an example curl request:
```
curl "http://127.0.0.1:3000/api/usbs/?identifier=USB%5CVID_1532%26PID_0084%26MI_03%5C6%2638C0FA5D%260%260003"
```
23 changes: 16 additions & 7 deletions src/cpu.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;

use levenshtein::levenshtein;
use log::debug;
use log::{debug, trace};
use serde::Serialize;
mod amd;
mod intel;
Expand Down Expand Up @@ -71,7 +71,7 @@ impl CpuCache {
attributes: HashMap::new(),
};
let mut best_score: usize = 10000;
println!("input model: {}", input_model);
trace!("input model: {}", input_model);
for cpu in cpus {
let model: String = find_model(&cpu.name).to_string();
// levenshtein distance is used to figure out how similar two strings are
Expand All @@ -81,7 +81,11 @@ impl CpuCache {
best_score = score;
best_fit = cpu.clone();

println!("Best fit of {} found, with a score of {}", best_fit.name, best_score);
trace!(
"Best fit of {} found, with a score of {}",
best_fit.name,
best_score
);
}
}
self.comparison_cache
Expand Down Expand Up @@ -130,7 +134,7 @@ fn find_model(input: &str) -> String {
return format!("PRO {}", best_fit);
}
}

best_fit.to_string()
}

Expand Down Expand Up @@ -180,13 +184,18 @@ mod tests {
("AMD Ryzen™ 5 5600", "AMD Ryzen 5 5600 6-Core Processor"),
("AMD Ryzen™ 5 2600", "AMD Ryzen 5 2600 Six-Core Processor"),
("AMD Ryzen™ 5 7600", "AMD Ryzen 5 7600 6-Core Processor"),
("AMD Ryzen™ 5 7530U", "AMD Ryzen 5 7530U with Radeon Graphics"),
(
"AMD Ryzen™ 5 7530U",
"AMD Ryzen 5 7530U with Radeon Graphics",
),
(
"Intel® Core™ i9-9900K Processor",
"Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz",
),
("Intel® Core™ i7 processor 14700K", "Intel(R) Core(TM) i7-14700K")

(
"Intel® Core™ i7 processor 14700K",
"Intel(R) Core(TM) i7-14700K",
),
];

for pairing in pairings {
Expand Down
34 changes: 31 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod cpu;
mod usb;

use axum::extract::Query;
use axum::http::HeaderValue;
Expand All @@ -12,6 +13,7 @@ use log::{Level, LevelFilter, Metadata, Record};
use serde::{Deserialize, Serialize};
use std::env;
use tower_http::cors::CorsLayer;
use usb::UsbCache;
/// https://docs.rs/log/latest/log/#implementing-a-logger
struct SimpleLogger;

Expand Down Expand Up @@ -54,6 +56,32 @@ static LOGGER: SimpleLogger = SimpleLogger;
#[derive(Clone)]
struct AppState {
pub cpu_cache: CpuCache,
pub usb_cache: UsbCache,
}

#[derive(Debug, Deserialize, Serialize)]
struct UsbQuery {
pub identifier: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct UsbResponse {
pub vendor: Option<String>,
pub device: Option<String>,
}

/// This handler accepts a `GET` request to `/api/usbs/?identifier.
/// It relies on a globally shared [AppState] to re-use the usb cache.
async fn get_usb_handler(
State(state): State<AppState>,
Query(query): Query<UsbQuery>,
) -> Json<UsbResponse> {
// TODO: update docs
let results = state.usb_cache.find(&query.identifier);
Json(UsbResponse {
vendor: results.0.map(|v| v.name),
device: results.1.map(|d| d.name),
})
}

#[derive(Debug, Deserialize, Serialize)]
Expand All @@ -65,11 +93,9 @@ struct CpuQuery {
/// It relies on a globally shared [AppState] to re-use the cpu cache, and responds to the request with a serialized [Cpu].
/// It will always attempt to find a cpu, and should always return a cpu. The correctness of the return value is not guaranteed.
async fn get_cpu_handler(
State(state): State<AppState>,
State(mut state): State<AppState>,
Query(query): Query<CpuQuery>,
) -> Json<Cpu> {
// just to get type annotations working
let mut state: AppState = state;
Json(state.cpu_cache.find(&query.name))
}

Expand All @@ -85,9 +111,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// create a new http router and register respective routes and handlers
let app = Router::new()
.route("/api/cpus/", get(get_cpu_handler))
.route("/api/usbs/", get(get_usb_handler))
.layer(CorsLayer::new().allow_origin("*".parse::<HeaderValue>().unwrap()))
.with_state(AppState {
cpu_cache: CpuCache::new(),
usb_cache: UsbCache::new(),
});

let mut port: String = String::from("3000");
Expand Down
224 changes: 224 additions & 0 deletions src/usb/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use nom::bytes::complete::{tag, take, take_until};
use nom::character::complete::char;
use nom::sequence::{delimited, preceded};
use nom::IResult;

// The input file was obtained from http://www.linux-usb.org/
// note: only vendors and devices are currently read from the file, there's extra crap at the bottom that might be useful
// This file contains one or two invalid utf 8 characters, so it's parsed slightly differently
const INPUT_FILE: &[u8] = include_bytes!("usb.ids.txt");

#[derive(Clone, Debug, PartialEq)]
pub struct Vendor {
pub id: String,
pub name: String,
pub devices: Vec<Device>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Device {
pub id: String,
pub name: String,
}

#[derive(Clone)]
pub struct UsbCache {
vendors: Vec<Vendor>,
}

impl UsbCache {
pub fn new() -> Self {
Self {
vendors: parse_usbs(),
}
}

/// Search the cache for the provided input string, returning the found device info, if it exists. If the `Option<Vendor>` is `None`,
/// you can assume that the device info will also be `None`.
pub fn find(&self, input: &str) -> (Option<Vendor>, Option<Device>) {
let found_stuff = parse_device_identifier(input).unwrap();
// first search for a vendor
let matching_vendor = self
.vendors
.iter()
.filter(|ven| ven.id == found_stuff.0)
.nth(0);
let mut matching_device: Option<Device> = None;
// if a vendor was found
if let Some(vendor) = matching_vendor {
matching_device = vendor
.devices
.iter()
.filter(|dev| dev.id == found_stuff.1)
.nth(0)
.cloned();
}
// if nothing was found
(matching_vendor.cloned(), matching_device)
}
}

/// This function searches the input string for a vendor id (vid) and product id (pid).
/// Input strings in the form of `USB\VID_1234&PID_5678\9479493` are assumed.
/// It returns a tuple, where the first value is the vendor id, and the second is the product id. This tuple contains substrings of the initial input string,
/// so handle lifetimes accordingly.
fn parse_device_identifier(
device_string: &str,
) -> Result<(&str, &str), nom::Err<nom::error::Error<&str>>> {
let vid_combinator = delimited(tag("USB\\VID_"), take(4 as u8), take(1 as u8))(device_string)?;
let pid_combinator = preceded(tag("PID_"), take(4 as u8))(vid_combinator.0)?;
// TODO: assert that the found values were actually valid hexadecimal strings
Ok((vid_combinator.1, pid_combinator.1))
}

fn parse_usbs() -> Vec<Vendor> {
// this is kind of awful, but there's an invalid utf 8 character at byte 703748,
// so we just stop before then, because it's past the section we care about
let file_as_str = std::str::from_utf8(&INPUT_FILE[0..703_748]).unwrap();
let header_combinator_output = read_header(file_as_str).unwrap();
let mut output: Vec<Vendor> = Vec::with_capacity(1024);

let mut iterated_output = read_section(header_combinator_output.0);
loop {
if let Ok(ref section_output) = iterated_output {
output.push(section_output.1.clone());
iterated_output = read_section(section_output.0);
} else {
break;
}
}
output
}

/// read the commented header up until the
/// start of the actual list. The `input` portion of the returned
/// tuple is the only part expected to be used, the header can be discarded
fn read_header(input: &str) -> IResult<&str, &str> {
// this is making the assumption that the list will always start with vendor 001
take_until("0001")(input)
}

/// This combinator reads a a vendor and all of the associated ids from the file
fn read_section(input: &str) -> IResult<&str, Vendor> {
// read the vendor id and vendor name
let vid_combinator_output = take(4 as u8)(input)?;
let vid = vid_combinator_output.1;
let vname_combinator =
delimited(tag(" "), take_until("\n"), char('\n'))(vid_combinator_output.0)?;
let vname = vname_combinator.1;
// read until the next line doesn't start with a tab
let mut devices: Vec<Device> = Vec::new();
let mut iterated_output = read_device_line(vname_combinator.0);
// this is so that we can actually return the leftover of the iterated parsing
let mut leftover = vname_combinator.0;
loop {
if let Ok(combinator_output) = iterated_output {
leftover = combinator_output.0;
devices.push(combinator_output.1);
iterated_output = read_device_line(combinator_output.0);
} else {
// Some lines have comments, handle those here, this is assuming the next line is indented
if leftover.starts_with("#") {
leftover = take_until("\t")(leftover)?.0;
iterated_output = read_device_line(leftover);
continue;
}
break;
}
}

Ok((
leftover,
Vendor {
id: vid.to_string(),
name: vname.to_string(),
devices,
},
))
}

/// This combinator reads a single device line from the input, if it is formed correctly
fn read_device_line(input: &str) -> IResult<&str, Device> {
let combinator_output = delimited(char('\t'), take_until("\n"), char('\n'))(input)?;
// read the device id and device name
let did_combinator_output = take(4 as u8)(combinator_output.1)?;
let dname = take(2 as u8)(did_combinator_output.0)?.0;
Ok((
combinator_output.0,
Device {
id: String::from(did_combinator_output.1),
name: String::from(dname),
},
))
}

#[cfg(test)]
mod tests {
use super::parse_device_identifier;
use super::{parse_usbs, read_section};
use super::{read_device_line, read_header, Device, Vendor};

#[test]
fn basic_parse_device_string() {
let mock_device_string = "USB\\VID_1234&PID_5678\\9479493";
assert_eq!(
parse_device_identifier(mock_device_string),
Ok(("1234", "5678"))
);
}

#[test]
fn basic_read_header() {
let mock_header = "#\tinterface interface_name\t\t<-- two tabs\n\n0001";
assert_eq!(
read_header(mock_header),
Ok(("0001", "#\tinterface interface_name\t\t<-- two tabs\n\n"))
);
}

#[test]
fn basic_read_section() {
let mock_section = "1234 vendor_name\n\t5678 device_name\n9123";
let expected_output = Vendor {
id: String::from("1234"),
name: String::from("vendor_name"),
devices: vec![Device {
id: String::from("5678"),
name: String::from("device_name"),
}],
};
assert_eq!(read_section(mock_section), Ok(("9123", expected_output)));
}

#[test]
fn read_section_no_devices() {
let mock_section = "1234 vendor_name\n5678";
let expected_output = Vendor {
id: String::from("1234"),
name: String::from("vendor_name"),
devices: vec![],
};
assert_eq!(read_section(mock_section), Ok(("5678", expected_output)));
}

#[test]
fn basic_read_device() {
// first make sure we can read a normal device without issue
let mock_device_entry = "\t1234 foo bar\n4567";
assert_eq!(
read_device_line(mock_device_entry),
Ok((
"4567",
Device {
id: String::from("1234"),
name: String::from("foo bar")
}
))
);
}

#[test]
fn basic_parse_usbs() {
parse_usbs();
}
}
Loading

0 comments on commit f1b75cb

Please sign in to comment.