<commit_before>extern crate base64;

extern crate byteorder;

extern crate hyper;
extern crate hyper_native_tls;

extern crate rayon;

#[macro_use]
extern crate serde_derive;
extern crate serde_json;

extern crate pem;

#[macro_use]
extern crate prettytable;

use std::{env, process};
use std::fs::File;
use std::io::{Read, Write};

use byteorder::{BigEndian, WriteBytesExt};

use rayon::iter::{IntoParallelRefIterator, ParallelIterator};

#[derive(Deserialize)]
struct LogsResponseLogs {
    description: String,
    url: String,
    operated_by: Vec<u32>,
    disqualified_at: Option<u64>,
}

#[derive(Deserialize)]
struct LogsResponseOperators {
    name: String,
    id: u32,
}

#[derive(Deserialize)]
struct LogsResponse {
    logs: Vec<LogsResponseLogs>,
    operators: Vec<LogsResponseOperators>,
}

#[derive(Debug, Clone)]
struct Log {
    description: String,
    url: String,
    is_google: bool,
}

const LOG_LIST_URL: &'static str = "https://www.gstatic.com/ct/log_list/log_list.json";

fn fetch_trusted_ct_logs(http_client: &hyper::Client) -> Vec<Log> {
    let response = http_client.get(LOG_LIST_URL).send().unwrap();
    // Limit the response to 10MB at most, to be resillient to DoS.
    let logs_response: LogsResponse = serde_json::from_reader(response.take(10 * 1024 * 1024))
        .unwrap();

    let google_id = logs_response
        .operators
        .iter()
        .find(|o| o.name == "Google")
        .map(|o| o.id);

    return logs_response
               .logs
               .into_iter()
               .filter(|log| log.disqualified_at.is_none())
               .map(|log| {
                        Log {
                            url: log.url,
                            description: log.description,
                            is_google: log.operated_by.contains(&google_id.unwrap()),
                        }
                    })
               .collect();
}

#[derive(Debug, Deserialize)]
struct SignedCertificateTimestamp {
    sct_version: u8,
    id: String,
    timestamp: u64,
    extensions: String,
    signature: String,
}

impl SignedCertificateTimestamp {
    fn to_raw_bytes(&self) -> Vec<u8> {
        let mut b = Vec::new();
        b.write_u8(self.sct_version).unwrap();

        let log_id = base64::decode(&self.id).unwrap();
        b.write(&log_id).unwrap();

        b.write_u64::<BigEndian>(self.timestamp).unwrap();

        let extensions = base64::decode(&self.extensions).unwrap();
        assert!(extensions.len() <= 65535);
        b.write_u16::<BigEndian>(extensions.len() as u16)
            .unwrap();
        b.write(&extensions).unwrap();

        let signature = base64::decode(&self.signature).unwrap();
        b.write(&signature).unwrap();

        return b;
    }
}

fn submit_to_log(http_client: &hyper::Client,
                 url: &str,
                 payload: &[u8])
                 -> Option<SignedCertificateTimestamp> {
    let mut url = "https://".to_string() + url;
    if !url.ends_with("/") {
        url += "/";
    }
    url += "ct/v1/add-chain";
    let response = http_client
        .post(&url)
        .body(hyper::client::Body::BufBody(payload, payload.len()))
        .header(hyper::header::ContentType::json())
        .send();
    let response = match response {
        Ok(r) => r,
        // TODO: maybe not all of these should be silently ignored.
        Err(_) => return None,
    };

    // 400, 403, and probably some others generally indicate a log doesn't accept certs from this
    // root, or that the log isn't accepting new submissions.
    if response.status.is_client_error() {
        return None;
    }

    // Limt the response to 10MB (well above what would ever be needed) to be resilient to DoS in
    // the face of a dumb or malicious log.
    return Some(serde_json::from_reader(response.take(10 * 1024 * 1024)).unwrap());
}

#[derive(Serialize)]
struct AddChainRequest {
    chain: Vec<String>,
}

fn submit_cert_to_logs<'a>(http_client: &hyper::Client,
                           logs: &'a [Log],
                           cert: Vec<Vec<u8>>)
                           -> Vec<(&'a Log, SignedCertificateTimestamp)> {
    let payload = serde_json::to_vec(&AddChainRequest {
                                          chain: cert.iter().map(|r| base64::encode(r)).collect(),
                                      })
            .unwrap();

    return logs.par_iter()
               .filter_map(|ref log| {
                               let sct = submit_to_log(http_client, &log.url, &payload);
                               return sct.map(|s| (log.clone(), s));
                           })
               .collect();
}

fn main() {
    let path = match env::args().skip(1).next() {
        Some(p) => p,
        None => {
            println!("Usage: {} <path/to/chain.pem>", env::args().next().unwrap());
            process::exit(1);
        }
    };

    let mut contents = String::new();
    File::open(path)
        .unwrap()
        .read_to_string(&mut contents)
        .unwrap();

    let chain = pem::parse_many(contents)
        .into_iter()
        .filter(|p| p.tag == "CERTIFICATE")
        .map(|p| p.contents)
        .collect();

    let http_client = hyper::Client::with_connector(
        hyper::net::HttpsConnector::new(hyper_native_tls::NativeTlsClient::new().unwrap())
    );
    let logs = fetch_trusted_ct_logs(&http_client);

    let scts = submit_cert_to_logs(&http_client, &logs, chain);

    let mut table = prettytable::Table::new();
    table.add_row(row!["Log", "SCT"]);
    for (log, sct) in scts {
        table.add_row(row![log.description, base64::encode(&sct.to_raw_bytes())]);
    }
    table.printstd();
}
<commit_msg>slap a timeout on the http requests<commit_after>extern crate base64;

extern crate byteorder;

extern crate hyper;
extern crate hyper_native_tls;

extern crate rayon;

#[macro_use]
extern crate serde_derive;
extern crate serde_json;

extern crate pem;

#[macro_use]
extern crate prettytable;

use std::{env, process};
use std::fs::File;
use std::io::{Read, Write};
use std::time::Duration;

use byteorder::{BigEndian, WriteBytesExt};

use rayon::iter::{IntoParallelRefIterator, ParallelIterator};

#[derive(Deserialize)]
struct LogsResponseLogs {
    description: String,
    url: String,
    operated_by: Vec<u32>,
    disqualified_at: Option<u64>,
}

#[derive(Deserialize)]
struct LogsResponseOperators {
    name: String,
    id: u32,
}

#[derive(Deserialize)]
struct LogsResponse {
    logs: Vec<LogsResponseLogs>,
    operators: Vec<LogsResponseOperators>,
}

#[derive(Debug, Clone)]
struct Log {
    description: String,
    url: String,
    is_google: bool,
}

const LOG_LIST_URL: &'static str = "https://www.gstatic.com/ct/log_list/log_list.json";

fn fetch_trusted_ct_logs(http_client: &hyper::Client) -> Vec<Log> {
    let response = http_client.get(LOG_LIST_URL).send().unwrap();
    // Limit the response to 10MB at most, to be resillient to DoS.
    let logs_response: LogsResponse = serde_json::from_reader(response.take(10 * 1024 * 1024))
        .unwrap();

    let google_id = logs_response
        .operators
        .iter()
        .find(|o| o.name == "Google")
        .map(|o| o.id);

    return logs_response
               .logs
               .into_iter()
               .filter(|log| log.disqualified_at.is_none())
               .map(|log| {
                        Log {
                            url: log.url,
                            description: log.description,
                            is_google: log.operated_by.contains(&google_id.unwrap()),
                        }
                    })
               .collect();
}

#[derive(Debug, Deserialize)]
struct SignedCertificateTimestamp {
    sct_version: u8,
    id: String,
    timestamp: u64,
    extensions: String,
    signature: String,
}

impl SignedCertificateTimestamp {
    fn to_raw_bytes(&self) -> Vec<u8> {
        let mut b = Vec::new();
        b.write_u8(self.sct_version).unwrap();

        let log_id = base64::decode(&self.id).unwrap();
        b.write(&log_id).unwrap();

        b.write_u64::<BigEndian>(self.timestamp).unwrap();

        let extensions = base64::decode(&self.extensions).unwrap();
        assert!(extensions.len() <= 65535);
        b.write_u16::<BigEndian>(extensions.len() as u16)
            .unwrap();
        b.write(&extensions).unwrap();

        let signature = base64::decode(&self.signature).unwrap();
        b.write(&signature).unwrap();

        return b;
    }
}

fn submit_to_log(http_client: &hyper::Client,
                 url: &str,
                 payload: &[u8])
                 -> Option<SignedCertificateTimestamp> {
    let mut url = "https://".to_string() + url;
    if !url.ends_with("/") {
        url += "/";
    }
    url += "ct/v1/add-chain";
    let response = http_client
        .post(&url)
        .body(hyper::client::Body::BufBody(payload, payload.len()))
        .header(hyper::header::ContentType::json())
        .send();
    let response = match response {
        Ok(r) => r,
        // TODO: maybe not all of these should be silently ignored.
        Err(_) => return None,
    };

    // 400, 403, and probably some others generally indicate a log doesn't accept certs from this
    // root, or that the log isn't accepting new submissions.
    if response.status.is_client_error() {
        return None;
    }

    // Limt the response to 10MB (well above what would ever be needed) to be resilient to DoS in
    // the face of a dumb or malicious log.
    return Some(serde_json::from_reader(response.take(10 * 1024 * 1024)).unwrap());
}

#[derive(Serialize)]
struct AddChainRequest {
    chain: Vec<String>,
}

fn submit_cert_to_logs<'a>(http_client: &hyper::Client,
                           logs: &'a [Log],
                           cert: Vec<Vec<u8>>)
                           -> Vec<(&'a Log, SignedCertificateTimestamp)> {
    let payload = serde_json::to_vec(&AddChainRequest {
                                          chain: cert.iter().map(|r| base64::encode(r)).collect(),
                                      })
            .unwrap();

    return logs.par_iter()
               .filter_map(|ref log| {
                               let sct = submit_to_log(http_client, &log.url, &payload);
                               return sct.map(|s| (log.clone(), s));
                           })
               .collect();
}

