use crate::{error::SvmError, platform::Platform};
use reqwest::get;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, sync::LazyLock};
use url::Url;
const SOLC_RELEASES_URL: &str = "https://binaries.soliditylang.org";
const OLD_SOLC_RELEASES_DOWNLOAD_PREFIX: &str =
"https://raw.githubusercontent.com/crytic/solc/master/linux/amd64";
const OLD_VERSION_MAX: Version = Version::new(0, 4, 9);
const OLD_VERSION_MIN: Version = Version::new(0, 4, 0);
static OLD_SOLC_RELEASES: LazyLock<Releases> = LazyLock::new(|| {
serde_json::from_str(include_str!("../list/linux-arm64-old.json"))
.expect("could not parse list linux-arm64-old.json")
});
const LINUX_AARCH64_MIN: Version = Version::new(0, 5, 0);
const LINUX_AARCH64_BINARIES: Version = Version::new(0, 8, 31);
static LINUX_AARCH64_PLATFORM: &str = "linux-arm64";
static LINUX_AARCH64_URL_PREFIX: &str = "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64";
static LINUX_AARCH64_RELEASES_URL: &str = "https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64/list.json";
const MACOS_AARCH64_NATIVE: Version = Version::new(0, 8, 5);
const UNIVERSAL_MACOS_BINARIES: Version = Version::new(0, 8, 24);
static MACOS_AARCH64_URL_PREFIX: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/e4b80d33bc4d015b2fc3583e217fbf248b2014e1/macosx/aarch64";
static MACOS_AARCH64_RELEASES_URL: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/e4b80d33bc4d015b2fc3583e217fbf248b2014e1/macosx/aarch64/list.json";
const ANDROID_AARCH64_MIN: Version = Version::new(0, 8, 24);
static ANDROID_AARCH64_URL_PREFIX: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/ac6f303a04b38e7ec507ced511fd3ed7a605179f/android/aarch64";
static ANDROID_AARCH64_RELEASES_URL: &str = "https://raw.githubusercontent.com/alloy-rs/solc-builds/ac6f303a04b38e7ec507ced511fd3ed7a605179f/android/aarch64/list.json";
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Releases {
pub builds: Vec<BuildInfo>,
pub releases: BTreeMap<Version, String>,
}
impl Releases {
pub fn get_checksum(&self, v: &Version) -> Option<Vec<u8>> {
let matches = |build_info: &BuildInfo| {
build_info.version == Version::new(v.major, v.minor, v.patch)
&& build_info
.prerelease
.as_deref()
.unwrap_or("")
.eq_ignore_ascii_case(v.pre.as_str())
};
self.builds
.iter()
.find(|build_info| matches(build_info))
.map(|build_info| build_info.sha256.clone())
}
pub fn get_artifact(&self, version: &Version) -> Option<&String> {
if let Some(artifact) = self.releases.get(version) {
return Some(artifact);
}
if !version.pre.is_empty()
&& let Some(build_info) = self.builds.iter().find(|b| {
b.version == Version::new(version.major, version.minor, version.patch)
&& b.prerelease == Some(version.pre.to_string())
})
{
return build_info.path.as_ref();
}
None
}
pub fn into_versions(self) -> Vec<Version> {
let mut versions = self.releases.into_keys().collect::<Vec<_>>();
versions.sort_unstable();
versions
}
fn retain_versions(&mut self, mut pred: impl FnMut(&Version) -> bool) {
self.builds.retain(|build| pred(&build.version));
self.releases.retain(|version, _| pred(version));
}
fn extend(&mut self, other: Self) {
self.builds.extend(other.builds);
self.releases.extend(other.releases);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildInfo {
pub version: Version,
#[serde(with = "hex_string")]
pub sha256: Vec<u8>,
pub path: Option<String>,
pub prerelease: Option<String>,
}
mod hex_string {
use super::*;
use serde::{Deserializer, Serializer, de};
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
hex::decode(String::deserialize(deserializer)?).map_err(de::Error::custom)
}
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<[u8]>,
{
serializer.serialize_str(&hex::encode_prefixed(value))
}
}
#[cfg(feature = "blocking")]
pub fn blocking_all_releases(platform: Platform) -> Result<Releases, SvmError> {
match platform {
Platform::LinuxAarch64 => {
let mut releases =
reqwest::blocking::get(LINUX_AARCH64_RELEASES_URL)?.json::<Releases>()?;
releases.retain_versions(|v| *v < LINUX_AARCH64_BINARIES);
fix_build_prerelease(&mut releases);
let mut official = reqwest::blocking::get(format!(
"{SOLC_RELEASES_URL}/{LINUX_AARCH64_PLATFORM}/list.json",
))?
.json::<Releases>()?;
official.retain_versions(|v| *v >= LINUX_AARCH64_BINARIES);
releases.extend(official);
Ok(releases)
}
Platform::MacOsAarch64 => {
let native = reqwest::blocking::get(MACOS_AARCH64_RELEASES_URL)?.json::<Releases>()?;
let mut releases = reqwest::blocking::get(format!(
"{}/{}/list.json",
SOLC_RELEASES_URL,
Platform::MacOsAmd64,
))?
.json::<Releases>()?;
releases
.retain_versions(|v| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
releases.extend(native);
Ok(releases)
}
Platform::AndroidAarch64 => {
Ok(reqwest::blocking::get(ANDROID_AARCH64_RELEASES_URL)?.json::<Releases>()?)
}
Platform::WindowsAarch64 => {
let releases = reqwest::blocking::get(format!(
"{SOLC_RELEASES_URL}/{}/list.json",
Platform::WindowsAmd64
))?
.json::<Releases>()?;
Ok(unified_releases(releases, platform))
}
_ => {
let releases =
reqwest::blocking::get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))?
.json::<Releases>()?;
Ok(unified_releases(releases, platform))
}
}
}
pub async fn all_releases(platform: Platform) -> Result<Releases, SvmError> {
match platform {
Platform::LinuxAarch64 => {
let mut releases = get(LINUX_AARCH64_RELEASES_URL)
.await?
.json::<Releases>()
.await?;
releases.retain_versions(|v| *v < LINUX_AARCH64_BINARIES);
fix_build_prerelease(&mut releases);
let mut official = get(format!(
"{SOLC_RELEASES_URL}/{LINUX_AARCH64_PLATFORM}/list.json",
))
.await?
.json::<Releases>()
.await?;
official.retain_versions(|v| *v >= LINUX_AARCH64_BINARIES);
releases.extend(official);
Ok(releases)
}
Platform::MacOsAarch64 => {
let native = get(MACOS_AARCH64_RELEASES_URL)
.await?
.json::<Releases>()
.await?;
let mut releases = get(format!(
"{}/{}/list.json",
SOLC_RELEASES_URL,
Platform::MacOsAmd64,
))
.await?
.json::<Releases>()
.await?;
releases
.retain_versions(|v| *v < MACOS_AARCH64_NATIVE || *v > UNIVERSAL_MACOS_BINARIES);
releases.extend(native);
Ok(releases)
}
Platform::AndroidAarch64 => Ok(get(ANDROID_AARCH64_RELEASES_URL)
.await?
.json::<Releases>()
.await?),
Platform::WindowsAarch64 => {
let releases = get(format!(
"{SOLC_RELEASES_URL}/{}/list.json",
Platform::WindowsAmd64
))
.await?
.json::<Releases>()
.await?;
Ok(unified_releases(releases, platform))
}
_ => {
let releases = get(format!("{SOLC_RELEASES_URL}/{platform}/list.json"))
.await?
.json::<Releases>()
.await?;
Ok(unified_releases(releases, platform))
}
}
}
fn unified_releases(releases: Releases, platform: Platform) -> Releases {
if platform == Platform::LinuxAmd64 {
let mut all_releases = OLD_SOLC_RELEASES.clone();
all_releases.extend(releases);
all_releases
} else {
releases
}
}
fn fix_build_prerelease(releases: &mut Releases) {
for build in &mut releases.builds {
build.prerelease = if build.version.pre.is_empty() {
None
} else {
Some(build.version.pre.as_str().to_owned())
};
}
}
pub(crate) fn artifact_url(
platform: Platform,
version: &Version,
artifact: &str,
) -> Result<Url, SvmError> {
if platform == Platform::LinuxAmd64
&& *version <= OLD_VERSION_MAX
&& *version >= OLD_VERSION_MIN
{
return Ok(Url::parse(&format!(
"{OLD_SOLC_RELEASES_DOWNLOAD_PREFIX}/{artifact}"
))?);
}
if platform == Platform::LinuxAarch64 {
if *version >= LINUX_AARCH64_BINARIES {
return Ok(Url::parse(&format!(
"{SOLC_RELEASES_URL}/{LINUX_AARCH64_PLATFORM}/{artifact}"
))?);
} else if *version >= LINUX_AARCH64_MIN {
return Ok(Url::parse(&format!(
"{LINUX_AARCH64_URL_PREFIX}/{artifact}"
))?);
} else {
return Err(SvmError::UnsupportedVersion(
version.to_string(),
platform.to_string(),
));
}
}
if platform == Platform::MacOsAmd64 && *version < OLD_VERSION_MIN {
return Err(SvmError::UnsupportedVersion(
version.to_string(),
platform.to_string(),
));
}
if platform == Platform::MacOsAarch64 {
if *version >= MACOS_AARCH64_NATIVE && *version <= UNIVERSAL_MACOS_BINARIES {
return Ok(Url::parse(&format!(
"{MACOS_AARCH64_URL_PREFIX}/{artifact}"
))?);
} else {
return Ok(Url::parse(&format!(
"{}/{}/{}",
SOLC_RELEASES_URL,
Platform::MacOsAmd64,
artifact,
))?);
}
}
if platform == Platform::AndroidAarch64 {
if version.ge(&ANDROID_AARCH64_MIN) {
return Ok(Url::parse(&format!(
"{ANDROID_AARCH64_URL_PREFIX}/{artifact}"
))?);
} else {
return Err(SvmError::UnsupportedVersion(
version.to_string(),
platform.to_string(),
));
}
}
if platform == Platform::WindowsAarch64 {
return Ok(Url::parse(&format!(
"{SOLC_RELEASES_URL}/{}/{artifact}",
Platform::WindowsAmd64,
))?);
}
Ok(Url::parse(&format!(
"{SOLC_RELEASES_URL}/{platform}/{artifact}"
))?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_artifact_url() {
let version = Version::new(0, 5, 0);
let artifact = "solc-v0.5.0";
assert_eq!(
artifact_url(Platform::LinuxAarch64, &version, artifact).unwrap(),
Url::parse(&format!(
"https://raw.githubusercontent.com/nikitastupin/solc/2287d4326237172acf91ce42fd7ec18a67b7f512/linux/aarch64/{artifact}"
))
.unwrap(),
)
}
#[test]
fn test_old_releases_deser() {
assert_eq!(OLD_SOLC_RELEASES.releases.len(), 10);
assert_eq!(OLD_SOLC_RELEASES.builds.len(), 10);
}
#[tokio::test]
async fn test_macos_aarch64() {
let releases = all_releases(Platform::MacOsAarch64)
.await
.expect("could not fetch releases for macos-aarch64");
let rosetta = Version::new(0, 8, 4);
let native = MACOS_AARCH64_NATIVE;
let url1 = artifact_url(
Platform::MacOsAarch64,
&rosetta,
releases.get_artifact(&rosetta).unwrap(),
)
.expect("could not fetch artifact URL");
let url2 = artifact_url(
Platform::MacOsAarch64,
&native,
releases.get_artifact(&native).unwrap(),
)
.expect("could not fetch artifact URL");
assert!(url1.to_string().contains(SOLC_RELEASES_URL));
assert!(url2.to_string().contains(MACOS_AARCH64_URL_PREFIX));
}
#[tokio::test]
async fn test_all_releases_macos_amd64() {
assert!(all_releases(Platform::MacOsAmd64).await.is_ok());
}
#[tokio::test]
async fn test_all_releases_macos_aarch64() {
assert!(all_releases(Platform::MacOsAarch64).await.is_ok());
}
#[tokio::test]
async fn test_all_releases_linux_amd64() {
assert!(all_releases(Platform::LinuxAmd64).await.is_ok());
}
#[tokio::test]
async fn test_all_releases_linux_aarch64() {
let releases = all_releases(Platform::LinuxAarch64)
.await
.expect("could not fetch releases for linux-aarch64");
let thirdparty = LINUX_AARCH64_MIN;
let url1 = artifact_url(
Platform::LinuxAarch64,
&thirdparty,
releases.get_artifact(&thirdparty).unwrap(),
)
.expect("could not fetch artifact URL");
let prerelease = Version::parse("0.8.31-pre.1").expect("failed to parse version");
let url2 = artifact_url(
Platform::LinuxAarch64,
&prerelease,
releases.get_artifact(&prerelease).unwrap(),
)
.expect("could not fetch artifact URL");
let official = LINUX_AARCH64_BINARIES;
let url3 = artifact_url(
Platform::LinuxAarch64,
&official,
releases.get_artifact(&official).unwrap(),
)
.expect("could not fetch artifact URL");
assert!(url1.to_string().contains(LINUX_AARCH64_URL_PREFIX));
assert!(url2.to_string().contains(LINUX_AARCH64_URL_PREFIX));
assert!(url3.to_string().contains(SOLC_RELEASES_URL));
}
#[tokio::test]
async fn test_all_releases_windows_aarch64() {
let releases = all_releases(Platform::WindowsAarch64).await;
assert!(releases.is_ok());
let releases = releases.unwrap();
let latest = releases.releases.keys().max().unwrap();
let artifact = releases.get_artifact(latest).unwrap();
let url = artifact_url(Platform::WindowsAarch64, latest, artifact).unwrap();
assert!(url.to_string().contains("windows-amd64"));
}
#[tokio::test]
async fn releases_roundtrip() {
let releases = all_releases(Platform::LinuxAmd64).await.unwrap();
let s = serde_json::to_string(&releases).unwrap();
let de_releases: Releases = serde_json::from_str(&s).unwrap();
assert_eq!(releases, de_releases);
}
}