1use std::collections::BTreeMap;
9use std::io::Write;
10use std::path::PathBuf;
11
12use andysu_core::payload::{Descriptor, Entry, Needs};
13
14use crate::apk::Apk;
15use crate::{write, R};
16
17#[derive(clap::Args, Clone, Debug)]
18pub struct SplitArgs {
19 pub apk: PathBuf,
21 #[arg(long, short)]
23 pub out: PathBuf,
24 #[arg(long)]
26 pub app: Option<String>,
27 #[arg(long)]
29 pub version: Option<String>,
30 #[arg(long)]
32 pub entry_class: Option<String>,
33 #[arg(long)]
35 pub entry_native: Option<String>,
36 #[arg(long, default_value_t = 1)]
38 pub host_api: u32,
39 #[arg(long)]
41 pub abi: Vec<String>,
42 #[arg(long, requires = "config")]
44 pub host_template: Option<PathBuf>,
45 #[arg(long)]
47 pub config: Option<PathBuf>,
48 #[arg(long)]
50 pub keystore: Option<PathBuf>,
51 #[arg(long)]
53 pub ks_pass: Option<String>,
54 #[arg(long)]
55 pub key_alias: Option<String>,
56 #[arg(long)]
58 pub cleartext: bool,
59 #[arg(long)]
62 pub transparent: bool,
63}
64
65#[derive(serde::Serialize, Debug, Default)]
66pub struct SplitReport {
67 pub payload: String,
68 pub app: String,
69 pub version: String,
70 pub dex: Vec<String>,
71 pub native: BTreeMap<String, Vec<String>>,
72 pub assets: usize,
73 pub java_resources: usize,
74 pub manifest: crate::apk::ManifestInfo,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub host: Option<crate::hostapk::HostReport>,
77}
78
79fn is_java_resource(n: &str) -> bool {
80 !(n.ends_with('/')
81 || n == "AndroidManifest.xml"
82 || n == "resources.arsc"
83 || n.starts_with("res/")
84 || n.starts_with("assets/")
85 || n.starts_with("lib/")
86 || n.starts_with("META-INF/")
87 && (n.ends_with(".SF")
88 || n.ends_with(".RSA")
89 || n.ends_with(".EC")
90 || n.ends_with(".DSA")
91 || n.ends_with("MANIFEST.MF"))
92 || (n.starts_with("classes") && n.ends_with(".dex") && !n.contains('/'))
93 || n.starts_with("stamp-cert-sha256"))
94}
95
96pub fn split(a: &SplitArgs) -> R<SplitReport> {
97 let mut apk = Apk::open(&a.apk)?;
98 let mi = apk.manifest()?;
99 let payload = a.out.join("payload");
100 if payload.exists() && std::fs::read_dir(&payload).map(|mut d| d.next().is_some()).unwrap_or(false) {
101 return Err(format!("{} is not empty", payload.display()));
102 }
103 let mut rep = SplitReport { payload: payload.display().to_string(), ..Default::default() };
104 let mut javares: Vec<(String, Vec<u8>)> = vec![];
105 for n in apk.names() {
106 if n.starts_with("classes") && n.ends_with(".dex") && !n.contains('/') {
107 write(&payload.join("dex").join(&n), &apk.read(&n)?)?;
108 rep.dex.push(format!("dex/{n}"));
109 } else if let Some(rest) = n.strip_prefix("lib/") {
110 let parts: Vec<&str> = rest.split('/').collect();
111 if parts.len() == 2
112 && parts[1].ends_with(".so")
113 && (a.abi.is_empty() || a.abi.iter().any(|x| x == parts[0]))
114 {
115 write(&payload.join(&n), &apk.read(&n)?)?;
116 rep.native.entry(parts[0].to_string()).or_default().push(parts[1].to_string());
117 }
118 } else if let Some(rest) = n.strip_prefix("assets/") {
119 if rest.is_empty() || n.ends_with('/') || rest.starts_with("andysu/") || rest.starts_with("andysu-factory/")
120 {
121 continue;
122 }
123 andysu_core::manifest::check_rel_path(&n).map_err(|e| e.to_string())?;
124 write(&payload.join(&n), &apk.read(&n)?)?;
125 rep.assets += 1;
126 } else if is_java_resource(&n) {
127 andysu_core::manifest::check_rel_path(&n).map_err(|e| e.to_string())?;
128 javares.push((n.clone(), apk.read(&n)?));
129 }
130 }
131 rep.dex.sort_by_key(|p| p.trim_start_matches("dex/classes").trim_end_matches(".dex").parse::<u32>().unwrap_or(1));
133 let mut dex = rep.dex.clone();
134 if !javares.is_empty() {
135 let mut buf = std::io::Cursor::new(Vec::new());
136 {
137 let mut w = zip::ZipWriter::new(&mut buf);
138 let o = zip::write::SimpleFileOptions::default().last_modified_time(zip::DateTime::default());
139 for (n, d) in &javares {
140 w.start_file(n.as_str(), o).map_err(|e| e.to_string())?;
141 w.write_all(d).map_err(|e| e.to_string())?;
142 }
143 w.finish().map_err(|e| e.to_string())?;
144 }
145 write(&payload.join("dex/resources.jar"), &buf.into_inner())?;
146 dex.push("dex/resources.jar".into());
147 rep.java_resources = javares.len();
148 }
149 let entry = match (&a.entry_class, &a.entry_native) {
150 _ if a.transparent => Entry { transparent: true, dex_class: a.entry_class.clone(), ..Default::default() },
151 (Some(c), None) => Entry { dex_class: Some(c.clone()), ..Default::default() },
152 (None, Some(n)) => {
153 let (lib, sym) = n.split_once(':').ok_or("--entry-native wants LIB:SYMBOL")?;
154 Entry {
155 native_lib: Some(lib.trim_end_matches(".so").into()),
156 symbol: Some(sym.into()),
157 ..Default::default()
158 }
159 }
160 _ => return Err("give exactly one of --entry-class or --entry-native".into()),
161 };
162 let app = a.app.clone().unwrap_or_else(|| mi.package.clone());
163 let version =
164 a.version.clone().or_else(|| mi.version_name.clone()).ok_or("no --version and no versionName in the APK")?;
165 let own =
166 |c: &str| !c.starts_with("com.andysu.host.") && !c.starts_with("androidx.") && !c.starts_with("com.google.");
167 let desc = Descriptor {
168 schema: 1,
169 app: app.clone(),
170 version: version.clone(),
171 host_api_min: a.host_api,
172 entry,
173 dex,
174 native_dir: None,
175 needs: Needs {
176 permissions: mi.permissions.clone(),
177 activities: mi.activities.iter().filter(|c| own(c)).count() as u32,
178 services: mi.services.iter().filter(|c| own(c)).count() as u32,
179 receivers: mi.receivers.iter().filter(|c| own(c)).count() as u32,
180 providers: mi.providers.iter().filter(|c| own(c)).count() as u32,
181 features: mi.features.clone(),
182 },
183 native: BTreeMap::new(),
184 };
185 desc.validate().map_err(|e| e.to_string())?;
186 let mut dj = serde_json::to_vec_pretty(&desc).unwrap();
187 dj.push(b'\n');
188 write(&payload.join("andysu-payload.json"), &dj)?;
189 rep.app = app;
190 rep.version = version;
191 rep.manifest = mi;
192 if let (Some(tpl), Some(cfg)) = (&a.host_template, &a.config) {
193 let site = a.out.join("factory-site");
195 let slot = a.out.join("factory");
196 let _ = std::fs::remove_dir_all(&site);
197 let o = crate::pack::Options {
198 payload: payload.clone(),
199 site,
200 channel: "stable".into(),
201 id: None,
202 host: None,
203 hot: vec![],
204 archive: vec![],
205 deltas: 0,
206 delta_format: andysu_core::manifest::PatchFormat::Zstd,
207 base_url: None,
208 require_16k: false,
209 allow_shell: false,
210 emit_slot: Some(slot.clone()),
211 no_rename: a.transparent,
212 };
213 crate::pack::pack(&o)?;
214 let ho = crate::hostapk::HostOptions {
215 template: tpl.clone(),
216 config: cfg.clone(),
217 factory: slot,
218 out: a.out.join("host.apk"),
219 keystore: a.keystore.clone(),
220 ks_pass: a.ks_pass.clone(),
221 key_alias: a.key_alias.clone(),
222 cleartext: a.cleartext,
223 transparent: a.transparent,
224 payload_has_assets: rep.assets > 0,
225 };
226 rep.host = Some(crate::hostapk::build(&a.apk, &ho)?);
227 }
228 Ok(rep)
229}