fn main() {
    let path = match env::args().skip(1).next() {
        Some(p) => p,
        None => {
            println!("Usage: {} <path/to/chain.pem>", env::args().next().unwrap());
            process::exit(1);
        }
    };

    let mut contents = String::new();
    File::open(path)
        .unwrap()
        .read_to_string(&mut contents)
        .unwrap();

    let chain = pem::parse_many(contents)
        .into_iter()
        .filter(|p| p.tag == "CERTIFICATE")
        .map(|p| p.contents)
        .collect();

    let mut http_client = hyper::Client::with_connector(
        hyper::net::HttpsConnector::new(hyper_native_tls::NativeTlsClient::new().unwrap())
    );
    http_client.set_read_timeout(Some(Duration::from_secs(5)));
    let logs = fetch_trusted_ct_logs(&http_client);

    let scts = submit_cert_to_logs(&http_client, &logs, chain);

    let mut table = prettytable::Table::new();
    table.add_row(row!["Log", "SCT"]);
    for (log, sct) in scts {
        table.add_row(row![log.description, base64::encode(&sct.to_raw_bytes())]);
    }
    table.printstd();
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate clap;
extern crate chrono;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;
extern crate colored;

use std::env;
use std::fs;
use std::fs::{OpenOptions, File, Metadata};
use std::io::{Write, ErrorKind};
use std::path::PathBuf;
use std::collections::{HashMap};

use chrono::{DateTime, Date, Utc, Local, Duration};
use clap::{App, Arg, AppSettings, SubCommand};
use serde_json::Error;
use colored::*;

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Period {
    project: String,
    start_time: DateTime<Utc>,
    end_time: Option<DateTime<Utc>>,
}

fn main() {
    let matches = App::new("Doug")
        .version(crate_version!())
        .about("A time tracking command-line utility")
        .author(crate_authors!())
        .settings(&[
            AppSettings::DeriveDisplayOrder,
            AppSettings::GlobalVersion,
            AppSettings::SubcommandRequiredElseHelp,
            AppSettings::VersionlessSubcommands,
            AppSettings::DisableHelpSubcommand,
        ])
        .subcommand(SubCommand::with_name("start")
            .about("Track new or existing project")
            .arg(Arg::with_name("project")
                .help("project to track")
                .required(true)))
        .subcommand(SubCommand::with_name("status")
            .about("Display elapsed time, start time, and running project name"))
        .subcommand(SubCommand::with_name("stop")
            .about("Stop any running projects"))
        .subcommand(SubCommand::with_name("cancel")
            .about("Stop running project and remove most recent time interval"))
        .subcommand(SubCommand::with_name("restart")
            .about("Track last running project"))
        .subcommand(SubCommand::with_name("log")
            .about("Display time intervals across all projects"))
        .subcommand(SubCommand::with_name("report")
            .about("Display aggregate time from last week"))
        .subcommand(SubCommand::with_name("amend")
            .about("Change name of currently running project")
            .arg(Arg::with_name("project")
                .help("new project name")
                .required(true)))
        .subcommand(SubCommand::with_name("edit")
            .about("Edit last frame or currently running frame")
            .arg(Arg::with_name("project")
                .help("project to track")))
        .get_matches();

    if let Some(matches) = matches.subcommand_matches("start") {
        if matches.is_present("project") {
            start(matches.value_of("project").unwrap());
        }
    }

    if let Some(matches) = matches.subcommand_matches("amend") {
        if matches.is_present("project") {
            amend(matches.value_of("project").expect("missing project name"));
        }
    }

    if let Some(matches) = matches.subcommand_matches("edit") {
        edit(matches.value_of("project"));
    }

    match matches.subcommand_name() {
        Some("stop") => stop(),
        Some("cancel") => cancel(),
        Some("restart") => restart(),
        Some("log") => log(),
        Some("report") => report(),
        Some("status") => status(),
        _ => {},
    }
}

fn start(project_name: &str) {
    let mut periods = get_periods();

    if !periods.is_empty() {
        let last_index = periods.len() - 1;
        if let Some(period) = periods.get_mut(last_index) {
            if period.end_time.is_none() {
                let message = format!("project {} is being tracked", period.project);
                eprintln!("Error: {}",  message.red());
                return eprintln!("Try stopping your current project with {} first.", "stop".blue());
            }
        }
    }
    let current_period = create_period(project_name);
    // store current period in file
    print!("Started tracking project {} at {}", current_period.project.blue(), humanize_time(current_period.start_time));
    periods.push(current_period);
    save_periods(periods.to_vec());
}

fn status() {
    let mut periods = get_periods();
    if let Some(period) = periods.pop() {
        if period.end_time.is_none() {
            let diff = Utc::now().signed_duration_since(period.start_time);
            return println!("Project {} started {} ({})", period.project.magenta(), humanize_duration(diff), humanize_datetime(period.start_time).blue());
        }
    }
    eprintln!("No running project");
}

fn stop() {
    let mut periods = get_periods();
    let mut updated_period = false;
    let last_index = periods.len() - 1;
    if periods.len() > 0 {
        if let Some(period) = periods.get_mut(last_index) {
            if period.end_time.is_none() {
                period.end_time = Some(Utc::now());
                updated_period = true;
                let diff = Utc::now().signed_duration_since(period.start_time);
                println!("Stopped project {}, started {}", period.project.blue(), humanize_duration(diff));
            }
        }
    }
    if updated_period {
        save_periods(periods);
    } else {
        eprintln!("Error: {}", "No project started.".red());
    }
}

fn cancel() {
    let mut periods = get_periods();
    if let Some(period) = periods.pop() {
        if period.end_time.is_none() {
            save_periods(periods);
            let diff = Utc::now().signed_duration_since(period.start_time);
            println!("Canceled project {}, started {}", period.project.blue(), humanize_duration(diff));
            return
        }
    }
    eprintln!("Error: {}", "No project started".red());
}

fn restart() {
    let periods = get_periods();
    let mut new_periods = periods.to_vec();
    if let Some(period) = periods.last() {
        if !period.end_time.is_none() {
            let new_period = create_period(&period.project);
            new_periods.push(new_period);
            save_periods(new_periods);
            return println!("Tracking last running project: {}", period.project.blue());
        } else {
            let message = format!("No project to restart. Project {} is being tracked", period.project);
            eprintln!("Error: {}",  message.red());
            return eprintln!("Try stopping your current project with {} first.", "stop".blue());
        }
    }
    eprintln!("Error: {}", "No previous project to restart".red());
}

fn log() {
    let periods = get_periods();
    let mut days: HashMap<Date<chrono::Local>, Vec<Period>> = HashMap::new();

    for period in periods.iter() {
        let time = period.start_time.with_timezone(&Local).date();
        days.entry(time).or_insert(Vec::new()).push(period.clone());
    }
    for (date, day) in days.iter() {
        let d = day.into_iter().fold(Duration::zero(), |acc, ref x| acc + (x.end_time.unwrap_or(Utc::now()).signed_duration_since(x.start_time)));
        println!("{date} ({duration})", date=date.with_timezone(&Local).format("%A %-d %B %Y").to_string().green(), duration=format_duration(d).bold());
        for period in day.iter() {
            match period.end_time {
                Some(end_time) => {
                    let diff = end_time.signed_duration_since(period.start_time);
                    println!("    {start} to {end} {project} {duration}",
                        project=period.project.blue(),
                        start=humanize_time(period.start_time),
                        end=humanize_time(end_time),
                        duration=format_duration(diff));
                },
                None => {
                    let diff = Utc::now().signed_duration_since(period.start_time);
                    println!("    {start} to --:-- {project} {duration}",
                        project=period.project.blue(),
                        start=humanize_time(period.start_time),
                        duration=format_duration(diff));
                },
            }
        }
    }
}

fn humanize_datetime(time: DateTime<Utc>) -> String {
    time.with_timezone(&Local).format("%F %H:%M").to_string()
}

fn humanize_time(time: DateTime<Utc>) -> String {
    time.with_timezone(&Local).format("%H:%M").to_string()
}

fn format_duration(duration: Duration) -> String{
    let days = duration.num_days();
    let hours = duration.num_hours();
    let minutes = duration.num_minutes();
    let seconds = duration.num_seconds();
    if minutes == 0 {
        return format!("{}s", seconds)
    } else if hours == 0 {
        return format!("{minutes}m", minutes=minutes)
    } else if days == 0 {
        return format!("{hours}h, {minutes}m", hours=hours, minutes=minutes/(hours*60))
    } else {
        return format!("{days}d, {hours}h, {minutes}m", hours=hours/(days*24), minutes=minutes/(hours*60), days=days)
    }
}

fn humanize_duration(time: Duration) -> String {
    let hours = time.num_hours();
    let minutes = time.num_minutes();
    let seconds = time.num_seconds();
    if minutes == 0 {
        if seconds < 5 {
            return String::from("just now")
        }
        return String::from("seconds ago")
    } else if hours == 0 {
        if minutes == 1 {
            return format!("{minutes} minute ago", minutes=minutes)
        } else {
            return format!("{minutes} minutes ago", minutes=minutes)
        }
    } else {
        return format!("{hours}:{minutes}", hours=hours, minutes=minutes)
    }
}

fn report() {
    unimplemented!();
}

fn amend(project_name: &str) {
    println!("Amend project: {:?}", project_name);
    unimplemented!();
}

fn edit(project_name: Option<&str>) {
    if let Some(name) = project_name {
        println!("Edit project: {}", name);
    }
    unimplemented!();
}

fn create_period(project: &str) -> Period {
    Period {
        project: String::from(project),
        start_time: Utc::now(),
        end_time: None,
    }
}

fn get_config_folder() -> PathBuf {
    let home_dir = env::var("HOME").expect("Failed to find home directory from environment 'HOME'");
    let mut config_folder = PathBuf::from(home_dir);
    config_folder.push(".config/doug");
    config_folder
}

fn get_config_file_path() -> PathBuf {
    let config_folder = get_config_folder();
    let mut config_file = PathBuf::from(&config_folder);
    config_file.push("periods.json");
    config_file
}

fn get_config_back_file_path() -> PathBuf {
    let config_file = get_config_file_path();
    config_file.with_extension("json-backup")
}

fn get_config_file() -> (File, Metadata) {
    let config_file = get_config_file_path();
    (OpenOptions::new()
        .create(true)
        .read(true)
        .write(true)
        .open(&config_file)
        .expect(&format!("Couldn't open datafile: {:?}", config_file)),
    fs::metadata(&config_file).expect("Couldn't access datafile metadata")
    )
}

fn get_periods() -> Vec<Period> {
    let config_folder = get_config_folder();

    match fs::create_dir(&config_folder) {
        Err(ref error) if error.kind() == ErrorKind::AlreadyExists => {},
        Err(error) => panic!("There was a problem creating the config directory: {:?}", error),
        Ok(_) => {},
    }

    let (file, metadata) = get_config_file();

    let periods: Result<Vec<Period>, Error> = serde_json::from_reader(file);
    let periods = match periods {
        Ok(p) => p,
        Err(_) if metadata.len() > 0 => Vec::new(),
        Err(error) => panic!("There was a serialization issue: {:?}", error),
    };
    periods
}

fn save_periods(periods: Vec<Period>) {
    let serialized = serde_json::to_string(&periods).expect("Couldn't serialize data to string");
    let config_file = get_config_file_path();
    let config_file_backup = get_config_back_file_path();
    fs::copy(&config_file, &config_file_backup).expect("Couldn't create backup file");
    let mut file = OpenOptions::new()
                    .create(true)
                    .write(true)
                    .truncate(true)
                    .open(&config_file)
                    .expect("Couldn't open file for saving period.");

    file.write_all(serialized.as_bytes()).expect("Couldn't write serialized data to file");
}
<commit_msg>Add basic report functionality<commit_after>#[macro_use]
extern crate clap;
extern crate chrono;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;
extern crate colored;

use std::env;
use std::fs;
use std::fs::{OpenOptions, File, Metadata};
use std::io::{Write, ErrorKind};
use std::path::PathBuf;
use std::collections::{HashMap};

use chrono::{DateTime, Date, Utc, Local, Duration};
use clap::{App, Arg, AppSettings, SubCommand};
use serde_json::Error;
use colored::*;

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Period {
    project: String,
    start_time: DateTime<Utc>,
    end_time: Option<DateTime<Utc>>,
}

fn main() {
    let matches = App::new("Doug")
        .version(crate_version!())
        .about("A time tracking command-line utility")
        .author(crate_authors!())
        .settings(&[
            AppSettings::DeriveDisplayOrder,
            AppSettings::GlobalVersion,
            AppSettings::SubcommandRequiredElseHelp,
            AppSettings::VersionlessSubcommands,
            AppSettings::DisableHelpSubcommand,
        ])
        .subcommand(SubCommand::with_name("start")
            .about("Track new or existing project")
            .arg(Arg::with_name("project")
                .help("project to track")
                .required(true)))
        .subcommand(SubCommand::with_name("status")
            .about("Display elapsed time, start time, and running project name"))
        .subcommand(SubCommand::with_name("stop")
            .about("Stop any running projects"))
        .subcommand(SubCommand::with_name("cancel")
            .about("Stop running project and remove most recent time interval"))
        .subcommand(SubCommand::with_name("restart")
            .about("Track last running project"))
        .subcommand(SubCommand::with_name("log")
            .about("Display time intervals across all projects"))
        .subcommand(SubCommand::with_name("report")
            .about("Display aggregate time from projects"))
        .subcommand(SubCommand::with_name("amend")
            .about("Change name of currently running project")
            .arg(Arg::with_name("project")
                .help("new project name")
                .required(true)))
        .subcommand(SubCommand::with_name("edit")
            .about("Edit last frame or currently running frame")
            .arg(Arg::with_name("project")
                .help("project to track")))
        .subcommand(SubCommand::with_name("delete")
            .about("Delete all intervals for project")
            .arg(Arg::with_name("project")
                .help("new project name")
                .required(true)))
        .get_matches();

    if let Some(matches) = matches.subcommand_matches("start") {
        if matches.is_present("project") {
            start(matches.value_of("project").unwrap());
        }
    }

    if let Some(matches) = matches.subcommand_matches("amend") {
        if matches.is_present("project") {
            amend(matches.value_of("project").expect("missing project name"));
        }
    }

    if let Some(matches) = matches.subcommand_matches("edit") {
        edit(matches.value_of("project"));
    }

    if let Some(matches) = matches.subcommand_matches("delete") {
        delete(matches.value_of("project").expect("missing project name"));
    }

    match matches.subcommand_name() {
        Some("status") => status(),
        Some("stop") => stop(),
        Some("cancel") => cancel(),
        Some("restart") => restart(),
        Some("log") => log(),
        Some("report") => report(),
        _ => {},
    }
}

fn start(project_name: &str) {
    let mut periods = get_periods();

    if !periods.is_empty() {
        let last_index = periods.len() - 1;
        if let Some(period) = periods.get_mut(last_index) {
            if period.end_time.is_none() {
                let message = format!("project {} is being tracked", period.project);
                eprintln!("Error: {}",  message.red());
                return eprintln!("Try stopping your current project with {} first.", "stop".blue());
            }
        }
    }
    let current_period = create_period(project_name);
    // store current period in file
    print!("Started tracking project {} at {}", current_period.project.blue(), humanize_time(current_period.start_time));
    periods.push(current_period);
    save_periods(periods.to_vec());
}

fn status() {
    let mut periods = get_periods();
    if let Some(period) = periods.pop() {
        if period.end_time.is_none() {
            let diff = Utc::now().signed_duration_since(period.start_time);
            return println!("Project {} started {} ({})", period.project.magenta(), humanize_duration(diff), humanize_datetime(period.start_time).blue());
        }
    }
    eprintln!("No running project");
}

fn stop() {
    let mut periods = get_periods();
    let mut updated_period = false;
    let last_index = periods.len() - 1;
    if periods.len() > 0 {
        if let Some(period) = periods.get_mut(last_index) {
            if period.end_time.is_none() {
                period.end_time = Some(Utc::now());
                updated_period = true;
                let diff = Utc::now().signed_duration_since(period.start_time);
                println!("Stopped project {}, started {}", period.project.blue(), humanize_duration(diff));
            }
        }
    }
    if updated_period {
        save_periods(periods);
    } else {
        eprintln!("Error: {}", "No project started.".red());
    }
}

fn cancel() {
    let mut periods = get_periods();
    if let Some(period) = periods.pop() {
        if period.end_time.is_none() {
            save_periods(periods);
            let diff = Utc::now().signed_duration_since(period.start_time);
            println!("Canceled project {}, started {}", period.project.blue(), humanize_duration(diff));
            return
        }
    }
    eprintln!("Error: {}", "No project started".red());
}

fn restart() {
    let periods = get_periods();
    let mut new_periods = periods.to_vec();
    if let Some(period) = periods.last() {
        if !period.end_time.is_none() {
            let new_period = create_period(&period.project);
            new_periods.push(new_period);
            save_periods(new_periods);
            return println!("Tracking last running project: {}", period.project.blue());
        } else {
            let message = format!("No project to restart. Project {} is being tracked", period.project);
            eprintln!("Error: {}",  message.red());
            return eprintln!("Try stopping your current project with {} first.", "stop".blue());
        }
    }
    eprintln!("Error: {}", "No previous project to restart".red());
}

fn log() {
    let periods = get_periods();
    let mut days: HashMap<Date<chrono::Local>, Vec<Period>> = HashMap::new();

    for period in periods.iter() {
        let time = period.start_time.with_timezone(&Local).date();
        days.entry(time).or_insert(Vec::new()).push(period.clone());
    }
    for (date, day) in days.iter() {
        let d = day.into_iter().fold(Duration::zero(), |acc, ref x| acc + (x.end_time.unwrap_or(Utc::now()).signed_duration_since(x.start_time)));
        println!("{date} ({duration})", date=date.with_timezone(&Local).format("%A %-d %B %Y").to_string().green(), duration=format_duration(d).bold());
        for period in day.iter() {
            match period.end_time {
                Some(end_time) => {
                    let diff = end_time.signed_duration_since(period.start_time);
                    println!("    {start} to {end} {project} {duration}",
                        project=period.project.blue(),
                        start=humanize_time(period.start_time),
                        end=humanize_time(end_time),
                        duration=format_duration(diff));
                },
                None => {
                    let diff = Utc::now().signed_duration_since(period.start_time);
                    println!("    {start} to --:-- {project} {duration}",
                        project=period.project.blue(),
                        start=humanize_time(period.start_time),
                        duration=format_duration(diff));
                },
            }
        }
    }
}

fn report() {
    let periods = get_periods();
    let mut days: HashMap<String, Vec<Period>> = HashMap::new();
    let mut start_date = Utc::now().with_timezone(&Local).date();
    let mut results = Vec::new();

    for period in periods.iter() {
        days.entry(period.project.clone()).or_insert(Vec::new()).push(period.clone());
    }
    for (project, intervals) in days.iter() {
        let d = intervals.into_iter().fold(Duration::zero(), |acc, ref x| acc + (x.end_time.unwrap_or(Utc::now()).signed_duration_since(x.start_time)));
        for x in intervals.iter() {
            if x.start_time.with_timezone(&Local).date() < start_date {
                start_date = x.start_time.with_timezone(&Local).date();
            }
        }
        results.push(format!("{project} {duration}", project=project.green(), duration=format_duration(d).bold()));
    }
    println!("{start} -> {end}",
        start=start_date.format("%A %-d %B %Y").to_string().blue(),
        end=Utc::now().format("%A %-d %B %Y").to_string().blue());
    for x in results.iter() {
        println!("{}", x);
    }
}

fn amend(project_name: &str) {
    println!("Amend project: {:?}", project_name);
    unimplemented!();
}

fn edit(project_name: Option<&str>) {
    if let Some(name) = project_name {
        println!("Edit project: {}", name);
    }
    unimplemented!();
}

fn delete(project_name: &str) {
    println!("Delete project: {:?}", project_name);
    unimplemented!();
}

fn humanize_datetime(time: DateTime<Utc>) -> String {
    time.with_timezone(&Local).format("%F %H:%M").to_string()
}

fn humanize_time(time: DateTime<Utc>) -> String {
    time.with_timezone(&Local).format("%H:%M").to_string()
}

fn format_duration(duration: Duration) -> String{
    let days = duration.num_days();
    let hours = duration.num_hours();
    let minutes = duration.num_minutes();
    let seconds = duration.num_seconds();
    if minutes == 0 {
        return format!("{}s", seconds)
    } else if hours == 0 {
        return format!("{minutes}m", minutes=minutes)
    } else if days == 0 {
        return format!("{hours}h, {minutes}m", hours=hours, minutes=minutes/(hours*60))
    } else {
        return format!("{days}d, {hours}h, {minutes}m", hours=hours/(days*24), minutes=minutes/(hours*60), days=days)
    }
}

fn humanize_duration(time: Duration) -> String {
    let hours = time.num_hours();
    let minutes = time.num_minutes();
    let seconds = time.num_seconds();
    if minutes == 0 {
        if seconds < 5 {
            return String::from("just now")
        }
        return String::from("seconds ago")
    } else if hours == 0 {
        if minutes == 1 {
            return format!("{minutes} minute ago", minutes=minutes)
        } else {
            return format!("{minutes} minutes ago", minutes=minutes)
        }
    } else {
        return format!("{hours}:{minutes}", hours=hours, minutes=minutes)
    }
}

fn create_period(project: &str) -> Period {
    Period {
        project: String::from(project),
        start_time: Utc::now(),
        end_time: None,
    }
}

fn get_config_folder() -> PathBuf {
    let home_dir = env::var("HOME").expect("Failed to find home directory from environment 'HOME'");
    let mut config_folder = PathBuf::from(home_dir);
    config_folder.push(".config/doug");
    config_folder
}

fn get_config_file_path() -> PathBuf {
    let config_folder = get_config_folder();
    let mut config_file = PathBuf::from(&config_folder);
    config_file.push("periods.json");
    config_file
}

fn get_config_back_file_path() -> PathBuf {
    let config_file = get_config_file_path();
    config_file.with_extension("json-backup")
}

fn get_config_file() -> (File, Metadata) {
    let config_file = get_config_file_path();
    (OpenOptions::new()
        .create(true)
        .read(true)
        .write(true)
        .open(&config_file)
        .expect(&format!("Couldn't open datafile: {:?}", config_file)),
    fs::metadata(&config_file).expect("Couldn't access datafile metadata")
    )
}

fn get_periods() -> Vec<Period> {
    let config_folder = get_config_folder();

    match fs::create_dir(&config_folder) {
        Err(ref error) if error.kind() == ErrorKind::AlreadyExists => {},
        Err(error) => panic!("There was a problem creating the config directory: {:?}", error),
        Ok(_) => {},
    }

    let (file, metadata) = get_config_file();

    let periods: Result<Vec<Period>, Error> = serde_json::from_reader(file);
    let periods = match periods {
        Ok(p) => p,
        Err(_) if metadata.len() > 0 => Vec::new(),
        Err(error) => panic!("There was a serialization issue: {:?}", error),
    };
    periods
}

fn save_periods(periods: Vec<Period>) {
    let serialized = serde_json::to_string(&periods).expect("Couldn't serialize data to string");
    let config_file = get_config_file_path();
    let config_file_backup = get_config_back_file_path();
    fs::copy(&config_file, &config_file_backup).expect("Couldn't create backup file");
    let mut file = OpenOptions::new()
                    .create(true)
                    .write(true)
                    .truncate(true)
                    .open(&config_file)
                    .expect("Couldn't open file for saving period.");

    file.write_all(serialized.as_bytes()).expect("Couldn't write serialized data to file");
}
<|endoftext|>


<commit_before>extern crate r2d2;
extern crate r2d2_postgres;
extern crate openssl;
#[macro_use] extern crate nickel;
extern crate nickel_postgres;
extern crate uuid;
extern crate serde_json;

use std::env;
use std::io::Read;
use r2d2::{NopErrorHandler, PooledConnection};
use r2d2_postgres::{SslMode, PostgresConnectionManager};
use nickel::{Nickel, HttpRouter, QueryString};
use nickel_postgres::{PostgresMiddleware, PostgresRequestExtensions};
use uuid::Uuid;
use nickel::status::StatusCode;
use serde_json::Value;

fn create_tables(conn: PooledConnection<PostgresConnectionManager>) {
    let _r = conn.execute(
            "CREATE TABLE IF NOT EXISTS services (
                key UUID PRIMARY KEY,
                name VARCHAR NOT NULL
                )",
            &[]
        );

    let _r = conn.execute(
            "CREATE TABLE IF NOT EXISTS permissions (
                key UUID REFERENCES services,
                username VARCHAR NOT NULL,
                permissions JSONB,
                PRIMARY KEY (key, username)
                )",
            &[]
        );
}

