install-framework-base 1.0.0

[Install Framework] Official generic interface implementation
Documentation
// Copyright 2021 Yuri6037

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

use std::collections::HashMap;
use std::string::String;
use std::path::Path;
use std::io::Read;
use std::io::Write;
use std::fs::File;
use std::io;
use zip::result::ZipResult;
use zip::result::ZipError;
use zip::ZipArchive;
use reqwest::blocking::Client;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
use reqwest::header::HeaderName;

use crate::interface::SimpleInterpreter;
use crate::cache::Cache;
use crate::error::Error;

fn save_file<F: FnMut (f32) -> bool>(mut update: F, stream: &mut dyn Read, outpath: &Path, max: Option<u64>) -> io::Result<()>
{
    let mut f = File::create(outpath)?;
    let mut buf: [u8; 8192] = [0; 8192];
    let mut cur: u64 = 0;

    loop
    {
        let len = stream.read(&mut buf)?;
        if len <= 0
        {
            break;
        }
        f.write(&buf[0..len])?;
        if let Some(v) = max
        {
            cur += len as u64;
            if !update((cur as f64 / v as f64) as f32)
            {
                return Ok(());
            }
        }
    }
    return Ok(());
}

fn zip_extract<F: FnMut (f32) -> bool>(mut update: F, infile: &Path, outpath: &Path) -> ZipResult<()>
{
    let f = match File::open(&infile)
    {
        Ok(v) => v,
        Err(e) => return Err(ZipError::Io(e))
    };
    let mut f = ZipArchive::new(f)?;

    for i in 0..f.len()
    {
        if !update((i as f64 / f.len() as f64) as f32)
        {
            return Ok(());
        }
        let mut file = f.by_index(i)?;
        let out = match file.enclosed_name() {
            Some(path) => path.to_owned(),
            None => continue,
        };
        let path = outpath.join(out);
        if (&*file.name()).ends_with('/')
        {
            if let Err(e) = std::fs::create_dir_all(&path)
            {
                return Err(ZipError::Io(e));
            }
            continue;
        }
        if let Some(p) = path.parent()
        {
            if !p.exists()
            {
                if let Err(e) = std::fs::create_dir_all(&p)
                {
                    return Err(ZipError::Io(e));
                }
            }
        }
        let mut outfile = match File::create(&path)
        {
            Ok(v) => v,
            Err(e) => return Err(ZipError::Io(e))
        };
        if let Err(e) = io::copy(&mut file, &mut outfile)
        {
            return Err(ZipError::Io(e));
        }
    }
    return Ok(());
}

pub fn buf_to_file(path: &Path, bytes: &[u8]) -> io::Result<()>
{
    let mut f = File::create(path)?;
    f.write(bytes)?;
    return Ok(());
}

pub fn download_file<TError: Error>(interpreter: &mut dyn SimpleInterpreter<TError::ErrorType>, client: &mut Client, cache: &mut Cache<TError>, resid: usize, filename: String, url: String, headers: HashMap<String, String>) -> Result<(), TError::ErrorType>
{
    interpreter.begin_substep(&format!("Downloading {}...", filename))?;
    let mut au = HeaderMap::new();
    for (k, v) in headers
    {
        let computed = cache.parse_string(&v)?;
        match HeaderValue::from_str(&computed.to_string_lossy()) //Must use lossy conversion as reqwest does not want to use an OsString
        {
            Ok(vv) =>
            {
                match HeaderName::from_bytes(k.as_ref())
                {
                    Ok(vvv) =>
                    {
                        au.insert(vvv, vv);
                    }
                    Err(e) => return Err(TError::network_invalid_header_name(e))
                };
            },
            Err(e) => return Err(TError::network_invalid_header_value(e))
        };
    }
    match client.get(url).headers(au).send()
    {
        Ok(mut v) =>
        {
            let useless = v.content_length();
            let outfile = cache.get_path(Path::new(&filename))?;
            let mut err = None;
            let updater = |progress|
            {
                if let Err(e) = interpreter.update_substep(progress)
                {
                    err = Some(e);
                    return false;
                }
                return true;
            };
            if let Err(e) = save_file(updater, &mut v, &outfile, useless)
            {
                return Err(TError::io(e));
            }
            if let Some(e) = err
            {
                return Err(e);
            }
            interpreter.end_substep()?;
            cache.insert(resid, outfile);
        },
        Err(e) => return Err(TError::network(e))
    };
    return Ok(());
}

pub fn unpack_cached<TError: Error>(interpreter: &mut dyn SimpleInterpreter<TError::ErrorType>, cache: &mut Cache<TError>, resid: usize, path: String) -> Result<(), TError::ErrorType>
{
    let computed = cache.parse_string(&path)?;
    interpreter.begin_substep(&format!("Unpacking {:?}...", computed))?;
    if !computed.to_string_lossy().ends_with(".zip")
    {
        return Err(TError::generic(String::from("Only ZIP archives are supported")));
    }
    let mut computed1 = computed.clone();
    computed1.push(".d");
    let inpath = Path::new(&computed);
    let outpath = Path::new(&computed1);
    if let Err(e) = std::fs::create_dir(&outpath)
    {
        return Err(TError::io(e));
    }
    let mut err = None;
    let updater = |progress|
    {
        if let Err(e) = interpreter.update_substep(progress)
        {
            err = Some(e);
            return false;
        }
        return true;
    };
    if let Err(e) = zip_extract(updater, &inpath, &outpath)
    {
        return Err(TError::zip(e));
    }
    if let Some(e) = err
    {
        return Err(e);
    }
    interpreter.end_substep()?;
    cache.insert(resid, outpath);
    return Ok(());
}

pub fn extract_resource<TError: Error>(cache: &mut Cache<TError>, resources: &HashMap<&'static str, &'static [u8]>, resid: usize, path: &'static str) -> Result<(), TError::ErrorType>
{
    let v = match resources.get(path)
    {
        Some(v) => v,
        None => return Err(TError::unknown_resource_path(path))
    };
    let name =
    {
        if let Some(id) = path.rfind('/')
        {
            &path[id + 1..]
        }
        else
        {
            path
        }
    };
    let outpath = cache.get_path(Path::new(name))?;
    if let Err(e) = buf_to_file(&outpath, v)
    {
        return Err(TError::io(e));
    }
    cache.insert(resid, outpath);
    return Ok(());
}

pub fn user_input<TError: Error>(interpreter: &mut dyn SimpleInterpreter<TError::ErrorType>, cache: &mut Cache<TError>, props: &HashMap<String, String>, resid: usize, prop: String, message: String) -> Result<(), TError::ErrorType>
{
    if let Some(val) = props.get(&prop)
    {
        cache.insert(resid, val);
    }
    else
    {
        let line = interpreter.read_user_input_text(&message)?;
        cache.insert(resid, line);
    }
    return Ok(());
}