Skip to content

Commit

Permalink
fix: extraction for zip files (#447)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xbe7a authored Dec 25, 2023
1 parent 2ba8703 commit 0185358
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 100 deletions.
14 changes: 11 additions & 3 deletions rust-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ mod tests {
}

/// doesn't correctly handle spaces within argument of args escape all spaces
fn shx<'a>(src: impl AsRef<str>) -> Option<String> {
fn shx(src: impl AsRef<str>) -> Option<String> {
let (prog, args) = src.as_ref().split_once(' ')?;
Command::new(prog)
.args(args.split(' '))
Expand Down Expand Up @@ -312,8 +312,7 @@ mod tests {
assert!(c["path_type"] == p["path_type"]);
if ppath
.components()
.find(|s| s.eq(&Component::Normal("dist-info".as_ref())))
.is_some()
.any(|s| s.eq(&Component::Normal("dist-info".as_ref())))
{
assert!(c["sha256"] == p["sha256"]);
assert!(c["size_in_bytes"] == p["size_in_bytes"]);
Expand Down Expand Up @@ -451,4 +450,13 @@ mod tests {
assert!(rattler_build.is_ok());
assert!(rattler_build.unwrap().status.success());
}

#[test]
fn test_zip_source() {
let tmp = tmp("test_zip_source");
let rattler_build =
rattler().build::<_, _, &str>(recipes().join("zip-source"), tmp.as_dir(), None);
assert!(rattler_build.is_ok());
assert!(rattler_build.unwrap().status.success());
}
}
143 changes: 46 additions & 97 deletions src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

use std::{
ffi::OsStr,
path::{Component, Path, PathBuf, StripPrefixError},
path::{Path, PathBuf, StripPrefixError},
};

use crate::recipe::parser::Source;

use fs_err as fs;
use fs_err::File;
use zip::{result::ZipResult, ZipArchive};

pub mod copy_dir;
pub mod git_source;
Expand Down Expand Up @@ -134,15 +133,15 @@ pub async fn fetch_sources(
.to_string_lossy()
.contains(".tar")
{
extract(&res, &dest_dir, 1)?;
extract_tar(&res, &dest_dir)?;
tracing::info!("Extracted to {:?}", dest_dir);
} else if res
.file_name()
.unwrap_or_default()
.to_string_lossy()
.ends_with(".zip")
{
extract_zip(&res, &dest_dir, 1)?;
extract_zip(&res, &dest_dir)?;
tracing::info!("Extracted zip to {:?}", dest_dir);
} else {
if let Some(file_name) = src.file_name() {
Expand Down Expand Up @@ -253,118 +252,68 @@ impl std::io::Read for TarCompression<'_> {
}
}

fn move_extracted_dir(src: &Path, dest: &Path) -> Result<(), SourceError> {
let entries = fs::read_dir(src)?;
let mut dir_name = None;

for entry in entries {
let entry = entry?;
if entry.file_type()?.is_dir() && dir_name.is_none() {
dir_name = Some(entry.file_name());
} else {
dir_name = None;
break;
}
}

let src_dir = if let Some(dir) = dir_name {
src.join(dir)
} else {
src.to_path_buf()
};

for inner_entry in fs::read_dir(&src_dir)? {
let inner_entry = inner_entry?;
let destination = dest.join(inner_entry.file_name());
fs::rename(inner_entry.path(), destination)?;
}

Ok(())
}

/// Extracts a tar archive to the specified target directory
fn extract(
archive: &Path,
target_directory: &Path,
strip_components: usize,
) -> Result<(), SourceError> {
fn extract_tar(archive: &Path, target_directory: &Path) -> Result<(), SourceError> {
let mut archive = tar::Archive::new(ext_to_compression(
archive.file_name(),
File::open(archive).map_err(|_| SourceError::FileNotFound(archive.to_path_buf()))?,
));

for entry in archive.entries()? {
let mut entry = entry?;
let mut path = PathBuf::new();
{
// Essentially from https://github.com/alexcrichton/tar-rs/blob/34744459084c1fffb03d6c742f5a5af9a6403bc4/src/entry.rs#L381
// for secure implementation of unpack, we skip all paths with ParentDir component as listed in below CVEs
let entrypath = entry.path()?;
for part in entrypath.components().skip(strip_components) {
match part {
// Leading '/' characters, root paths, and '.'
// components are just ignored and treated as "empty
// components"
Component::Prefix(..) | Component::RootDir | Component::CurDir => continue,
// If any part of the filename is '..', then skip over
// unpacking the file to prevent directory traversal
// security issues. See, e.g.: CVE-2001-1267,
// CVE-2002-0399, CVE-2005-1918, CVE-2007-4131
Component::ParentDir => continue,
Component::Normal(part) => path.push(part),
}
}
}
let path = target_directory.join(path);
if entry.header().entry_type().is_dir() {
// only errors if fails to create dir
// and if file doesn't already exists
std::fs::create_dir_all(&path)?;
continue;
}
// create parent dir if doesn't already exists before unpacking
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
// should setup permissions and xattrs
entry.unpack(path)?;
}
let tmp_extraction_dir = tempfile::tempdir()?;

archive
.unpack(&tmp_extraction_dir)
.map_err(|e| SourceError::TarExtractionError(e.to_string()))?;

move_extracted_dir(tmp_extraction_dir.path(), target_directory)?;

Ok(())
}

/// Extracts a zip archive to the specified target directory
/// currently this doesn't support bzip2 and zstd, zip archived with compression other than deflate would fail.
/// <!-- TODO: we can trivially add support for bzip2 and zstd by enabling the feature flags -->
fn extract_zip(
archive: &Path,
target_directory: &Path,
strip_components: usize,
) -> Result<(), SourceError> {
let archive = zip::ZipArchive::new(
fn extract_zip(archive: &Path, target_directory: &Path) -> Result<(), SourceError> {
let mut archive = zip::ZipArchive::new(
File::open(archive).map_err(|_| SourceError::FileNotFound(archive.to_path_buf()))?,
)
.map_err(|e| SourceError::InvalidZip(e.to_string()))?;

extract_zip_stripped(archive, target_directory, strip_components)
let tmp_extraction_dir = tempfile::tempdir()?;
archive
.extract(&tmp_extraction_dir)
.map_err(|e| SourceError::ZipExtractionError(e.to_string()))?;

Ok(())
}

fn extract_zip_stripped(
mut zip: ZipArchive<File>,
target_directory: &Path,
strip_components: usize,
) -> ZipResult<()> {
use std::fs;
for i in 0..zip.len() {
let mut file = zip.by_index(i)?;
let filepath = file
.enclosed_name()
.ok_or(zip::result::ZipError::InvalidArchive("Invalid file path"))?;

let filepath = filepath
.components()
.skip(strip_components)
.collect::<PathBuf>();
if filepath.as_os_str().is_empty() {
continue;
}
let outpath = target_directory.join(filepath);
move_extracted_dir(tmp_extraction_dir.path(), target_directory)?;

if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = fs::File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
}
// set permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
}
}
}
Ok(())
}
15 changes: 15 additions & 0 deletions test-data/recipes/zip-source/recipe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json
package:
name: zip-source
version: 0.1.0
source:
- url: https://cache-redirector.jetbrains.com/download.jetbrains.com/idea/jdbc-drivers/web/snowflake-3.13.27.zip
sha256: 6a15e95ee7e6c55b862dab9758ea803350aa2e3560d6183027b0c29919fcab18

build:
script:
- if: unix
then:
- test -f snowflake-jdbc-3.13.27.jar
else:
- if not exist snowflake-jdbc-3.13.27.jar exit 1

0 comments on commit 0185358

Please sign in to comment.