fn main() {
    let mut app = Nickel::new();

    let postgres_url = env::var("DATABASE_URL")
        .expect("Failed to get DATABASE_URL");

    let dbpool = PostgresMiddleware::new(&*postgres_url,
                                         SslMode::None,
                                         5,
                                         Box::new(NopErrorHandler)).unwrap();

    create_tables(dbpool.pool.clone().get().unwrap());
    app.utilize(dbpool);
    app.get("/register", middleware! { |request, response|
        let conn = request.db_conn();

        let name = match request.query().get("service_name") {
            Some(s) => s,
            None => {
                println!("No service_name, quitting");
                return response.error(StatusCode::BadRequest,
                                      "No service_name specified");
            }
        };

        let key = Uuid::new_v4();

        println!("Name: {}; key: {:?}", &name, &key);

        let r = conn.execute(
            "INSERT INTO services (key, name) VALUES ($1, $2)",
            &[&key, &name]
            );

        println!("{:?}", r);

        key.to_hyphenated_string()
    });

    app.post("/adduser", middleware! { |request, response|
        let conn = request.db_conn();

        let mut body = vec![];
        request.origin.read_to_end(&mut body).unwrap();
        let permissions = match String::from_utf8(body) {
            Ok(s) => s,
            Err(e) => {
                println!("Error decoding input string: {}", e);
                return response.error(StatusCode::InternalServerError,
                                      "Failed");
            }
        };

        let data: Value = serde_json::from_str(&permissions).unwrap();

        let q = request.query();

        // TODO: I'm writting a lot of code like this, to get stuff from the
        // get/post params.  This should become a macro (or something)
        let username = match q.get("username") {
            Some(s) => s,
            None => {
                println!("No username, quitting");
                return response.error(StatusCode::BadRequest,
                                      "No username specified");
            }
        };

        let key = match q.get("key") {
            Some(s) => s,
            None => {
                println!("No key, quitting");
                return response.error(StatusCode::BadRequest,
                                      "No key specified");
            }
        };

        let key = match Uuid::parse_str(key) {
            Ok(u) => u,
            Err(e) => {
                println!("Error with uuid conversion: {}", e);
                return response.error(StatusCode::BadRequest,
                                      "Bad key format");
            }
        };

        let r = conn.execute(
            "INSERT INTO permissions (key, username, permissions)
                VALUES ($1, $2, $3)
                ON CONFLICT (key, username) DO UPDATE SET
                permissions=excluded.permissions",
                &[&key, &username, &data]
            );
        println!("{:?}", r);
        // TODO: add username and permission to the database, if it works
        format!("You posted {}", permissions)
    });

    app.get("/getperms", middleware! { |request, response|
        let conn = request.db_conn();
        let q = request.query();
        let key = match q.get("key") {
            Some(s) => s,
            None => {
                println!("No key, quitting");
                return response.error(StatusCode::BadRequest,
                                      "No key specified");
            }
        };

        let key = Uuid::parse_str(&key).unwrap();
        let username = match q.get("username") {
            Some(s) => s,
            None => {
                println!("No username, quitting");
                return response.error(StatusCode::BadRequest,
                                      "No username specified");
            }
        };

        let r = &conn.query(
            "SELECT permissions FROM permissions
            WHERE key=$1 and username=$2",
            &[&key, &username]
            ).unwrap();

        let row = r.get(0);
        let s: Value = row.get(0);

        format!("{:?}", s)
    });

    let listen = match env::var("LISTEN") {
        Ok(s) => s,
        Err(_) => "127.0.0.1:8080".to_string()
    };

    app.listen(&listen[..]);
}
<commit_msg>Better error handling<commit_after>extern crate r2d2;
extern crate r2d2_postgres;
extern crate openssl;
#[macro_use] extern crate nickel;
extern crate nickel_postgres;
extern crate uuid;
extern crate serde_json;

