Skip to main content

android/agent/
jar.rs

1//! The agent's dex jar and the version it must report.
2
3use std::fmt;
4use std::io;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8/// The agent version this driver deploys: `agent-android/VERSION`, which
9/// `build.sh` compiles into the agent's banner and hello, read through the
10/// crate's `AGENT_VERSION`, a link to it that the crate's package holds as a
11/// copy of the file.
12pub const AGENT_VERSION: &str = include_str!("../../AGENT_VERSION").trim_ascii();
13
14/// The name every agent build puts before its version in its hello.
15pub const AGENT_NAME: &str = "mobile-agent-android";
16
17/// Environment variable naming the jar to deploy instead of the default.
18pub const JAR_VAR: &str = "MOBILE_AGENT_JAR";
19
20/// Where `agent-android/build.sh` writes the jar in this source tree.
21pub fn default_jar_path() -> PathBuf {
22    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
23        .join("../../agent-android/dist/mobile-agent-android.jar")
24}
25
26/// The jar could not be read.
27#[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/// The agent's jar, read into memory, and the version its hello must name.
52#[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    /// The jar at `$MOBILE_AGENT_JAR`, else at [`default_jar_path`], expected
71    /// to report [`AGENT_VERSION`].
72    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    /// The jar at `path`, expected to report [`AGENT_VERSION`].
80    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    /// A jar (or a bare dex, which `app_process` also loads) held in memory
93    /// that reports `version`.
94    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    /// The version the agent reports, such as `0.1.0`.
103    pub fn version(&self) -> &str {
104        &self.version
105    }
106
107    /// The build: the first 12 hex digits of an xxh3 digest of the jar's
108    /// bytes. Two jars that report the same version but differ, such as the
109    /// agent before and after a change to its payload, are two builds.
110    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    /// The agent name and version its hello carries:
116    /// `mobile-agent-android <version>`.
117    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    /// Where the jar was read from, when it came from a file.
126    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}