Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ chrono = "0.4.42"
clap = "4.5.51"
const_format = "0.2.35"
easy-logging = "1"
file-format = { version = "0.28.0", features = ["reader"] }
flate2 = "1.1"
futures-util = "0.3.31"
globset = "0.4.18"
Expand Down
138 changes: 96 additions & 42 deletions src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,73 +9,127 @@ use url::Url;
use crate::core::{EmptyResult, GenericResult};
use crate::util;

pub enum FileType {
Single,
Archived {mode: u32},
}

pub trait Installer {
fn on_file(&mut self, path: &Path, mode: u32, data: &mut dyn Read) -> EmptyResult;
fn has_binary_matcher(&self) -> bool;
fn on_file(&mut self, path: &Path, file_type: FileType, data: &mut dyn Read) -> EmptyResult;
}

pub fn download(url: &Url, name: &str, installer: &mut dyn Installer) -> EmptyResult {
let reader = ReleaseReaderBuilder::new(name)?;
let client = ClientBuilder::new().user_agent(util::USER_AGENT).build()?;
let (stripped_name, decompressor_builder) = get_decompressor_builder(name)?;
let archive_type = stripped_name.rsplit_once('.').map(|(_, extension)| extension);

let reader_builder: ReaderBuilder = match archive_type.unwrap_or_default() {
"tar" => Box::new(|| Box::new(TarReader::new())),
"7z" | "apk" | "deb" | "dmg" | "msi" | "pkg" | "rar" | "rpm" | "zip" => {
return Err!("Unsupported file type: {name:?}")
},
_ => {
if installer.has_binary_matcher() {
return Err!(concat!(
"The release file {:?} looks like a simple binary (not an archive), ",
"but a binary matcher is specified",
), name);
}

let binary_name = stripped_name.to_owned();
Box::new(move || Box::new(BinaryReader::new(&binary_name)))
},
};

debug!("Downloading {url}...");
let client = ClientBuilder::new().user_agent(util::USER_AGENT).build()?;

let response = client.get(url.to_owned()).send()?;
if !response.status().is_success() {
return Err!("The server returned and error: {}", response.status())
return Err!("The server returned an error: {}", response.status())
}

let mut archive = reader.build(response);
let decompressor = decompressor_builder(Box::new(response));
let reader = reader_builder();

reader.read(decompressor, installer)
}