use std::env;
use std::io::Read;
use r2d2::{NopErrorHandler, PooledConnection};
use r2d2_postgres::{SslMode, PostgresConnectionManager};
use nickel::{Nickel, HttpRouter, QueryString};
use nickel_postgres::{PostgresMiddleware, PostgresRequestExtensions};
use uuid::Uuid;
use nickel::status::StatusCode;
use serde_json::Value;

macro_rules! try_or_return {
    ( $op:expr, $error:expr ) => {
        match $op {
            Some(v) => v,
            None => return $error
        }
    }
}

fn create_tables(conn: PooledConnection<PostgresConnectionManager>) {
    let _r = conn.execute(
            "CREATE TABLE IF NOT EXISTS services (
                key UUID PRIMARY KEY,
                name VARCHAR NOT NULL
                )",
            &[]
        );

    let _r = conn.execute(
            "CREATE TABLE IF NOT EXISTS permissions (
                key UUID REFERENCES services,
                username VARCHAR NOT NULL,
                permissions JSONB,
                PRIMARY KEY (key, username)
                )",
            &[]
        );
}

fn main() {
    let mut app = Nickel::new();

    let postgres_url = env::var("DATABASE_URL")
        .expect("Failed to get DATABASE_URL");

    let dbpool = PostgresMiddleware::new(&*postgres_url,
                                         SslMode::None,
                                         5,
                                         Box::new(NopErrorHandler)).unwrap();

    create_tables(dbpool.pool.clone().get().unwrap());
    app.utilize(dbpool);
    app.get("/register", middleware! { |request, response|
        let conn = request.db_conn();

        let name = try_or_return!(request.query().get("service_name"),
            response.error(
                StatusCode::BadRequest, "No service_name specified"));

        let key = Uuid::new_v4();

        let r = conn.execute(
            "INSERT INTO services (key, name) VALUES ($1, $2)",
            &[&key, &name]
            );

        println!("{:?}", r);

        key.to_hyphenated_string()
    });

    app.post("/adduser", middleware! { |request, response|
        let conn = request.db_conn();

        let mut body = vec![];
        try_or_return!(request.origin.read_to_end(&mut body).ok(),
                       response.error(StatusCode::InternalServerError,
                                      "Failed"));

        let permissions = try_or_return!(String::from_utf8(body).ok(),
            response.error(StatusCode::InternalServerError, "Failed"));

        let data: Value = try_or_return!(
            serde_json::from_str(&permissions).ok(),
            response.error(StatusCode::InternalServerError, "Failed"));

        let q = request.query();

        let username = try_or_return!(q.get("username"),
            response.error(StatusCode::BadRequest, "No username specified"));
        let key = try_or_return!(q.get("key").map(|s| Uuid::parse_str(s).ok()),
            response.error(StatusCode::BadRequest, "No key specified"));

        let r = conn.execute(
            "INSERT INTO permissions (key, username, permissions)
                VALUES ($1, $2, $3)
                ON CONFLICT (key, username) DO UPDATE SET
                permissions=excluded.permissions",
                &[&key, &username, &data]
            );
        println!("{:?}", r); // TODO: something with r
        format!("You posted {}", permissions)
    });

    app.get("/getperms", middleware! { |request, response|
        let conn = request.db_conn();
        let q = request.query();
        let key = try_or_return!(q.get("key").map(|s| Uuid::parse_str(s).ok()),
            response.error(StatusCode::BadRequest, "No key specified"));

        let username = try_or_return!(q.get("username"),
            response.error(StatusCode::BadRequest, "No username specified"));

        let r = try_or_return!(conn.query(
            "SELECT permissions FROM permissions
            WHERE key=$1 and username=$2",
            &[&key, &username]
            ).ok(), response.error(StatusCode::InternalServerError,
                              "Failed"));

        let row = r.get(0);
        let s: Value = row.get(0);

        format!("{:?}", s)
    });

    let listen = match env::var("LISTEN") {
        Ok(s) => s,
        Err(_) => "127.0.0.1:8080".to_string()
    };

    app.listen(&listen[..]);
}
<|endoftext|>


