diff --git a/Cargo.lock b/Cargo.lock index 36eac55..d0e6e4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "anstream" version = "0.6.15" @@ -38,7 +44,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -48,7 +54,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -57,6 +63,27 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "build-edk2" version = "0.0.0" @@ -65,6 +92,27 @@ dependencies = [ "clap", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.20" @@ -111,21 +159,213 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "ovmf-prebuilt" -version = "0.2.0" +version = "0.3.0" +dependencies = [ + "log", + "lzma-rs", + "sha2", + "tar", + "ureq", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "proc-macro2" @@ -145,12 +385,110 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.79" @@ -162,18 +500,119 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -183,6 +622,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -246,3 +694,20 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/ovmf-prebuilt/Cargo.toml b/ovmf-prebuilt/Cargo.toml index c0bf230..11b95c4 100644 --- a/ovmf-prebuilt/Cargo.toml +++ b/ovmf-prebuilt/Cargo.toml @@ -1,8 +1,14 @@ [package] name = "ovmf-prebuilt" -version = "0.2.0" +version = "0.3.0" edition.workspace = true categories = ["development-tools"] keywords = ["edk2", "firmware", "ovmf", "prebuilt"] +license = "MIT OR Apache-2.0" [dependencies] +log = "0.4" +lzma-rs = "0.3" +sha2 = "0.10" +tar = "0.4" +ureq = "2.0" diff --git a/ovmf-prebuilt/src/error.rs b/ovmf-prebuilt/src/error.rs new file mode 100644 index 0000000..69d6eb5 --- /dev/null +++ b/ovmf-prebuilt/src/error.rs @@ -0,0 +1,61 @@ +use std::fmt::{self, Display, Formatter}; +use std::io; + +/// Cache or fetch error. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// Hash of the downloaded file does not match the expected value. + HashMismatch { + /// Expected hash. + expected: String, + /// Actual hash. + actual: String, + }, + + /// Failed to write the hash file. + HashWrite(io::Error), + + /// Remote request failed. + Request(Box), + + /// Download failed. + Download(io::Error), + + /// Tarball decompression failed. + Decompress(lzma_rs::error::Error), + + /// Tarball extraction failed. + Extract(io::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::HashMismatch { expected, actual } => write!( + f, + "file hash {actual} does not match expected hash {expected}" + ), + // `source` returns non-None for these variants, so do not + // format the inner error. + Self::HashWrite(_) => write!(f, "failed to write hash file"), + Self::Request(_) => write!(f, "remote request failed"), + Self::Download(_) => write!(f, "download failed"), + Self::Decompress(_) => write!(f, "tarball decompression failed"), + Self::Extract(_) => write!(f, "tarball extraction failed"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::HashMismatch { .. } => None, + Self::HashWrite(err) => Some(err), + Self::Request(err) => Some(err), + Self::Download(err) => Some(err), + Self::Extract(err) => Some(err), + Self::Decompress(err) => Some(err), + } + } +} diff --git a/ovmf-prebuilt/src/fetch.rs b/ovmf-prebuilt/src/fetch.rs new file mode 100644 index 0000000..c689702 --- /dev/null +++ b/ovmf-prebuilt/src/fetch.rs @@ -0,0 +1,120 @@ +use crate::{Error, Source}; +use log::info; +use sha2::{Digest, Sha256}; +use std::fs; +use std::io::{self, Cursor, Read}; +use std::path::{Path, PathBuf}; +use tar::Archive; +use ureq::Agent; + +/// User-Agent header to send with download requests. +const USER_AGENT: &str = "https://github.com/rust-osdev/ovmf-prebuilt"; + +/// Maximum number of bytes to download (10 MiB). +const MAX_DOWNLOAD_SIZE_IN_BYTES: usize = 10 * 1024 * 1024; + +/// Update the local cache. Does nothing if the cache is already up to date. +pub(crate) fn update_cache(source: Source, prebuilt_dir: &Path) -> Result<(), Error> { + let hash_path = prebuilt_dir.join("sha256"); + + // Check if the hash file already has the expected hash in it. If so, assume + // that we've already got the correct prebuilt downloaded and unpacked. + if let Ok(current_hash) = fs::read_to_string(&hash_path) { + if current_hash == source.sha256 { + return Ok(()); + } + } + + let base_url = "https://github.com/rust-osdev/ovmf-prebuilt/releases/download"; + let url = format!( + "{base_url}/{release}/{release}-bin.tar.xz", + release = source.tag + ); + + let data = download_url(&url)?; + + // Validate the hash. + let actual_hash = format!("{:x}", Sha256::digest(&data)); + if actual_hash != source.sha256 { + return Err(Error::HashMismatch { + actual: actual_hash, + expected: source.sha256.to_owned(), + }); + } + + // Unpack the tarball. + let decompressed = decompress(&data)?; + + // Clear out the existing prebuilt dir, if present. + let _ = fs::remove_dir_all(prebuilt_dir); + + // Extract the files. + extract(&decompressed, prebuilt_dir).map_err(Error::Extract)?; + + // Write out the hash file. When we upgrade to a new release of + // ovmf-prebuilt, the hash will no longer match, triggering a fresh + // download. + fs::write(&hash_path, actual_hash).map_err(Error::HashWrite)?; + + Ok(()) +} + +/// Download `url` and return the raw data. +fn download_url(url: &str) -> Result, Error> { + let agent: Agent = ureq::AgentBuilder::new().user_agent(USER_AGENT).build(); + + // Download the file. + info!("downloading {url}"); + let resp = agent + .get(url) + .call() + .map_err(|err| Error::Request(Box::new(err)))?; + let mut data = Vec::with_capacity(MAX_DOWNLOAD_SIZE_IN_BYTES); + resp.into_reader() + // Limit the size of the download. + .take(MAX_DOWNLOAD_SIZE_IN_BYTES.try_into().unwrap()) + .read_to_end(&mut data) + .map_err(Error::Download)?; + info!("received {} bytes", data.len()); + + Ok(data) +} + +fn decompress(data: &[u8]) -> Result, Error> { + info!("decompressing tarball"); + let mut decompressed = Vec::new(); + let mut compressed = Cursor::new(data); + lzma_rs::xz_decompress(&mut compressed, &mut decompressed).map_err(Error::Decompress)?; + Ok(decompressed) +} + +/// Extract the tarball's files into `prebuilt_dir`. +/// +/// `tarball_data` is raw decompressed tar data. +fn extract(tarball_data: &[u8], prebuilt_dir: &Path) -> Result<(), io::Error> { + let cursor = Cursor::new(tarball_data); + let mut archive = Archive::new(cursor); + + // Extract each file entry. + for entry in archive.entries()? { + let mut entry = entry?; + + // Skip directories. + if entry.size() == 0 { + continue; + } + + let path = entry.path()?; + // Strip the leading directory, which is the release name. + let path: PathBuf = path.components().skip(1).collect(); + + let dir = path.parent().unwrap(); + let dst_dir = prebuilt_dir.join(dir); + let dst_path = prebuilt_dir.join(path); + info!("unpacking to {}", dst_path.display()); + fs::create_dir_all(dst_dir)?; + entry.unpack(dst_path)?; + } + + Ok(()) +} diff --git a/ovmf-prebuilt/src/lib.rs b/ovmf-prebuilt/src/lib.rs index 8b13789..c2a97df 100644 --- a/ovmf-prebuilt/src/lib.rs +++ b/ovmf-prebuilt/src/lib.rs @@ -1 +1,134 @@ +//! Download, cache, and access [OVMF] prebuilts. +//! +//! [OVMF]: https://github.com/tianocore/edk2/tree/master/OvmfPkg#readme +//! +//! # Example +//! +//! ``` +//! use ovmf_prebuilt::{Arch, FileType, Source, Prebuilt}; +//! use std::path::Path; +//! +//! let prebuilt = Prebuilt::fetch(Source::LATEST, "target/ovmf") +//! .expect("failed to update prebuilt"); +//! assert_eq!( +//! prebuilt.get_file(Arch::X64, FileType::Code), +//! Path::new("target/ovmf/x64/code.fd") +//! ); +//! ``` +#![warn(missing_docs)] + +mod error; +mod fetch; + +use fetch::update_cache; +use std::path::{Path, PathBuf}; + +pub use error::Error; + +/// Which prebuilt to download. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Source { + /// Release tag name, e.g. "edk2-stable202408-r1". + pub tag: &'static str, + + /// SHA-256 hash of the compressed tarball. + pub sha256: &'static str, +} + +impl Source { + #[allow(missing_docs)] + pub const EDK2_STABLE202408_R1: Self = Self { + tag: "edk2-stable202408-r1", + sha256: "63a9217ddd51fa45d0a89fd83c483cc971765de6bb08e83cf70836b0baff0d48", + }; + + /// Latest release tag. + /// + /// Note that this is not necessarily the latest prebuilt available + /// from the git repo. + pub const LATEST: Self = Self::EDK2_STABLE202408_R1; +} + +/// UEFI architecture. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Arch { + Aarch64, + Ia32, + Riscv64, + X64, +} + +impl Arch { + /// Convert to a string. + pub fn as_str(self) -> &'static str { + match self { + Self::Aarch64 => "aarch64", + Self::Ia32 => "ia32", + Self::Riscv64 => "riscv64", + Self::X64 => "x64", + } + } +} + +/// Type of file within the prebuilt archive. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[allow(missing_docs)] +pub enum FileType { + Code, + Vars, + Shell, +} + +impl FileType { + /// Convert to a string. + pub fn as_str(self) -> &'static str { + match self { + Self::Code => "code.fd", + Self::Vars => "vars.fd", + Self::Shell => "shell.efi", + } + } +} + +/// Cached prebuilt. +pub struct Prebuilt { + dir: PathBuf, +} + +impl Prebuilt { + /// Fetch a prebuilt from a local cache. If the cache is out of + /// date, the prebuilt is downloaded and the cache is updated. + /// + /// The SHA-256 hash of the original prebuilt is stored in + /// `/sha256`. This is used to determine whether the + /// cache is up-to-date. Note that if some external process modifies + /// the cached files but leaves the `sha256` file unmodified, this + /// code will not detect that the cache is invalid. + /// + /// If the cache is updated, the downloaded prebuilt's hash will be + /// checked against [`source.sha256`]. An error will be + /// returned if the hash does not match, and the filesystem will not + /// be modified. This ensures that if you pin this library in + /// `Cargo.lock`, and use one of the [`Source`] associated + /// constants, the library will never unpack unverified files. This + /// provides some protection against a malicious attack modifying + /// the release tarballs on Github. + /// + /// [`source.sha256`]: Source::sha256 + pub fn fetch>(source: Source, prebuilt_dir: P) -> Result { + let prebuilt_dir = prebuilt_dir.as_ref(); + + update_cache(source, prebuilt_dir)?; + + Ok(Self { + dir: prebuilt_dir.to_owned(), + }) + } + + /// Get the path of a specific file within the cache. + pub fn get_file(&self, arch: Arch, file_type: FileType) -> PathBuf { + self.dir.join(arch.as_str()).join(file_type.as_str()) + } +}