mirror of
https://github.com/openai/codex.git
synced 2026-05-01 09:56:37 +00:00
271 lines
9.1 KiB
Rust
271 lines
9.1 KiB
Rust
use crate::PackageManagerError;
|
|
use flate2::read::GzDecoder;
|
|
use sha2::Digest;
|
|
use sha2::Sha256;
|
|
use std::fs::File;
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::Component;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use tar::Archive;
|
|
use zip::ZipArchive;
|
|
|
|
/// Archive metadata for a platform entry in a release manifest.
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
|
pub struct PackageReleaseArchive {
|
|
/// Archive file name relative to the package release location.
|
|
pub archive: String,
|
|
/// Expected SHA-256 of the downloaded archive body.
|
|
pub sha256: String,
|
|
/// Archive format used by the download.
|
|
pub format: ArchiveFormat,
|
|
/// Expected archive length in bytes, when the manifest provides it.
|
|
pub size_bytes: Option<u64>,
|
|
}
|
|
|
|
/// Archive formats supported by the generic extractor.
|
|
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
|
pub enum ArchiveFormat {
|
|
/// A `.zip` archive.
|
|
#[serde(rename = "zip")]
|
|
Zip,
|
|
/// A `.tar.gz` archive.
|
|
#[serde(rename = "tar.gz")]
|
|
TarGz,
|
|
}
|
|
|
|
/// Detects a package root with a `manifest.json` in an extraction directory.
|
|
pub(crate) fn detect_single_package_root(
|
|
extraction_root: &Path,
|
|
) -> Result<PathBuf, PackageManagerError> {
|
|
let direct_manifest = extraction_root.join("manifest.json");
|
|
if direct_manifest.exists() {
|
|
return Ok(extraction_root.to_path_buf());
|
|
}
|
|
|
|
let mut directory_candidates = Vec::new();
|
|
for entry in std::fs::read_dir(extraction_root).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to read {}", extraction_root.display()),
|
|
source,
|
|
})? {
|
|
let entry = entry.map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to read entry in {}", extraction_root.display()),
|
|
source,
|
|
})?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
directory_candidates.push(path);
|
|
}
|
|
}
|
|
|
|
if directory_candidates.len() == 1 {
|
|
let candidate = &directory_candidates[0];
|
|
if candidate.join("manifest.json").exists() {
|
|
return Ok(candidate.clone());
|
|
}
|
|
}
|
|
|
|
Err(PackageManagerError::MissingPackageRoot(
|
|
extraction_root.to_path_buf(),
|
|
))
|
|
}
|
|
|
|
pub(crate) fn verify_archive_size(
|
|
bytes: &[u8],
|
|
expected: Option<u64>,
|
|
) -> Result<(), PackageManagerError> {
|
|
let Some(expected) = expected else {
|
|
return Ok(());
|
|
};
|
|
let actual = bytes.len() as u64;
|
|
if actual == expected {
|
|
return Ok(());
|
|
}
|
|
Err(PackageManagerError::UnexpectedArchiveSize { expected, actual })
|
|
}
|
|
|
|
pub(crate) fn verify_sha256(bytes: &[u8], expected: &str) -> Result<(), PackageManagerError> {
|
|
let actual = format!("{:x}", Sha256::digest(bytes));
|
|
if actual == expected.to_ascii_lowercase() {
|
|
return Ok(());
|
|
}
|
|
Err(PackageManagerError::ChecksumMismatch {
|
|
expected: expected.to_string(),
|
|
actual,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn extract_archive(
|
|
archive_path: &Path,
|
|
destination: &Path,
|
|
format: ArchiveFormat,
|
|
) -> Result<(), PackageManagerError> {
|
|
match format {
|
|
ArchiveFormat::Zip => extract_zip_archive(archive_path, destination),
|
|
ArchiveFormat::TarGz => extract_tar_gz_archive(archive_path, destination),
|
|
}
|
|
}
|
|
|
|
fn extract_zip_archive(archive_path: &Path, destination: &Path) -> Result<(), PackageManagerError> {
|
|
let file = File::open(archive_path).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to open {}", archive_path.display()),
|
|
source,
|
|
})?;
|
|
let mut archive = ZipArchive::new(file)
|
|
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
|
|
for index in 0..archive.len() {
|
|
let mut entry = archive
|
|
.by_index(index)
|
|
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
|
|
let Some(relative_path) = entry.enclosed_name() else {
|
|
return Err(PackageManagerError::ArchiveExtraction(format!(
|
|
"zip entry `{}` escapes extraction root",
|
|
entry.name()
|
|
)));
|
|
};
|
|
let output_path = destination.join(relative_path);
|
|
if entry.is_dir() {
|
|
std::fs::create_dir_all(&output_path).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to create {}", output_path.display()),
|
|
source,
|
|
})?;
|
|
continue;
|
|
}
|
|
if let Some(parent) = output_path.parent() {
|
|
std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to create {}", parent.display()),
|
|
source,
|
|
})?;
|
|
}
|
|
let mut output = File::create(&output_path).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to create {}", output_path.display()),
|
|
source,
|
|
})?;
|
|
std::io::copy(&mut entry, &mut output).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to write {}", output_path.display()),
|
|
source,
|
|
})?;
|
|
apply_zip_permissions(&entry, &output_path)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn apply_zip_permissions(
|
|
entry: &zip::read::ZipFile<'_>,
|
|
output_path: &Path,
|
|
) -> Result<(), PackageManagerError> {
|
|
let Some(mode) = entry.unix_mode() else {
|
|
return Ok(());
|
|
};
|
|
std::fs::set_permissions(output_path, std::fs::Permissions::from_mode(mode)).map_err(|source| {
|
|
PackageManagerError::Io {
|
|
context: format!("failed to set permissions on {}", output_path.display()),
|
|
source,
|
|
}
|
|
})
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn apply_zip_permissions(
|
|
_entry: &zip::read::ZipFile<'_>,
|
|
_output_path: &Path,
|
|
) -> Result<(), PackageManagerError> {
|
|
Ok(())
|
|
}
|
|
|
|
fn extract_tar_gz_archive(
|
|
archive_path: &Path,
|
|
destination: &Path,
|
|
) -> Result<(), PackageManagerError> {
|
|
let file = File::open(archive_path).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to open {}", archive_path.display()),
|
|
source,
|
|
})?;
|
|
let decoder = GzDecoder::new(file);
|
|
let mut archive = Archive::new(decoder);
|
|
for entry in archive
|
|
.entries()
|
|
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?
|
|
{
|
|
let mut entry =
|
|
entry.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
|
|
let path = entry
|
|
.path()
|
|
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
|
|
let output_path = safe_extract_path(destination, path.as_ref())?;
|
|
let entry_type = entry.header().entry_type();
|
|
|
|
if entry_type.is_symlink()
|
|
|| entry_type.is_hard_link()
|
|
|| entry_type.is_block_special()
|
|
|| entry_type.is_character_special()
|
|
|| entry_type.is_fifo()
|
|
|| entry_type.is_gnu_sparse()
|
|
{
|
|
return Err(PackageManagerError::ArchiveExtraction(format!(
|
|
"tar entry `{}` has unsupported type",
|
|
path.display()
|
|
)));
|
|
}
|
|
|
|
if entry_type.is_pax_global_extensions()
|
|
|| entry_type.is_pax_local_extensions()
|
|
|| entry_type.is_gnu_longname()
|
|
|| entry_type.is_gnu_longlink()
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if entry_type.is_dir() {
|
|
std::fs::create_dir_all(&output_path).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to create {}", output_path.display()),
|
|
source,
|
|
})?;
|
|
continue;
|
|
}
|
|
|
|
if !entry_type.is_file() && !entry_type.is_contiguous() {
|
|
return Err(PackageManagerError::ArchiveExtraction(format!(
|
|
"tar entry `{}` has unsupported type",
|
|
path.display()
|
|
)));
|
|
}
|
|
|
|
if let Some(parent) = output_path.parent() {
|
|
std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io {
|
|
context: format!("failed to create {}", parent.display()),
|
|
source,
|
|
})?;
|
|
}
|
|
entry
|
|
.unpack(&output_path)
|
|
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn safe_extract_path(root: &Path, relative_path: &Path) -> Result<PathBuf, PackageManagerError> {
|
|
let mut clean_relative = PathBuf::new();
|
|
for component in relative_path.components() {
|
|
match component {
|
|
Component::Normal(segment) => clean_relative.push(segment),
|
|
Component::CurDir => {}
|
|
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
|
|
return Err(PackageManagerError::ArchiveExtraction(format!(
|
|
"entry `{}` escapes extraction root",
|
|
relative_path.display()
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if clean_relative.as_os_str().is_empty() {
|
|
return Err(PackageManagerError::ArchiveExtraction(
|
|
"archive entry had an empty path".to_string(),
|
|
));
|
|
}
|
|
Ok(root.join(clean_relative))
|
|
}
|