<commit_before>use std::io;
use std::io::prelude::*;
use std::net::TcpStream;
use std::fs::File;
use std::env;

extern crate chrono;
use chrono::*;

extern crate stats;

extern crate clap;
use clap::{Arg, App, ArgMatches};

mod http_status;
mod log_parser;
use log_parser::parse_line;
use log_parser::log_events::*;
mod request_response_matcher;
use request_response_matcher::*;
mod filter;
mod analyzer;
mod render;

// http://stackoverflow.com/a/27590832/376138
macro_rules! println_stderr(
    ($($arg:tt)*) => { {
        let r = writeln!(&mut ::std::io::stderr(), $($arg)*);
        r.expect("failed printing to stderr");
    } }
);

#[derive(Debug)]
#[derive(PartialEq)]
struct RequestLogAnalyzerArgs {
    filename: String,
    conditions: filter::FilterConditions,
    graphite_server: Option<String>,
    graphite_port: Option<u16>,
    graphite_prefix: Option<String>,
}

fn parse_args<'a, T>(args: T) -> Result<RequestLogAnalyzerArgs, &'a str> where T: IntoIterator<Item=String> {
    let app = App::new("Request.log Analyzer")
        .arg(Arg::with_name("filename")
            .index(1)
            .value_name("FILE")
            .required(false)
            .help("Log file to analyze, defaults to stdin")
            .takes_value(true))
        .arg(Arg::with_name("time_filter_minutes")
            .value_name("MINUTES")
            .short("t")
            .help("Limit to the last n minutes")
            .takes_value(true))
        .arg(Arg::with_name("include_term")
            .value_name("TERM")
            .long("include")
            .help("Only includes lines that contain this term")
            .takes_value(true))
        .arg(Arg::with_name("exclude_term")
            .value_name("TERM")
            .long("exclude")
            .help("Excludes lines that contain this term")
            .takes_value(true))
        .arg(Arg::with_name("graphite-server")
            .value_name("GRAPHITE_SERVER")
            .long("graphite-server")
            .help("Send values to this Graphite server instead of stdout")
            .takes_value(true))
        .arg(Arg::with_name("graphite-port")
            .value_name("GRAPHITE_PORT")
            .long("graphite-port")
            .takes_value(true)
            .default_value("2003"))
        .arg(Arg::with_name("graphite-prefix")
            .value_name("GRAPHITE_PREFIX")
            .long("graphite-prefix")
            .help("Prefix for Graphite key, e.g. 'servers.prod.publisher1'")
            .takes_value(true))
        .get_matches_from(args);

        let filename = app.value_of("filename").unwrap_or("-").to_string();

        let conditions = filter::FilterConditions {
            include_terms: match app.value_of("include_term") {
                Some(value) => Some(vec![value.to_string()]),
                None => None,
            },
            exclude_terms: match app.value_of("exclude_term") {
                Some(value) => Some(vec![value.to_string()]),
                None => None,
            },
            latest_time: match app.value_of("time_filter_minutes") {
                Some(minutes) => Some(Duration::minutes(minutes.parse().expect("Minutes value must be numeric"))),
                None => None,
            },
        };

        let graphite_server = match app.value_of("graphite-server") {
            Some(value) => Some(String::from(value)),
            None => None,
        };

        let graphite_port: Option<u16> = match app.value_of("graphite-port") {
            Some(value) => Some(value.parse().expect("Port number must be numeric.")),
            None => None,
        };

        let graphite_prefix = match app.value_of("graphite-prefix") {
            Some(value) => Some(String::from(value)),
            None => None,
        };

        Ok(RequestLogAnalyzerArgs {
            filename: filename,
            conditions: conditions,
            graphite_server: graphite_server,
            graphite_port: graphite_port,
            graphite_prefix: graphite_prefix,
        })
}

fn main() {
    let args = parse_args(env::args()).unwrap();

    let input: Box<io::Read> = match args.filename.as_ref() {
        "-" => Box::new(io::stdin()),
        _ => Box::new(File::open(&args.filename).unwrap()),
    };

    let reader = io::BufReader::new(input);
    let lines = reader.lines();

    let mut requests: Vec<Request> = Vec::new();
    let mut responses: Vec<Response> = Vec::new();
    let mut times: Vec<i64> = Vec::new();

    // We need to store 1 Request in order to determine the timezone later
    let mut first_request: Option<Request> = None;

    for line in lines {
        let line_value = &line.unwrap();

        let event = parse_line(line_value).unwrap();

        match event {
            LogEvent::Request(request) => {
                if first_request.is_none() {
                    first_request = Some(request.clone());
                }
                requests.push(request)
            }
            LogEvent::Response(response) => responses.push(response),
        }

        let pairs = extract_matching_request_response_pairs(&mut requests, &mut responses);

        let mut new_times: Vec<i64> = pairs.iter()
            .filter(|pair| filter::matches_filter(&pair, &args.conditions))
            .map(|pair| pair.response.response_time.num_milliseconds())
            .collect();

        times.append(&mut new_times);
    }

    let result = analyzer::analyze(&times);

    match result {
        Some(result) => {
            if args.graphite_server.is_some() {
                let stream =
                    TcpStream::connect((args.graphite_server.unwrap().as_ref(), args.graphite_port.unwrap()))
                        .expect("Could not connect to the Graphite server");
                let timezone = first_request.unwrap().time.timezone();

                render::render_graphite(result,
                                        UTC::now().with_timezone(&timezone),
                                        args.graphite_prefix,
                                        stream);
            } else {
                render::render_terminal(result);
            }
        }
        None => println_stderr!("No matching log lines in file."),
    }
}

#[test]
fn test_parse_args_default() {
    let raw_args = vec!["request_log_analyzer".to_string()];

    let expected = RequestLogAnalyzerArgs {
        filename: String::from("-"),
        conditions: filter::FilterConditions {
            include_terms: None,
            exclude_terms: None,
            latest_time: None,
        },
        graphite_server: None,
        graphite_port: Some(2003),
        graphite_prefix: None,
    };

    let result = parse_args(raw_args).unwrap();

    assert_eq!(result, expected);
}

#[test]
fn test_parse_args_all() {
    let raw_args = vec![String::from("request_log_analyzer"),
                        String::from("--include"), String::from("one"),
                        String::from("--exclude"), String::from("this other"),
                        String::from("-t"), String::from("10"),
                        String::from("my-logfile.log"),
                        String::from("--graphite-server"), String::from("localhost"),
                        String::from("--graphite-port"), String::from("4000"),
                        String::from("--graphite-prefix"), String::from("prod"),
                        ];

    let expected = RequestLogAnalyzerArgs {
        filename: String::from("my-logfile.log"),
        conditions: filter::FilterConditions {
            include_terms: Some(vec![String::from("one")]),
            exclude_terms: Some(vec![String::from("this other")]),
            latest_time: Some(Duration::minutes(10)),
        },
        graphite_server: Some(String::from("localhost")),
        graphite_port: Some(4000),
        graphite_prefix: Some(String::from("prod")),
    };

    let result = parse_args(raw_args).unwrap();

    assert_eq!(result, expected);
}
<commit_msg>Fix code formatting<commit_after>use std::io;
use std::io::prelude::*;
use std::net::TcpStream;
use std::fs::File;
use std::env;

extern crate chrono;
use chrono::*;

extern crate stats;

extern crate clap;
use clap::{Arg, App};

mod http_status;
mod log_parser;
use log_parser::parse_line;
use log_parser::log_events::*;
mod request_response_matcher;
use request_response_matcher::*;
mod filter;
mod analyzer;
mod render;

// http://stackoverflow.com/a/27590832/376138
macro_rules! println_stderr(
    ($($arg:tt)*) => { {
        let r = writeln!(&mut ::std::io::stderr(), $($arg)*);
        r.expect("failed printing to stderr");
    } }
);

#[derive(Debug)]
#[derive(PartialEq)]
struct RequestLogAnalyzerArgs {
    filename: String,
    conditions: filter::FilterConditions,
    graphite_server: Option<String>,
    graphite_port: Option<u16>,
    graphite_prefix: Option<String>,
}

