1use std::{io::Read, sync::LazyLock};
4
5use anyhow::{Result, anyhow, bail};
6use bytes::Buf;
7use futures::executor::block_on;
8use libabbs::apml::{
9	ApmlContext,
10	value::{array::StringArray, union::Union},
11};
12use log::{debug, info, warn};
13use opendal::{
14	Operator,
15	layers::RetryLayer,
16	services::{Github, Memory},
17};
18use regex::Regex;
19use reqwest::ClientBuilder;
20use tempfile::tempfile;
21
22pub mod pypi;
23
24static REGEX_GH_URL: LazyLock<Regex> = LazyLock::new(|| {
25	Regex::new(
26		r##"http(s|)://github\.com/(?<user>[a-zA-Z_-]+)/(?<repo>[a-zA-Z_-]+)"##,
27	)
28	.unwrap()
29});
30
31pub async fn open(ctx: ApmlContext) -> Result<Operator> {
33	let srcs = ctx.read("SRCS").into_string();
34	let version = ctx.read("VER").into_string();
35	let srcs = StringArray::from(srcs);
36
37	if srcs.len() == 1 {
38		let src = srcs[0].clone();
39		let un = if src.starts_with("https://") || src.starts_with("http://") {
40			Union::try_from(format!("tbl::{src}").as_str())?
41		} else {
42			Union::try_from(src.as_str())?
43		};
44
45		match un.tag.as_str() {
46			"tarball" | "tbl" => {
47				if let Some(url) = un.argument {
48					if let Some(fs) = find_alt_fs(&url).await? {
49						return Ok(fs);
50					}
51					return fetch_tarball(url).await;
52				}
53			}
54			"git" => {
55				if let Some(url) = un.argument
56					&& let Some(fs) = find_alt_fs(&url).await? {
57						return Ok(fs);
58					}
59			}
60			"pypi" => {
61				if let Some(package) = un.argument {
62					return pypi::load(
63						&package,
64						un.properties.get("version").unwrap_or(&version),
65					)
66					.await;
67				}
68			}
69			_ => {
70				warn!("unsupported source type: {}", un.tag);
71			}
72		}
73		warn!("failed to recognize source provider: {}", &src);
74	} else {
75		warn!("multiple sources are not supported yet");
76	}
77	Ok(Operator::new(Memory::default())?.finish())
78}
79
80async fn find_alt_fs(url: &str) -> Result<Option<Operator>> {
86	if let Some(cap) = REGEX_GH_URL.captures(url) {
87		let owner = &cap["user"];
88		let repo = &cap["repo"];
89		debug!(
90			"recognized GitHub repository {owner}/{repo} from {url}"
91		);
92		Ok(Some(
93			Operator::new(Github::default().owner(owner).repo(repo))?
94				.layer(RetryLayer::new())
95				.finish(),
96		))
97	} else {
98		Ok(None)
99	}
100}
101
102fn http_client() -> Result<reqwest::Client> {
103	Ok(ClientBuilder::new()
104		.user_agent(format!(
105			"libpfu/{} (https://github.com/AOSC-Dev/pfu)",
106			env!("CARGO_PKG_VERSION")
107		))
108		.build()?)
109}
110
111async fn fetch_tarball(url: String) -> Result<Operator> {
113	info!("Downloading tarball: {url}");
114	let client = http_client()?;
115	let resp = client
116		.execute(client.get(&url).build()?)
117		.await?
118		.error_for_status()?;
119
120	let reader = resp.bytes().await?.reader();
121	let fs = block_on(async { load_compressed_tarball(&url, reader).await })?;
122	Ok(fs)
123}
124
125async fn load_compressed_tarball(
127	name: &str,
128	reader: impl Read,
129) -> Result<Operator> {
130	if name.ends_with(".tar") {
131		debug!("Recognized bare tarball");
132		load_tarball(reader).await
133	} else if name.ends_with(".tar.gz")
134		|| name.ends_with(".tar.gzip")
135		|| name.ends_with(".tgz")
136	{
137		debug!("Recognized tarball + gzip");
138		let reader = flate2::read::GzDecoder::new(reader);
139		load_tarball(reader).await
140	} else if name.ends_with(".tar.xz") {
141		debug!("Recognized tarball + XZ");
142		let reader = xz2::read::XzDecoder::new(reader);
143		load_tarball(reader).await
144	} else if name.ends_with(".tar.zst") || name.ends_with(".tar.zstd") {
145		debug!("Recognized tarball + zstd");
146		let reader = zstd::Decoder::new(reader)?;
147		load_tarball(reader).await
148	} else if name.ends_with(".tar.bz")
149		|| name.ends_with(".tar.bz2")
150		|| name.ends_with(".tar.bzip")
151	{
152		debug!("Recognized tarball + bz");
153		let reader = bzip2::read::BzDecoder::new(reader);
154		load_tarball(reader).await
155	} else {
156		bail!("unsupported archive type")
157	}
158}
159
160async fn load_tarball(mut reader: impl Read) -> Result<Operator> {
162	let fs = Operator::new(Memory::default())?.finish();
163
164	let mut temp = tempfile()?;
165	std::io::copy(&mut reader, &mut temp)?;
166
167	let mut tar = tar::Archive::new(temp);
168	for entry in tar.entries()? {
169		let mut entry = entry?;
170		if entry.header().entry_type() == tar::EntryType::Directory {
171			fs.create_dir(
172				entry
173					.path()?
174					.to_str()
175					.ok_or_else(|| anyhow!("invalid dir name in tarball"))?,
176			)
177			.await?;
178		} else {
179			let path = entry.path()?.to_path_buf();
180			if let Some(parent) = path.parent() {
181				fs.create_dir(
182					parent.to_str().ok_or_else(|| {
183						anyhow!("invalid dir name in tarball")
184					})?,
185				)
186				.await?;
187			}
188			let mut buf = Vec::with_capacity(entry.size() as usize);
189			entry.read_to_end(&mut buf)?;
190			fs.write(
191				path.to_str()
192					.ok_or_else(|| anyhow!("invalid filename in tarball"))?,
193				buf,
194			)
195			.await?;
196		}
197	}
198
199	Ok(fs)
200}