1use std::fmt;
4use std::io;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8pub const AGENT_VERSION: &str = include_str!("../../AGENT_VERSION").trim_ascii();
13
14pub const AGENT_NAME: &str = "mobile-agent-android";
16
17pub const JAR_VAR: &str = "MOBILE_AGENT_JAR";
19
20pub fn default_jar_path() -> PathBuf {
22 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
23 .join("../../agent-android/dist/mobile-agent-android.jar")
24}
25
26#[derive(Debug)]
28pub struct JarError {
29 pub path: PathBuf,
30 pub source: io::Error,
31}
32
33impl fmt::Display for JarError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 write!(
36 f,
37 "cannot read the agent jar at {} ({}); build it with agent-android/build.sh or \
38 set {JAR_VAR} to its path",
39 self.path.display(),
40 self.source
41 )
42 }
43}
44
45impl std::error::Error for JarError {
46 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
47 Some(&self.source)
48 }
49}
50
51#[derive(Clone)]
53pub struct AgentJar {
54 bytes: Arc<[u8]>,
55 version: String,
56 path: Option<PathBuf>,
57}
58
59impl fmt::Debug for AgentJar {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.debug_struct("AgentJar")
62 .field("version", &self.version)
63 .field("bytes", &self.bytes.len())
64 .field("path", &self.path)
65 .finish()
66 }
67}
68
69impl AgentJar {
70 pub fn locate() -> Result<AgentJar, JarError> {
73 let path = std::env::var_os(JAR_VAR)
74 .filter(|value| !value.is_empty())
75 .map_or_else(default_jar_path, PathBuf::from);
76 AgentJar::from_path(&path)
77 }
78
79 pub fn from_path(path: &Path) -> Result<AgentJar, JarError> {
81 let bytes = std::fs::read(path).map_err(|source| JarError {
82 path: path.to_owned(),
83 source,
84 })?;
85 Ok(AgentJar {
86 bytes: bytes.into(),
87 version: AGENT_VERSION.to_owned(),
88 path: Some(path.to_owned()),
89 })
90 }
91
92 pub fn from_bytes(bytes: impl Into<Arc<[u8]>>, version: &str) -> AgentJar {
95 AgentJar {
96 bytes: bytes.into(),
97 version: version.to_owned(),
98 path: None,
99 }
100 }
101
102 pub fn version(&self) -> &str {
104 &self.version
105 }
106
107 pub fn build(&self) -> String {
111 let digest = xxhash_rust::xxh3::xxh3_64(&self.bytes);
112 format!("{digest:016x}")[..12].to_owned()
113 }
114
115 pub fn agent_name(&self) -> String {
118 format!("{AGENT_NAME} {}", self.version)
119 }
120
121 pub fn bytes(&self) -> &[u8] {
122 &self.bytes
123 }
124
125 pub fn path(&self) -> Option<&Path> {
127 self.path.as_deref()
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn the_expected_version_is_the_agents_version_file() {
137 let from_file = std::fs::read_to_string(concat!(
138 env!("CARGO_MANIFEST_DIR"),
139 "/../../agent-android/VERSION"
140 ))
141 .expect("agent-android/VERSION is readable");
142 assert_eq!(AGENT_VERSION, from_file.trim());
143 let jar = AgentJar::from_bytes(vec![1, 2, 3], AGENT_VERSION);
144 assert_eq!(
145 jar.agent_name(),
146 format!("mobile-agent-android {AGENT_VERSION}")
147 );
148 assert_eq!(jar.bytes(), &[1, 2, 3]);
149 assert_eq!(jar.path(), None);
150 }
151
152 #[test]
153 fn a_jars_build_is_a_digest_of_its_bytes() {
154 let a = AgentJar::from_bytes(vec![1, 2, 3], AGENT_VERSION);
155 let same = AgentJar::from_bytes(vec![1, 2, 3], "9.9.9");
156 let other = AgentJar::from_bytes(vec![1, 2, 4], AGENT_VERSION);
157 assert_eq!(a.build(), same.build(), "the version is not part of it");
158 assert_ne!(a.build(), other.build());
159 assert_eq!(a.build().len(), 12);
160 assert!(a.build().chars().all(|c| c.is_ascii_hexdigit()));
161 }
162
163 #[test]
164 fn a_missing_jar_says_how_to_build_it() {
165 let err =
166 AgentJar::from_path(Path::new("/nonexistent/mobile-agent-android.jar")).unwrap_err();
167 let text = err.to_string();
168 assert!(
169 text.contains("/nonexistent/mobile-agent-android.jar"),
170 "{text}"
171 );
172 assert!(text.contains("agent-android/build.sh"), "{text}");
173 assert!(text.contains(JAR_VAR), "{text}");
174 }
175
176 #[test]
177 fn a_jar_file_is_read_with_the_current_version() {
178 let dir = std::env::temp_dir().join(format!("android-jar-test-{}", std::process::id()));
179 std::fs::create_dir_all(&dir).unwrap();
180 let path = dir.join("agent.jar");
181 std::fs::write(&path, b"PK fake").unwrap();
182 let jar = AgentJar::from_path(&path).unwrap();
183 std::fs::remove_dir_all(&dir).unwrap();
184 assert_eq!(jar.bytes(), b"PK fake");
185 assert_eq!(jar.version(), AGENT_VERSION);
186 assert_eq!(jar.path(), Some(path.as_path()));
187 }
188
189 #[test]
190 fn the_default_path_is_the_build_output() {
191 let path = default_jar_path();
192 assert!(
193 path.ends_with("agent-android/dist/mobile-agent-android.jar"),
194 "{}",
195 path.display()
196 );
197 }
198}