fn parse_args<'a, T>(args: T) -> Result<RequestLogAnalyzerArgs, &'a str>
    where T: IntoIterator<Item = String>
{
    let app = App::new("Request.log Analyzer")
        .arg(Arg::with_name("filename")
            .index(1)
            .value_name("FILE")
            .required(false)
            .help("Log file to analyze, defaults to stdin")
            .takes_value(true))
        .arg(Arg::with_name("time_filter_minutes")
            .value_name("MINUTES")
            .short("t")
            .help("Limit to the last n minutes")
            .takes_value(true))
        .arg(Arg::with_name("include_term")
            .value_name("TERM")
            .long("include")
            .help("Only includes lines that contain this term")
            .takes_value(true))
        .arg(Arg::with_name("exclude_term")
            .value_name("TERM")
            .long("exclude")
            .help("Excludes lines that contain this term")
            .takes_value(true))
        .arg(Arg::with_name("graphite-server")
            .value_name("GRAPHITE_SERVER")
            .long("graphite-server")
            .help("Send values to this Graphite server instead of stdout")
            .takes_value(true))
        .arg(Arg::with_name("graphite-port")
            .value_name("GRAPHITE_PORT")
            .long("graphite-port")
            .takes_value(true)
            .default_value("2003"))
        .arg(Arg::with_name("graphite-prefix")
            .value_name("GRAPHITE_PREFIX")
            .long("graphite-prefix")
            .help("Prefix for Graphite key, e.g. 'servers.prod.publisher1'")
            .takes_value(true))
        .get_matches_from(args);

    let filename = app.value_of("filename").unwrap_or("-").to_string();

    let conditions = filter::FilterConditions {
        include_terms: match app.value_of("include_term") {
            Some(value) => Some(vec![value.to_string()]),
            None => None,
        },
        exclude_terms: match app.value_of("exclude_term") {
            Some(value) => Some(vec![value.to_string()]),
            None => None,
        },
        latest_time: match app.value_of("time_filter_minutes") {
            Some(minutes) => {
                Some(Duration::minutes(minutes.parse().expect("Minutes value must be numeric")))
            }
            None => None,
        },
    };

    let graphite_server = match app.value_of("graphite-server") {
        Some(value) => Some(String::from(value)),
        None => None,
    };

    let graphite_port: Option<u16> = match app.value_of("graphite-port") {
        Some(value) => Some(value.parse().expect("Port number must be numeric.")),
        None => None,
    };

    let graphite_prefix = match app.value_of("graphite-prefix") {
        Some(value) => Some(String::from(value)),
        None => None,
    };

    Ok(RequestLogAnalyzerArgs {
        filename: filename,
        conditions: conditions,
        graphite_server: graphite_server,
        graphite_port: graphite_port,
        graphite_prefix: graphite_prefix,
    })
}

fn main() {
    let args = parse_args(env::args()).unwrap();

    let input: Box<io::Read> = match args.filename.as_ref() {
        "-" => Box::new(io::stdin()),
        _ => Box::new(File::open(&args.filename).unwrap()),
    };

    let reader = io::BufReader::new(input);
    let lines = reader.lines();

    let mut requests: Vec<Request> = Vec::new();
    let mut responses: Vec<Response> = Vec::new();
    let mut times: Vec<i64> = Vec::new();

    // We need to store 1 Request in order to determine the timezone later
    let mut first_request: Option<Request> = None;

    for line in lines {
        let line_value = &line.unwrap();

        let event = parse_line(line_value).unwrap();

        match event {
            LogEvent::Request(request) => {
                if first_request.is_none() {
                    first_request = Some(request.clone());
                }
                requests.push(request)
            }
            LogEvent::Response(response) => responses.push(response),
        }

        let pairs = extract_matching_request_response_pairs(&mut requests, &mut responses);

        let mut new_times: Vec<i64> = pairs.iter()
            .filter(|pair| filter::matches_filter(&pair, &args.conditions))
            .map(|pair| pair.response.response_time.num_milliseconds())
            .collect();

        times.append(&mut new_times);
    }

    let result = analyzer::analyze(&times);

    match result {
        Some(result) => {
            if args.graphite_server.is_some() {
                let stream = TcpStream::connect((args.graphite_server.unwrap().as_ref(),
                                                 args.graphite_port.unwrap()))
                    .expect("Could not connect to the Graphite server");
                let timezone = first_request.unwrap().time.timezone();

                render::render_graphite(result,
                                        UTC::now().with_timezone(&timezone),
                                        args.graphite_prefix,
                                        stream);
            } else {
                render::render_terminal(result);
            }
        }
        None => println_stderr!("No matching log lines in file."),
    }
}

#[test]
fn test_parse_args_default() {
    let raw_args = vec!["request_log_analyzer".to_string()];

    let expected = RequestLogAnalyzerArgs {
        filename: String::from("-"),
        conditions: filter::FilterConditions {
            include_terms: None,
            exclude_terms: None,
            latest_time: None,
        },
        graphite_server: None,
        graphite_port: Some(2003),
        graphite_prefix: None,
    };

    let result = parse_args(raw_args).unwrap();

    assert_eq!(result, expected);
}