for (index, entry) in archive.entries()?.enumerate() {
let mut entry = entry?;
type DecompressorBuilder = Box<dyn FnOnce(Box<dyn Read>) -> Box<dyn Read>>;
pub const COMPRESSION_EXTENSION_REGEX: &str = r"\.(?:bz2|gz|lz|lz4|lzma|lzo|xz|z|zst)";

let header = entry.header();
let path = entry.path()?;
let entry_type = header.entry_type();
fn get_decompressor_builder(name: &str) -> GenericResult<(&str, DecompressorBuilder)> {
let Some((stripped_name, extension)) = name.rsplit_once('.') else {
return Ok((name, Box::new(|reader| reader)));
};

if index == 0 {
debug!("Processing the archive:")
}
debug!("* {path:?} ({entry_type:?})");
let builder: DecompressorBuilder = match extension {
"bz2" => Box::new(|reader| Box::new(bzip2::read::BzDecoder::new(reader))),
"gz" => Box::new(|reader| Box::new(flate2::read::GzDecoder::new(reader))),
"xz" => Box::new(|reader| Box::new(xz2::read::XzDecoder::new(reader))),
"lz" | "lz4" | "lzma" | "lzo" | "z" | "zst" => return Err!("Unsupported file type: {name:?}"),
_ => return Ok((name, Box::new(|reader| reader))),
};

Ok((stripped_name, builder))
}

type ReaderBuilder = Box<dyn FnOnce() -> Box<dyn ReleaseReader>>;

trait ReleaseReader {
fn read(self: Box<Self>, reader: Box<dyn Read>, installer: &mut dyn Installer) -> EmptyResult;
}

if matches!(entry_type, EntryType::Regular | EntryType::Continuous) {
let path = path.to_path_buf();
let mode = header.mode()?;
installer.on_file(&path, mode, &mut entry)?;
struct BinaryReader {
name: String,
}

impl BinaryReader {
fn new(name: &str) -> BinaryReader {
BinaryReader {
name: name.to_owned(),
}
}
}

Ok(())
impl ReleaseReader for BinaryReader {
fn read(self: Box<Self>, mut reader: Box<dyn Read>, installer: &mut dyn Installer) -> EmptyResult {
installer.on_file(Path::new(self.name.as_str()), FileType::Single, &mut reader)
}
}

type DecoderBuilder = Box<dyn FnOnce(Box<dyn Read>) -> Box<dyn Read>>;
struct TarReader {
}

struct ReleaseReaderBuilder {
decoder_builder: DecoderBuilder,
impl TarReader {
fn new() -> TarReader {
TarReader {}
}
}

impl ReleaseReaderBuilder {
fn new(name: &str) -> GenericResult<ReleaseReaderBuilder> {
let decoder_builder = name.rsplit_once('.').and_then(|(name, extension)| {
let decoder: DecoderBuilder = match extension {
"bz2" => Box::new(|reader| Box::new(bzip2::read::BzDecoder::new(reader))),
"gz" => Box::new(|reader| Box::new(flate2::read::GzDecoder::new(reader))),
"xz" => Box::new(|reader| Box::new(xz2::read::XzDecoder::new(reader))),
_ => return None,
};

if name.rsplit_once('.')?.1 != "tar" {
return None;
}
impl ReleaseReader for TarReader {
fn read(self: Box<Self>, reader: Box<dyn Read>, installer: &mut dyn Installer) -> EmptyResult {
let mut archive = Archive::new(reader);

Some(decoder)
}).ok_or_else(|| format!("Unsupported file type: {name:?}"))?;
for (index, entry) in archive.entries()?.enumerate() {
let mut entry = entry?;

Ok(ReleaseReaderBuilder {decoder_builder})
}
let header = entry.header();
let path = entry.path()?;
let entry_type = header.entry_type();

if index == 0 {
debug!("Processing the archive:")
}
debug!("* {path:?} ({entry_type:?})");

if matches!(entry_type, EntryType::Regular | EntryType::Continuous) {
let path = path.to_path_buf();
let file_type = FileType::Archived {mode: header.mode()?};
installer.on_file(&path, file_type, &mut entry)?;
}
}

fn build<R: Read + 'static>(self, reader: R) -> Archive<impl Read> {
let reader = (self.decoder_builder)(Box::new(reader));
Archive::new(reader)
Ok(())
}
}
47 changes: 47 additions & 0 deletions src/file_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::io::{Read, Seek, SeekFrom};
use std::env::consts;
use std::str::FromStr;

use file_format::{FileFormat, Kind};
use platforms::OS;

use crate::core::GenericResult;

pub fn is_executable<R: Read + Seek>(mut reader: R) -> GenericResult<(String, bool)> {
let format = {
reader.seek(SeekFrom::Start(0))?;
FileFormat::from_reader(reader)?
};

let description = format!(
"{full_name}{short_name} ({kind:?}, {media_type})",
full_name=format.name(), short_name=format.short_name().map(|name| format!(" / {name}")).unwrap_or_default(),
kind=format.kind(), media_type=format.media_type(),
);

let executable = get_os_specific_executable_types().unwrap_or_default().contains(&format)
|| format.kind() == Kind::Other && format.name().ends_with(" Script");

Ok((description, executable))
}

fn get_os_specific_executable_types() -> Option<Vec<FileFormat>> {
Some(match OS::from_str(consts::OS).ok()? {
OS::Linux => vec![FileFormat::ExecutableAndLinkableFormat],
OS::MacOS => vec![FileFormat::MachO],
_ => return None,
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn os_support() {
assert!(
get_os_specific_executable_types().is_some(),
"Unsupported OS: {}", consts::OS,
);
}
}
81 changes: 57 additions & 24 deletions src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use url::Url;

use crate::config::Config;
use crate::core::{EmptyResult, GenericResult};
use crate::download;
use crate::download::{self, FileType};
use crate::file_types;
use crate::github::{self, Github};
use crate::matcher::Matcher;
use crate::release::{self, Release};
Expand Down Expand Up @@ -270,38 +271,56 @@ impl Installer {

impl Drop for Installer {
fn drop(&mut self) {
if let Some(temp_path) = self.temp_path.take() && let Err(err) = fs::remove_file(&temp_path) {
if let Some(temp_path) = self.temp_path.take()
&& let Err(err) = fs::remove_file(&temp_path) {
error!("Unable to delete {temp_path:?}: {err}.");
}
}
}

impl download::Installer for Installer {
fn on_file(&mut self, path: &Path, mode: u32, data: &mut dyn Read) -> EmptyResult {
let is_executable = mode & 0o100 != 0;
fn has_binary_matcher(&self) -> bool {
!self.automatic_matcher
}

if is_executable {
self.binaries.push(path.to_owned());
}
fn on_file(&mut self, path: &Path, file_type: FileType, data: &mut dyn Read) -> EmptyResult {
let mut check_file_type = false;

if self.matcher.matches(path) {
debug!("{path:?} matches binary matcher.");
match file_type {
FileType::Single => {
assert!(!self.has_binary_matcher()); // It must be checked before release downloading
self.binaries.push(path.to_owned());
self.matches.push(path.to_owned());
check_file_type = true;
},

self.matches.push(path.to_owned());
if self.matches.len() > 1 {
return Ok(()); // We'll return error later when collect all matches
}
FileType::Archived {mode} => {
let is_executable = mode & 0o100 != 0;

if !is_executable {
return Err!("{path:?} in the archive is not executable");
}
} else if self.automatic_matcher && is_executable && self.temp_path.is_none() {
debug!(concat!(
"Got first executable in archive: {:?}. ",
"Download it for the case if it's the only one executable in archive.",
), path);
} else {
return Ok(());
if is_executable {
self.binaries.push(path.to_owned());
}

if self.matcher.matches(path) {
debug!("{path:?} matches binary matcher.");

self.matches.push(path.to_owned());
if self.matches.len() > 1 {
return Ok(()); // We'll return error later when collect all matches
}

if !is_executable {
return Err!("{path:?} in the archive is not executable");
}
} else if self.automatic_matcher && is_executable && self.temp_path.is_none() {
debug!(concat!(
"Got first executable in archive: {:?}. ",
"Download it for the case if it's the only one executable in archive.",
), path);
} else {
return Ok(());
}
},
}

let temp_path = match self.temp_path.as_ref() {
Expand All @@ -320,6 +339,7 @@ impl download::Installer for Installer {
let mut file = OpenOptions::new()
.create(true)
.mode(0o755)
.read(check_file_type)
.write(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW)
Expand All @@ -329,8 +349,21 @@ impl download::Installer for Installer {

io::copy(data, &mut file)?;
file.set_modified(self.time)?;
file.sync_all()?;

if check_file_type {
let (description, executable) = file_types::is_executable(&mut file).map_err(|e| format!(
"Failed to determine {path:?} file type: {e}"))?;

if !executable {
return Err!(
"{} doesn't look like an executable for current operating system and identified as {}",
path.display(), description)
}

debug!("{} is identified as {description}.", path.display());
}

file.sync_all()?;
Ok(())
}
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod cli;
mod config;
mod download;
mod file_types;
mod github;
mod install;
mod list;
Expand Down
Loading