1 | use serde::de::{self, Deserialize, DeserializeOwned, Deserializer}; |
2 | use serde_derive::Deserialize; |
3 | use std::env; |
4 | use std::error::Error; |
5 | use std::ffi::OsStr; |
6 | use std::fs; |
7 | use std::path::PathBuf; |
8 | |
9 | pub fn find() -> Option<Vec<String>> { |
10 | try_find().ok() |
11 | } |
12 | |
13 | struct Ignored; |
14 | |
15 | impl<E: Error> From<E> for Ignored { |
16 | fn from(_error: E) -> Self { |
17 | Ignored |
18 | } |
19 | } |
20 | |
21 | #[derive(Deserialize)] |
22 | struct Build { |
23 | #[serde(deserialize_with = "from_json" )] |
24 | features: Vec<String>, |
25 | } |
26 | |
27 | fn try_find() -> Result<Vec<String>, Ignored> { |
28 | // This will look something like: |
29 | // /path/to/crate_name/target/debug/deps/test_name-HASH |
30 | let test_binary = env::args_os().next().ok_or(Ignored)?; |
31 | |
32 | // The hash at the end is ascii so not lossy, rest of conversion doesn't |
33 | // matter. |
34 | let test_binary_lossy = test_binary.to_string_lossy(); |
35 | let hash_range = if cfg!(windows) { |
36 | // Trim ".exe" from the binary name for windows. |
37 | test_binary_lossy.len() - 21..test_binary_lossy.len() - 4 |
38 | } else { |
39 | test_binary_lossy.len() - 17..test_binary_lossy.len() |
40 | }; |
41 | let hash = test_binary_lossy.get(hash_range).ok_or(Ignored)?; |
42 | if !hash.starts_with('-' ) || !hash[1..].bytes().all(is_lower_hex_digit) { |
43 | return Err(Ignored); |
44 | } |
45 | |
46 | let binary_path = PathBuf::from(&test_binary); |
47 | |
48 | // Feature selection is saved in: |
49 | // /path/to/crate_name/target/debug/.fingerprint/*-HASH/*-HASH.json |
50 | let up = binary_path |
51 | .parent() |
52 | .ok_or(Ignored)? |
53 | .parent() |
54 | .ok_or(Ignored)?; |
55 | let fingerprint_dir = up.join(".fingerprint" ); |
56 | if !fingerprint_dir.is_dir() { |
57 | return Err(Ignored); |
58 | } |
59 | |
60 | let mut hash_matches = Vec::new(); |
61 | for entry in fingerprint_dir.read_dir()? { |
62 | let entry = entry?; |
63 | let is_dir = entry.file_type()?.is_dir(); |
64 | let matching_hash = entry.file_name().to_string_lossy().ends_with(hash); |
65 | if is_dir && matching_hash { |
66 | hash_matches.push(entry.path()); |
67 | } |
68 | } |
69 | |
70 | if hash_matches.len() != 1 { |
71 | return Err(Ignored); |
72 | } |
73 | |
74 | let mut json_matches = Vec::new(); |
75 | for entry in hash_matches[0].read_dir()? { |
76 | let entry = entry?; |
77 | let is_file = entry.file_type()?.is_file(); |
78 | let is_json = entry.path().extension() == Some(OsStr::new("json" )); |
79 | if is_file && is_json { |
80 | json_matches.push(entry.path()); |
81 | } |
82 | } |
83 | |
84 | if json_matches.len() != 1 { |
85 | return Err(Ignored); |
86 | } |
87 | |
88 | let build_json = fs::read_to_string(&json_matches[0])?; |
89 | let build: Build = serde_json::from_str(&build_json)?; |
90 | Ok(build.features) |
91 | } |
92 | |
93 | fn is_lower_hex_digit(byte: u8) -> bool { |
94 | byte >= b'0' && byte <= b'9' || byte >= b'a' && byte <= b'f' |
95 | } |
96 | |
97 | fn from_json<'de, T, D>(deserializer: D) -> Result<T, D::Error> |
98 | where |
99 | T: DeserializeOwned, |
100 | D: Deserializer<'de>, |
101 | { |
102 | let json = String::deserialize(deserializer)?; |
103 | serde_json::from_str(&json).map_err(de::Error::custom) |
104 | } |
105 | |