#[test]
fn test_parse_args_all() {
    let raw_args = vec![String::from("request_log_analyzer"),
                        String::from("--include"), String::from("one"),
                        String::from("--exclude"), String::from("this other"),
                        String::from("-t"), String::from("10"),
                        String::from("my-logfile.log"),
                        String::from("--graphite-server"), String::from("localhost"),
                        String::from("--graphite-port"), String::from("4000"),
                        String::from("--graphite-prefix"), String::from("prod"),
                        ];

    let expected = RequestLogAnalyzerArgs {
        filename: String::from("my-logfile.log"),
        conditions: filter::FilterConditions {
            include_terms: Some(vec![String::from("one")]),
            exclude_terms: Some(vec![String::from("this other")]),
            latest_time: Some(Duration::minutes(10)),
        },
        graphite_server: Some(String::from("localhost")),
        graphite_port: Some(4000),
        graphite_prefix: Some(String::from("prod")),
    };

    let result = parse_args(raw_args).unwrap();

    assert_eq!(result, expected);
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate clap;

mod wiz;

use clap::{App, Arg};

arg_enum! {
    #[derive(Debug)]
    enum Method {
        Install,
        Remove
    }
}

fn main() {
    let arg_matches = App::new("wiz")
        .version("0.1.0")

        .arg(Arg::with_name("method")
             .help("Specify what to do with the specified package")
             .index(1)
             .possible_values(&Method::variants())
             .required(true))

        .arg(Arg::with_name("package")
             .help("Specify package")
             .index(2)
             .required(true))

        .get_matches();

    let method = value_t!(arg_matches.value_of("method"), Method).unwrap();
    let package = arg_matches.value_of("package").unwrap_or("invalid");

    match method {
        Method::Install => println!("install: {}", package),
        Method::Remove => println!("remove: {}", package)
    }
}
<commit_msg>Added a todo<commit_after>#[macro_use]
extern crate clap;

mod wiz;
mod constants;

use clap::{App, Arg};

arg_enum! {
    #[derive(Debug)]
    enum Method {
        Install,
        Remove
    }
}

fn main() {

/*
 * TODO: Eliminate hardcoded strings, instead
 * putting them in constants.rs or other places.
 */

    let arg_matches = App::new("wiz")
        .version("0.1.0")

        .arg(Arg::with_name("method")
             .help("Specify what to do with the specified package")
             .index(1)
             .possible_values(&Method::variants())
             .required(true))

        .arg(Arg::with_name("package")
             .help("Specify package")
             .index(2)
             .required(true))

        .get_matches();

    let method = value_t!(arg_matches.value_of("method"), Method).unwrap();
    let package = arg_matches.value_of("package").unwrap_or("invalid");

    match method {
        Method::Install => println!("install: {}", package),
        Method::Remove => println!("remove: {}", package)
    }
}
<|endoftext|>


<commit_before>//! # Hexdino
//!
//! A hex editor with vim like keybindings written in Rust.

#![doc(html_logo_url = "https://raw.githubusercontent.com/Luz/hexdino/master/logo.png")]

use std::io::prelude::*;
use std::fs::OpenOptions;
use std::io::SeekFrom;
use std::path::Path;
use std::error::Error;
use std::env;

mod draw;
use draw::draw;
use draw::get_absolute_draw_indices;
mod find;
use find::FindOptSubset;

extern crate ncurses;
use ncurses::*;

extern crate getopts;
use getopts::Options;

extern crate pest;
#[macro_use]
extern crate pest_derive;

use pest::Parser;

#[derive(Parser)]
#[grammar = "cmd.pest"]
struct IdentParser;

extern crate memmem;
use memmem::{Searcher, TwoWaySearcher};

#[derive(PartialEq, Copy, Clone)]
pub enum Cursorstate {
    Leftnibble,
    Rightnibble,
    Asciichar,
}

fn main() {
    const VERSION: &str = env!("CARGO_PKG_VERSION");
    let mut buf = vec![];
    let mut cursorpos: usize = 0;
    let mut cstate: Cursorstate = Cursorstate::Leftnibble;
    // 0 = display data from first line of file
    let mut screenoffset: usize = 0;
    const SPALTEN: usize = 16;
    let mut command = String::new();
    let mut debug = String::new();

    // start ncursesw
    initscr();
    let screenheight = getmaxy(stdscr()) as usize;
    // ctrl+z and fg works with this
    cbreak();
    noecho();
    start_color();
    init_pair(1, COLOR_GREEN, COLOR_BLACK);

    let args: Vec<_> = env::args().collect();
    let program = args[0].clone();
    let mut opts = Options::new();
    opts.optflag("h", "help", "print this help menu");
    opts.optflag("v", "version", "print the version");
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            println!("{}", f.to_string());
            println!("Usage: {} FILE [options]", program);
            endwin();
            return;
        }
    };
    if matches.opt_present("v") {
        println!("Version: {}", VERSION);
        endwin();
        return;
    }
    if matches.opt_present("h") {
        println!("Usage: {} FILE [options]", program);
        endwin();
        return;
    }

    if !has_colors() {
        endwin();
        println!("Your terminal does not support color!\n");
        return;
    }

    let patharg = match matches.free.is_empty() {
        true => String::new(),
        false => matches.free[0].clone(),
    };
    let path = Path::new(&patharg);

    if !patharg.is_empty() {
        let mut file = match OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(&path) {
            Err(why) => {
                println!("Could not open {}: {}", path.display(), why.to_string());
                endwin();
                return;
            }
            Ok(file) => file,
        };
        file.read_to_end(&mut buf).ok().expect(
            "File could not be read.",
        );
    } else {
        command.push_str("File create failed, no file name given.");
    }

    let draw_range = get_absolute_draw_indices(buf.len(), SPALTEN, screenoffset);
    draw(&buf[draw_range.0 .. draw_range.1], cursorpos, SPALTEN, &command, &mut debug, cstate, screenoffset);

    let mut quitnow = false;
    while quitnow == false {
        let key = std::char::from_u32(getch() as u32).unwrap();
        printw(&format!("   {:?}   ", key));
        command.push_str(&key.clone().to_string());

        let parsethisstring = command.clone();
        let commands = IdentParser::parse(Rule::cmd_list, &parsethisstring)
            .unwrap_or_else(|e| panic!("{}", e));

        let mut clear = true;
        let mut save = false;
        for cmd in commands {
            match cmd.as_rule() {
                Rule::down => {
                    printw(&format!("{:?}", cmd.as_rule()));
                    if cursorpos + SPALTEN < buf.len() {
                        // not at end
                        cursorpos += SPALTEN;
                    } else {
                        // when at end
                        if buf.len() != 0 {
                            // Suppress underflow
                            cursorpos = buf.len() - 1;
                        }
                    }
                }
                Rule::up => {
                    if cursorpos >= SPALTEN {
                        cursorpos -= SPALTEN;
                    }
                }
                Rule::left => {
                    if cstate == Cursorstate::Asciichar {
                        if cursorpos > 0 {
                            cursorpos -= 1;
                        }
                    } else if cstate == Cursorstate::Rightnibble {
                        cstate = Cursorstate::Leftnibble;
                    } else if cstate == Cursorstate::Leftnibble {
                        if cursorpos > 0 {
                            // not at start
                            cstate = Cursorstate::Rightnibble;
                            cursorpos -= 1;
                        }
                    }
                }
                Rule::right => {
                    if cstate == Cursorstate::Asciichar {
                        if cursorpos + 1 < buf.len() {
                            // not at end
                            cursorpos += 1;
                        }
                    } else if cstate == Cursorstate::Leftnibble {
                        cstate = Cursorstate::Rightnibble;
                    } else if cstate == Cursorstate::Rightnibble {
                        if cursorpos + 1 < buf.len() {
                            // not at end
                            cstate = Cursorstate::Leftnibble;
                            cursorpos += 1;
                        }
                    }
                }
                Rule::start => {
                    cursorpos -= cursorpos % SPALTEN; // jump to start of line
                    if cstate == Cursorstate::Rightnibble {
                        cstate = Cursorstate::Leftnibble;
                    }
                }
                Rule::end => {
                    // check if no overflow
                    if cursorpos - (cursorpos % SPALTEN) + (SPALTEN - 1) < buf.len() {
                        // jump to end of line
                        cursorpos = cursorpos - (cursorpos % SPALTEN) + (SPALTEN - 1);
                    } else {
                        // jump to end of line
                        cursorpos = buf.len() - 1
                    }
                    if cstate == Cursorstate::Leftnibble {
                        cstate = Cursorstate::Rightnibble;
                    }
                }
                Rule::top => {
                    cursorpos = 0;
                }
                Rule::bottom => {
                    cursorpos = buf.len() - 1;
                    cursorpos -= cursorpos % SPALTEN; // jump to start of line
                }
                Rule::replace => {
                    // printw("next char will be the replacement!");
                    clear = false;
                }
                Rule::remove => {
                    // check if in valid range
                    if buf.len() > 0 && cursorpos < buf.len() {
                        // remove the current char
                        buf.remove(cursorpos);
                    }
                    // always perform the movement if possible
                    if cursorpos > 0 && cursorpos >= buf.len() {
                        cursorpos -= 1;
                    }
                }
                Rule::insert => {
                    printw("next chars will be inserted!");
                    clear = false;
                }
                Rule::jumpascii => {
                    if cstate == Cursorstate::Asciichar {
                        cstate = Cursorstate::Leftnibble;
                    } else {
                        cstate = Cursorstate::Asciichar;
                    }
                }
                Rule::helpfile => {
                    command.push_str("No helpfile yet");
                }
                Rule::backspace => {
                    command.pop();
                    command.pop();
                    clear = false;
                }
                Rule::saveandexit => {
                    save = true;
                    quitnow = true;
                }
                Rule::exit => quitnow = true,
                Rule::save => save = true,

                _ => (),
            }

            for inner_cmd in cmd.into_inner() {
                match inner_cmd.as_rule() {
                    Rule::replacement => {
                        // TODO: use inner_cmd and not just "key"
                        // printw(&format!("Replacement: {:?}", inner_cmd.as_str()));
                        if cstate == Cursorstate::Asciichar {
                            if cursorpos >= buf.len() {
                                buf.insert(cursorpos, 0);
                            }
                            // buf[cursorpos] = inner_cmd.as_str();
                            buf[cursorpos] = key as u8;
                        } else {
                            let mask = if cstate == Cursorstate::Leftnibble {
                                0x0F
                            } else {
                                0xF0
                            };
                            let shift = if cstate == Cursorstate::Leftnibble {
                                4
                            } else {
                                0
                            };
                            if cursorpos >= buf.len() {
                                buf.insert(cursorpos, 0);
                            }
                            // Change the selected nibble
                            if let Some(c) = key.to_digit(16) {
                                buf[cursorpos] = buf[cursorpos] & mask | (c as u8) << shift;
                            }
                        }

                    }
                    // TODO: use inner_cmd and not just "key"
                    Rule::insertment => {
                        // printw(&format!("Inserted: {:?}", inner_cmd.as_str()));
                        command.pop(); // remove the just inserted thing
                        clear = false;

                        if cstate == Cursorstate::Leftnibble {
                            // Left nibble
                            if let Some(c) = (key as char).to_digit(16) {
                                buf.insert(cursorpos, (c as u8) << 4);
                                cstate = Cursorstate::Rightnibble;
                            }
                        } else if cstate == Cursorstate::Rightnibble {
                            // Right nibble
                            if cursorpos == buf.len() {
                                buf.insert(cursorpos, 0);
                            }
                            if let Some(c) = (key as char).to_digit(16) {
                                buf[cursorpos] = buf[cursorpos] & 0xF0 | c as u8;
                                cstate = Cursorstate::Leftnibble;
                                cursorpos += 1;
                            }
                        } else if cstate == Cursorstate::Asciichar {
                            buf.insert(cursorpos, key as u8);
                            cursorpos += 1;
                        }
                    }
                    Rule::searchstr => {
                        let search = inner_cmd.as_str().as_bytes();
                        let foundpos = TwoWaySearcher::new(&search);
                        cursorpos = foundpos.search_in(&buf).unwrap_or(cursorpos);
                    }
                    Rule::searchbytes => {
                        let search = inner_cmd.as_str().as_bytes();
                        let mut needle = vec![];
                        for i in 0..search.len() {
                            let nibble = match search[i] as u8 {
                                c @ 48...57 => c - 48, // Numbers from 0 to 9
                                b'x' => 0x10, // x is the wildcard
                                b'X' => 0x10, // X is the wildcard
                                c @ b'a'...b'f' => c - 87,
                                c @ b'A'...b'F' => c - 55,
                                _ => panic!("Should not get to this position!"),
                            };
                            needle.push(nibble);
                        }
                        cursorpos = buf.find_subset(&needle).unwrap_or(cursorpos);
                        // endwin(); println!("Searching for: {:?}", needle ); return;
                    }
                    Rule::linenumber => {
                        let linenr: usize = inner_cmd.as_str().parse().unwrap();
                        cursorpos = linenr * SPALTEN; // jump to the line
                        if cursorpos > buf.len() { // detect file end
                            cursorpos = buf.len();
                        }
                        cursorpos -= cursorpos % SPALTEN; // jump to start of line
                    }
                    Rule::escape => (),
                    Rule::gatherone => clear = false,
                    _ => {
                        command.push_str(&format!("no rule for {:?} ", inner_cmd.as_rule()));
                        clear = false;
                    }
                };
            }
            if save {
                if path.exists() {
                    let mut file = match OpenOptions::new()
                        .read(true)
                        .write(true)
                        .create(true)
                        .open(&path) {
                        Err(why) => {
                            panic!(
                                "Could not open {}: {}",
                                path.display(),
                                Error::description(&why)
                            )
                        }
                        Ok(file) => file,
                    };
                    file.seek(SeekFrom::Start(0)).ok().expect(
                        "Filepointer could not be set to 0",
                    );
                    file.write_all(&mut buf).ok().expect(
                        "File could not be written.",
                    );
                    file.set_len(buf.len() as u64).ok().expect(
                        "File could not be set to correct lenght.",
                    );
                    command.push_str("File saved!");
                } else {
                    command.push_str("Careful, file could not be saved!");
                }
                // TODO: define filename during runtime
                save = false;
            }
            if clear {
                command.clear();
            }

            // Always move screen when cursor leaves screen
            if cursorpos > (screenheight + screenoffset - 1) * SPALTEN - 1 {
                screenoffset = 2 + cursorpos / SPALTEN - screenheight;
            }
            if cursorpos < screenoffset * SPALTEN {
                screenoffset = cursorpos / SPALTEN;
            }

        }

    let draw_range = get_absolute_draw_indices(buf.len(), SPALTEN, screenoffset);
        draw(&buf[draw_range.0 .. draw_range.1], cursorpos, SPALTEN, &command, &mut debug, cstate, screenoffset);
    }

    refresh();
    endwin();
}
<commit_msg>Add earlier return without path argument<commit_after>//! # Hexdino
//!
//! A hex editor with vim like keybindings written in Rust.

#![doc(html_logo_url = "https://raw.githubusercontent.com/Luz/hexdino/master/logo.png")]

use std::io::prelude::*;
use std::fs::OpenOptions;
use std::io::SeekFrom;
use std::path::Path;
use std::error::Error;
use std::env;

mod draw;
use draw::draw;
use draw::get_absolute_draw_indices;
mod find;
use find::FindOptSubset;

extern crate ncurses;
use ncurses::*;

extern crate getopts;
use getopts::Options;

extern crate pest;
#[macro_use]
extern crate pest_derive;

use pest::Parser;

#[derive(Parser)]
#[grammar = "cmd.pest"]
struct IdentParser;

extern crate memmem;
use memmem::{Searcher, TwoWaySearcher};

#[derive(PartialEq, Copy, Clone)]
pub enum Cursorstate {
    Leftnibble,
    Rightnibble,
    Asciichar,
}

fn main() {
    const VERSION: &str = env!("CARGO_PKG_VERSION");
    let mut buf = vec![];
    let mut cursorpos: usize = 0;
    let mut cstate: Cursorstate = Cursorstate::Leftnibble;
    // 0 = display data from first line of file
    let mut screenoffset: usize = 0;
    const SPALTEN: usize = 16;
    let mut command = String::new();
    let mut debug = String::new();

    // start ncursesw
    initscr();
    let screenheight = getmaxy(stdscr()) as usize;
    // ctrl+z and fg works with this
    cbreak();
    noecho();
    start_color();
    init_pair(1, COLOR_GREEN, COLOR_BLACK);

    let args: Vec<_> = env::args().collect();
    let program = args[0].clone();
    let mut opts = Options::new();
    opts.optflag("h", "help", "print this help menu");
    opts.optflag("v", "version", "print the version");
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            println!("{}", f.to_string());
            println!("Usage: {} FILE [options]", program);
            endwin();
            return;
        }
    };
    if matches.opt_present("v") {
        println!("Version: {}", VERSION);
        endwin();
        return;
    }
    if matches.opt_present("h") {
        println!("Usage: {} FILE [options]", program);
        endwin();
        return;
    }

    if !has_colors() {
        endwin();
        println!("Your terminal does not support color!\n");
        return;
    }

    let patharg = match matches.free.is_empty() {
        true => String::new(),
        false => matches.free[0].clone(),
    };
    let path = Path::new(&patharg);

    if patharg.is_empty() {
        endwin();
        println!("Patharg is empty!\n");
        return;
    }

    let mut file = match OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open(&path) {
        Err(why) => {
            println!("Could not open {}: {}", path.display(), why.to_string());
            endwin();
            return;
        }
        Ok(file) => file,
    };
    file.read_to_end(&mut buf).ok().expect(
        "File could not be read.",
    );

    let draw_range = get_absolute_draw_indices(buf.len(), SPALTEN, screenoffset);
    draw(&buf[draw_range.0 .. draw_range.1], cursorpos, SPALTEN, &command, &mut debug, cstate, screenoffset);

    let mut quitnow = false;
    while quitnow == false {
        let key = std::char::from_u32(getch() as u32).unwrap();
        printw(&format!("   {:?}   ", key));
        command.push_str(&key.clone().to_string());

        let parsethisstring = command.clone();
        let commands = IdentParser::parse(Rule::cmd_list, &parsethisstring)
            .unwrap_or_else(|e| panic!("{}", e));

        let mut clear = true;
        let mut save = false;
        for cmd in commands {
            match cmd.as_rule() {
                Rule::down => {
                    printw(&format!("{:?}", cmd.as_rule()));
                    if cursorpos + SPALTEN < buf.len() {
                        // not at end
                        cursorpos += SPALTEN;
                    } else {
                        // when at end
                        if buf.len() != 0 {
                            // Suppress underflow
                            cursorpos = buf.len() - 1;
                        }
                    }
                }
                Rule::up => {
                    if cursorpos >= SPALTEN {
                        cursorpos -= SPALTEN;
                    }
                }
                Rule::left => {
                    if cstate == Cursorstate::Asciichar {
                        if cursorpos > 0 {
                            cursorpos -= 1;
                        }
                    } else if cstate == Cursorstate::Rightnibble {
                        cstate = Cursorstate::Leftnibble;
                    } else if cstate == Cursorstate::Leftnibble {
                        if cursorpos > 0 {
                            // not at start
                            cstate = Cursorstate::Rightnibble;
                            cursorpos -= 1;
                        }
                    }
                }
                Rule::right => {
                    if cstate == Cursorstate::Asciichar {
                        if cursorpos + 1 < buf.len() {
                            // not at end
                            cursorpos += 1;
                        }
                    } else if cstate == Cursorstate::Leftnibble {
                        cstate = Cursorstate::Rightnibble;
                    } else if cstate == Cursorstate::Rightnibble {
                        if cursorpos + 1 < buf.len() {
                            // not at end
                            cstate = Cursorstate::Leftnibble;
                            cursorpos += 1;
                        }
                    }
                }
                Rule::start => {
                    cursorpos -= cursorpos % SPALTEN; // jump to start of line
                    if cstate == Cursorstate::Rightnibble {
                        cstate = Cursorstate::Leftnibble;
                    }
                }
                Rule::end => {
                    // check if no overflow
                    if cursorpos - (cursorpos % SPALTEN) + (SPALTEN - 1) < buf.len() {
                        // jump to end of line
                        cursorpos = cursorpos - (cursorpos % SPALTEN) + (SPALTEN - 1);
                    } else {
                        // jump to end of line
                        cursorpos = buf.len() - 1
                    }
                    if cstate == Cursorstate::Leftnibble {
                        cstate = Cursorstate::Rightnibble;
                    }
                }
                Rule::top => {
                    cursorpos = 0;
                }
                Rule::bottom => {
                    cursorpos = buf.len() - 1;
                    cursorpos -= cursorpos % SPALTEN; // jump to start of line
                }
                Rule::replace => {
                    // printw("next char will be the replacement!");
                    clear = false;
                }
                Rule::remove => {
                    // check if in valid range
                    if buf.len() > 0 && cursorpos < buf.len() {
                        // remove the current char
                        buf.remove(cursorpos);
                    }
                    // always perform the movement if possible
                    if cursorpos > 0 && cursorpos >= buf.len() {
                        cursorpos -= 1;
                    }
                }
                Rule::insert => {
                    printw("next chars will be inserted!");
                    clear = false;
                }
                Rule::jumpascii => {
                    if cstate == Cursorstate::Asciichar {
                        cstate = Cursorstate::Leftnibble;
                    } else {
                        cstate = Cursorstate::Asciichar;
                    }
                }
                Rule::helpfile => {
                    command.push_str("No helpfile yet");
                }
                Rule::backspace => {
                    command.pop();
                    command.pop();
                    clear = false;
                }
                Rule::saveandexit => {
                    save = true;
                    quitnow = true;
                }
                Rule::exit => quitnow = true,
                Rule::save => save = true,

                _ => (),
            }

            for inner_cmd in cmd.into_inner() {
                match inner_cmd.as_rule() {
                    Rule::replacement => {
                        // TODO: use inner_cmd and not just "key"
                        // printw(&format!("Replacement: {:?}", inner_cmd.as_str()));
                        if cstate == Cursorstate::Asciichar {
                            if cursorpos >= buf.len() {
                                buf.insert(cursorpos, 0);
                            }
                            // buf[cursorpos] = inner_cmd.as_str();
                            buf[cursorpos] = key as u8;
                        } else {
                            let mask = if cstate == Cursorstate::Leftnibble {
                                0x0F
                            } else {
                                0xF0
                            };
                            let shift = if cstate == Cursorstate::Leftnibble {
                                4
                            } else {
                                0
                            };
                            if cursorpos >= buf.len() {
                                buf.insert(cursorpos, 0);
                            }
                            // Change the selected nibble
                            if let Some(c) = key.to_digit(16) {
                                buf[cursorpos] = buf[cursorpos] & mask | (c as u8) << shift;
                            }
                        }

                    }
                    // TODO: use inner_cmd and not just "key"
                    Rule::insertment => {
                        // printw(&format!("Inserted: {:?}", inner_cmd.as_str()));
                        command.pop(); // remove the just inserted thing
                        clear = false;

                        if cstate == Cursorstate::Leftnibble {
                            // Left nibble
                            if let Some(c) = (key as char).to_digit(16) {
                                buf.insert(cursorpos, (c as u8) << 4);
                                cstate = Cursorstate::Rightnibble;
                            }
                        } else if cstate == Cursorstate::Rightnibble {
                            // Right nibble
                            if cursorpos == buf.len() {
                                buf.insert(cursorpos, 0);
                            }
                            if let Some(c) = (key as char).to_digit(16) {
                                buf[cursorpos] = buf[cursorpos] & 0xF0 | c as u8;
                                cstate = Cursorstate::Leftnibble;
                                cursorpos += 1;
                            }
                        } else if cstate == Cursorstate::Asciichar {
                            buf.insert(cursorpos, key as u8);
                            cursorpos += 1;
                        }
                    }
                    Rule::searchstr => {
                        let search = inner_cmd.as_str().as_bytes();
                        let foundpos = TwoWaySearcher::new(&search);
                        cursorpos = foundpos.search_in(&buf).unwrap_or(cursorpos);
                    }
                    Rule::searchbytes => {
                        let search = inner_cmd.as_str().as_bytes();
                        let mut needle = vec![];
                        for i in 0..search.len() {
                            let nibble = match search[i] as u8 {
                                c @ 48...57 => c - 48, // Numbers from 0 to 9
                                b'x' => 0x10, // x is the wildcard
                                b'X' => 0x10, // X is the wildcard
                                c @ b'a'...b'f' => c - 87,
                                c @ b'A'...b'F' => c - 55,
                                _ => panic!("Should not get to this position!"),
                            };
                            needle.push(nibble);
                        }
                        cursorpos = buf.find_subset(&needle).unwrap_or(cursorpos);
                        // endwin(); println!("Searching for: {:?}", needle ); return;
                    }
                    Rule::linenumber => {
                        let linenr: usize = inner_cmd.as_str().parse().unwrap();
                        cursorpos = linenr * SPALTEN; // jump to the line
                        if cursorpos > buf.len() { // detect file end
                            cursorpos = buf.len();
                        }
                        cursorpos -= cursorpos % SPALTEN; // jump to start of line
                    }
                    Rule::escape => (),
                    Rule::gatherone => clear = false,
                    _ => {
                        command.push_str(&format!("no rule for {:?} ", inner_cmd.as_rule()));
                        clear = false;
                    }
                };
            }
            if save {
                if path.exists() {
                    let mut file = match OpenOptions::new()
                        .read(true)
                        .write(true)
                        .create(true)
                        .open(&path) {
                        Err(why) => {
                            panic!(
                                "Could not open {}: {}",
                                path.display(),
                                Error::description(&why)
                            )
                        }
                        Ok(file) => file,
                    };
                    file.seek(SeekFrom::Start(0)).ok().expect(
                        "Filepointer could not be set to 0",
                    );
                    file.write_all(&mut buf).ok().expect(
                        "File could not be written.",
                    );
                    file.set_len(buf.len() as u64).ok().expect(
                        "File could not be set to correct lenght.",
                    );
                    command.push_str("File saved!");
                } else {
                    command.push_str("Careful, file could not be saved!");
                }
                // TODO: define filename during runtime
                save = false;
            }
            if clear {
                command.clear();
            }

            // Always move screen when cursor leaves screen
            if cursorpos > (screenheight + screenoffset - 1) * SPALTEN - 1 {
                screenoffset = 2 + cursorpos / SPALTEN - screenheight;
            }
            if cursorpos < screenoffset * SPALTEN {
                screenoffset = cursorpos / SPALTEN;
            }

        }

    let draw_range = get_absolute_draw_indices(buf.len(), SPALTEN, screenoffset);
        draw(&buf[draw_range.0 .. draw_range.1], cursorpos, SPALTEN, &command, &mut debug, cstate, screenoffset);
    }

    refresh();
    endwin();
}
<|endoftext|>