<commit_before>extern crate flexi_logger;
extern crate time;
#[macro_use]
extern crate log;
#[macro_use]
extern crate quick_error;
#[macro_use]
extern crate clap;

#[macro_use]
extern crate varnishslog;

use std::io::{self, stdin, Read, Write};
use std::error::Error;
use clap::{Arg, App};

use varnishslog::stream_buf::{StreamBuf, ReadStreamBuf, FillError, FillApplyError};
use varnishslog::access_log::*;

mod program;

quick_error! {
    #[derive(Debug)]
    pub enum ProcessingError {
        IO(err: io::Error) {
            display("IO Error while processing records: {}", err)
            description("I/O error")
            from()
        }
        InputBuffer(err: FillError) {
            display("Failed to fill parsing buffer: {}", err)
            description("Input buffer error")
        }
        Parsing(err: String) {
            display("Failed to parse VSL record: {}", err)
            description("Parser error")
        }
        Serialization(err: String) {
            display("Failed to serialize log record: {}", err)
            description("Serialization error")
        }
    }
}

impl<'b> From<FillApplyError<&'b[u8], u32>> for ProcessingError {
    fn from(err: FillApplyError<&'b[u8], u32>) -> ProcessingError {
        match err {
            FillApplyError::FillError(FillError::Io(err)) => ProcessingError::IO(err),
            FillApplyError::FillError(err) => ProcessingError::InputBuffer(err),
            // we need to convert it to string due to input reference
            FillApplyError::Parser(err) => ProcessingError::Parsing(format!("nom parser error: {}", err)),
        }
    }
}

impl From<OutputError> for ProcessingError {
    fn from(err: OutputError) -> ProcessingError {
        match err {
            OutputError::JsonSerialization(JsonError::Io(err)) |
            OutputError::Io(err) => ProcessingError::IO(err),
            err => ProcessingError::Serialization(format!("Serialization error: {}", err)),
        }
    }
}

impl ProcessingError {
    fn to_exit_code(&self) -> i32 {
        match *self {
            ProcessingError::IO(_) => 10,
            ProcessingError::InputBuffer(_) => 11,
            ProcessingError::Parsing(_) => 20,
            ProcessingError::Serialization(_) => 30,
        }
    }
}

impl ProcessingError {
    fn is_brokend_pipe(&self) -> bool {
        match *self {
            ProcessingError::IO(ref err) =>
                err.kind() == io::ErrorKind::UnexpectedEof || err.kind() == io::ErrorKind::BrokenPipe,
            _ => false
        }
    }
}

fn try_read_vsl_tag<R: Read>(stream: &mut ReadStreamBuf<R>) -> Result<(), ProcessingError> {
    loop {
        match try!(stream.fill_apply(binary_vsl_tag)) {
            None => continue,
            Some(Some(_)) => {
                info!("Found VSL tag");
                break
            }
            Some(_) => break,
        }
    }
    Ok(())
}

trait WriteRecord {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write;
    fn log_reports(&self) {}
}

fn process_vsl_records<R, W, P>(stream: &mut ReadStreamBuf<R>, mut writer: P, output: &mut W) -> Result<(), ProcessingError> where R: Read, W: Write, P: WriteRecord {
    loop {
        match stream.fill_apply(vsl_record_v4).map_err(ProcessingError::from) {
            Ok(None) => continue,
            Ok(Some(record)) => try!(writer.write_record(record, output)),
            Err(err) => {
                //TODO: need better tracking of orphan records and other stats
                if err.is_brokend_pipe() {
                    writer.log_reports();
                }
                return Err(err)
            }
        }
    }
}

fn process_vsl_stream<R, W>(input: R, mut output: W, output_format: OutputFormat, config: Config) -> Result<(), ProcessingError> where R: Read, W: Write {
    //TODO: make buffer size configurable
    let mut stream = ReadStreamBuf::new(input);

    try!(try_read_vsl_tag(&mut stream));

    match output_format {
        OutputFormat::Log => process_vsl_records(&mut stream, LogWriter::default(), &mut output),
        OutputFormat::LogDebug => process_vsl_records(&mut stream, LogDebugWriter::default(), &mut output),
        OutputFormat::RecordDebug => process_vsl_records(&mut stream, RecordDebugWriter::default(), &mut output),
        OutputFormat::SessionDebug => process_vsl_records(&mut stream, SessionDebugWriter::default(), &mut output),
        OutputFormat::Json => process_vsl_records(&mut stream, SerdeWriter::new(Format::Json, config), &mut output),
        OutputFormat::JsonPretty => process_vsl_records(&mut stream, SerdeWriter::new(Format::JsonPretty, config), &mut output),
        OutputFormat::NcsaJson => process_vsl_records(&mut stream, SerdeWriter::new(Format::NcsaJson, config), &mut output),
    }
}

#[derive(Default)]
struct LogWriter;
impl WriteRecord for LogWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        writeln!(output, "{:#}", record).map_err(From::from)
    }
}

#[derive(Default)]
struct LogDebugWriter;
impl WriteRecord for LogDebugWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        writeln!(output, "{:#?}", record).map_err(From::from)
    }
}

#[derive(Default)]
struct RecordDebugWriter {
    state: RecordState,
}

impl WriteRecord for RecordDebugWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        if let Some(record) = self.state.apply(&record) {
            writeln!(output, "{:#?}", record).map_err(From::from)
        } else {
            Ok(())
        }
    }
}

#[derive(Default)]
struct SessionDebugWriter {
    state: SessionState,
}

impl WriteRecord for SessionDebugWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        if let Some(record) = self.state.apply(&record) {
            writeln!(output, "{:#?}", record).map_err(From::from)
        } else {
            Ok(())
        }
    }
}

struct SerdeWriter {
    state: SessionState,
    format: Format,
    config: Config,
}

impl SerdeWriter {
    fn new(format: Format, config: Config) -> SerdeWriter {
        SerdeWriter {
            state: SessionState::default(),
            format: format,
            config: config,
        }
    }
}

impl WriteRecord for SerdeWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        if let Some(session) = self.state.apply(&record) {
            log_session_record(&session, &self.format, output, &self.config).map_err(From::from)
        } else {
            Ok(())
        }
    }

    fn log_reports(&self) {
        for client in self.state.unmatched_client_access_records() {
            warn!("ClientAccessRecord without matching session left: {:?}", client)
        }

        for backend in self.state.unmatched_backend_access_records() {
            warn!("BackendAccessRecord without matching session left: {:?}", backend)
        }

        for session in self.state.unresolved_sessions() {
            warn!("SessionRecord with unresolved links to other objects left: {:?}", session)
        }
    }
}

arg_enum! {
    #[derive(Debug)]
    enum OutputFormat {
        Log,
        LogDebug,
        RecordDebug,
        SessionDebug,
        Json,
        JsonPretty,
        NcsaJson
    }
}

fn main() {
    let arguments = App::new("Varnish VSL log to syslog logger")
        .version(crate_version!())
        .author(crate_authors!())
        .about("Reads binary VSL log entreis, correlates them togeter and emits JSON log entry to syslog")
        .arg(Arg::with_name("log-spec")
             .short("d")
             .long("log-sepc")
             .value_name("LOG_LEVEL_SPEC")
             .help("Logging level specification, e.g: info")
             .takes_value(true)
             .default_value("warn"))
        .arg(Arg::with_name("output")
             .long("output-format")
             .short("o")
             .help("Format of the output")
             .takes_value(true)
             .possible_values(&OutputFormat::variants())
             .default_value(OutputFormat::variants().last().unwrap()))
        .arg(Arg::with_name("no-log-processing")
             .long("no-log-processing")
             .short("l")
             .help("Do not process VSL log into vars, messages and ACL matches"))
        .arg(Arg::with_name("keep-raw-log")
             .long("keep-raw-log")
             .short("L")
             .help("Include raw log messages"))
        .arg(Arg::with_name("no-header-indexing")
             .long("no-header-indexing")
             .short("i")
             .help("Do not make indices of request and response headers with normalized header names"))
        .arg(Arg::with_name("keep-raw-headers")
             .long("keep-raw-headers")
             .short("I")
             .help("Keep raw header name/value pairs; any indices are moved to top level"))
        .get_matches();

    program::init(arguments.value_of("log-spec"));

    let output_format = value_t!(arguments, "output", OutputFormat).unwrap_or_else(|e| e.exit());

    let input = stdin();
    let input = input.lock();
    let output = std::io::stdout();

    let config = Config {
        no_log_processing: arguments.is_present("no-log-processing"),
        keep_raw_log: arguments.is_present("keep-raw-log"),
        no_header_indexing: arguments.is_present("no-header-indexing"),
        keep_raw_headers: arguments.is_present("keep-raw-headers"),
    };

    if let Err(err) = process_vsl_stream(input, output, output_format, config) {
        if err.is_brokend_pipe() {
            info!("Broken pipe")
        } else {
            error!("{}", err);
            program::exit_with_error(err.description(), err.to_exit_code())
        }
    }

    info!("Done");
}
<commit_msg>stream buffer size is now configurable<commit_after>extern crate flexi_logger;
extern crate time;
#[macro_use]
extern crate log;
#[macro_use]
extern crate quick_error;
#[macro_use]
extern crate clap;

#[macro_use]
extern crate varnishslog;

use std::io::{self, stdin, Read, Write};
use std::error::Error;
use clap::{Arg, App};

use varnishslog::stream_buf::{StreamBuf, ReadStreamBuf, FillError, FillApplyError};
use varnishslog::access_log::*;

mod program;

quick_error! {
    #[derive(Debug)]
    pub enum ProcessingError {
        IO(err: io::Error) {
            display("IO Error while processing records: {}", err)
            description("I/O error")
            from()
        }
        InputBuffer(err: FillError) {
            display("Failed to fill parsing buffer: {}", err)
            description("Input buffer error")
        }
        Parsing(err: String) {
            display("Failed to parse VSL record: {}", err)
            description("Parser error")
        }
        Serialization(err: String) {
            display("Failed to serialize log record: {}", err)
            description("Serialization error")
        }
    }
}

impl<'b> From<FillApplyError<&'b[u8], u32>> for ProcessingError {
    fn from(err: FillApplyError<&'b[u8], u32>) -> ProcessingError {
        match err {
            FillApplyError::FillError(FillError::Io(err)) => ProcessingError::IO(err),
            FillApplyError::FillError(err) => ProcessingError::InputBuffer(err),
            // we need to convert it to string due to input reference
            FillApplyError::Parser(err) => ProcessingError::Parsing(format!("nom parser error: {}", err)),
        }
    }
}

impl From<OutputError> for ProcessingError {
    fn from(err: OutputError) -> ProcessingError {
        match err {
            OutputError::JsonSerialization(JsonError::Io(err)) |
            OutputError::Io(err) => ProcessingError::IO(err),
            err => ProcessingError::Serialization(format!("Serialization error: {}", err)),
        }
    }
}

impl ProcessingError {
    fn to_exit_code(&self) -> i32 {
        match *self {
            ProcessingError::IO(_) => 10,
            ProcessingError::InputBuffer(_) => 11,
            ProcessingError::Parsing(_) => 20,
            ProcessingError::Serialization(_) => 30,
        }
    }
}

impl ProcessingError {
    fn is_brokend_pipe(&self) -> bool {
        match *self {
            ProcessingError::IO(ref err) =>
                err.kind() == io::ErrorKind::UnexpectedEof || err.kind() == io::ErrorKind::BrokenPipe,
            _ => false
        }
    }
}

fn try_read_vsl_tag<R: Read>(stream: &mut ReadStreamBuf<R>) -> Result<(), ProcessingError> {
    loop {
        match try!(stream.fill_apply(binary_vsl_tag)) {
            None => continue,
            Some(Some(_)) => {
                info!("Found VSL tag");
                break
            }
            Some(_) => break,
        }
    }
    Ok(())
}

trait WriteRecord {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write;
    fn log_reports(&self) {}
}

fn process_vsl_records<R, W, P>(stream: &mut ReadStreamBuf<R>, mut writer: P, output: &mut W) -> Result<(), ProcessingError> where R: Read, W: Write, P: WriteRecord {
    loop {
        match stream.fill_apply(vsl_record_v4).map_err(ProcessingError::from) {
            Ok(None) => continue,
            Ok(Some(record)) => try!(writer.write_record(record, output)),
            Err(err) => {
                //TODO: need better tracking of orphan records and other stats
                if err.is_brokend_pipe() {
                    writer.log_reports();
                }
                return Err(err)
            }
        }
    }
}

fn process_vsl_stream<R, W>(input: R, mut output: W, stream_buf_size: usize, output_format: OutputFormat, config: Config) -> Result<(), ProcessingError> where R: Read, W: Write {
    //TODO: make buffer size configurable
    let mut stream = ReadStreamBuf::with_capacity(input, stream_buf_size);

    try!(try_read_vsl_tag(&mut stream));

    match output_format {
        OutputFormat::Log => process_vsl_records(&mut stream, LogWriter::default(), &mut output),
        OutputFormat::LogDebug => process_vsl_records(&mut stream, LogDebugWriter::default(), &mut output),
        OutputFormat::RecordDebug => process_vsl_records(&mut stream, RecordDebugWriter::default(), &mut output),
        OutputFormat::SessionDebug => process_vsl_records(&mut stream, SessionDebugWriter::default(), &mut output),
        OutputFormat::Json => process_vsl_records(&mut stream, SerdeWriter::new(Format::Json, config), &mut output),
        OutputFormat::JsonPretty => process_vsl_records(&mut stream, SerdeWriter::new(Format::JsonPretty, config), &mut output),
        OutputFormat::NcsaJson => process_vsl_records(&mut stream, SerdeWriter::new(Format::NcsaJson, config), &mut output),
    }
}

#[derive(Default)]
struct LogWriter;
impl WriteRecord for LogWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        writeln!(output, "{:#}", record).map_err(From::from)
    }
}

#[derive(Default)]
struct LogDebugWriter;
impl WriteRecord for LogDebugWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        writeln!(output, "{:#?}", record).map_err(From::from)
    }
}

#[derive(Default)]
struct RecordDebugWriter {
    state: RecordState,
}

impl WriteRecord for RecordDebugWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        if let Some(record) = self.state.apply(&record) {
            writeln!(output, "{:#?}", record).map_err(From::from)
        } else {
            Ok(())
        }
    }
}

#[derive(Default)]
struct SessionDebugWriter {
    state: SessionState,
}

impl WriteRecord for SessionDebugWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        if let Some(record) = self.state.apply(&record) {
            writeln!(output, "{:#?}", record).map_err(From::from)
        } else {
            Ok(())
        }
    }
}

struct SerdeWriter {
    state: SessionState,
    format: Format,
    config: Config,
}

impl SerdeWriter {
    fn new(format: Format, config: Config) -> SerdeWriter {
        SerdeWriter {
            state: SessionState::default(),
            format: format,
            config: config,
        }
    }
}

impl WriteRecord for SerdeWriter {
    fn write_record<W>(&mut self, record: VslRecord, output: &mut W) -> Result<(), ProcessingError> where W: Write {
        if let Some(session) = self.state.apply(&record) {
            log_session_record(&session, &self.format, output, &self.config).map_err(From::from)
        } else {
            Ok(())
        }
    }

    fn log_reports(&self) {
        for client in self.state.unmatched_client_access_records() {
            warn!("ClientAccessRecord without matching session left: {:?}", client)
        }

        for backend in self.state.unmatched_backend_access_records() {
            warn!("BackendAccessRecord without matching session left: {:?}", backend)
        }

        for session in self.state.unresolved_sessions() {
            warn!("SessionRecord with unresolved links to other objects left: {:?}", session)
        }
    }
}

arg_enum! {
    #[derive(Debug)]
    enum OutputFormat {
        Log,
        LogDebug,
        RecordDebug,
        SessionDebug,
        Json,
        JsonPretty,
        NcsaJson
    }
}

fn main() {
    let arguments = App::new("Varnish VSL log to syslog logger")
        .version(crate_version!())
        .author(crate_authors!())
        .about("Reads binary VSL log entreis, correlates them togeter and emits JSON log entry to syslog")
        .arg(Arg::with_name("log-spec")
             .short("d")
             .long("log-sepc")
             .value_name("LOG_LEVEL_SPEC")
             .help("Logging level specification, e.g: info")
             .takes_value(true)
             .default_value("warn"))
        .arg(Arg::with_name("output")
             .long("output-format")
             .short("o")
             .help("Format of the output")
             .takes_value(true)
             .possible_values(&OutputFormat::variants())
             .default_value(OutputFormat::variants().last().unwrap()))
        .arg(Arg::with_name("no-log-processing")
             .long("no-log-processing")
             .short("l")
             .help("Do not process VSL log into vars, messages and ACL matches"))
        .arg(Arg::with_name("keep-raw-log")
             .long("keep-raw-log")
             .short("L")
             .help("Include raw log messages"))
        .arg(Arg::with_name("no-header-indexing")
             .long("no-header-indexing")
             .short("i")
             .help("Do not make indices of request and response headers with normalized header names"))
        .arg(Arg::with_name("keep-raw-headers")
             .long("keep-raw-headers")
             .short("I")
             .help("Keep raw header name/value pairs; any indices are moved to top level"))
        .arg(Arg::with_name("stream-buffer-size")
             .long("stream-buffer-size")
             .short("s")
             .help("Size of stream buffer in bytes - must be bigger than biggest VSL record")
             .default_value("262144"))
        .get_matches();

    program::init(arguments.value_of("log-spec"));

    let output_format = value_t!(arguments, "output", OutputFormat).unwrap_or_else(|e| e.exit());
    let stream_buf_size = value_t!(arguments, "stream-buffer-size", usize).unwrap_or_else(|e| e.exit());

    let input = stdin();
    let input = input.lock();
    let output = std::io::stdout();

    let config = Config {
        no_log_processing: arguments.is_present("no-log-processing"),
        keep_raw_log: arguments.is_present("keep-raw-log"),
        no_header_indexing: arguments.is_present("no-header-indexing"),
        keep_raw_headers: arguments.is_present("keep-raw-headers"),
    };

    if let Err(err) = process_vsl_stream(input, output, stream_buf_size, output_format, config) {
        if err.is_brokend_pipe() {
            info!("Broken pipe")
        } else {
            error!("{}", err);
            program::exit_with_error(err.description(), err.to_exit_code())
        }
    }

    info!("Done");
}
<|endoftext|>


<commit_before>#![allow(dead_code)]
#![feature(slice_patterns)]

mod core;
mod toolkit;
mod view;

#[macro_use]
extern crate bitflags;

#[macro_use]
extern crate log;
extern crate env_logger;

extern crate glob;
extern crate oniguruma;
extern crate unicode_width;

extern crate ncurses;

extern crate rustc_serialize;
extern crate weakjson;
extern crate plist;

use ncurses::*;

use core::Core;
use core::bindings::Key;
use view::window::Window;
use toolkit::*;
use view::theme::PALETTE;

fn main() {

    setlocale(LcCategory::all, "en_US.utf-8");

    initscr();

    noecho();
    keypad(stdscr, true);
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
    raw();

    start_color();
    use_default_colors();

    for (i, &(ref fg, ref bg)) in PALETTE.iter().enumerate() {
        init_pair(i as i16, fg.to_term(), bg.to_term());
    }

    let mut window = Window::new(Core::load());
    window.render(Canvas::screen());
    loop {
        if let Some(key) = Key::from_keycode(getch()) {
            if key == Key::Enter {
                break;
            }
            window.on_keypress(Canvas::screen(), key);
        }
    }

    // println!("{:?}", window);

    // core.package_repository.get_keymap("default/Default (OSX).sublime-keymap");
    // core.package_repository.get_keymap("default/Default
    // (Windows).sublime-keymap");
    // core.package_repository.get_keymap("default/Default (Linux).sublime-keymap");

    // info!("hello world!");

    // menu.render(Canvas { x1: 0, y1: 0, x2: 80, y2: 20});

    // Start ncurses.

    // mousemask(ALL_MOUSE_EVENTS as u64, None);

    // attron(COLOR_PAIR(150));

    // mvaddstr(10, 10, "hello world");
    // clear();
    // /* Print to the back buffer. */
    // // attron(COLOR_PAIR(0));
    // // printw("Базовый 0..15\n\n");
    // // for i in 0..15+1 {
    // //   attron(COLOR_PAIR(i));
    // //   printw("██");
    // // }

    // // printw("\n\nColor cube 16..231\n\n");
    // // for i in 16..231+1 {
    // //   attron(COLOR_PAIR(i));
    // //   printw("██");
    // // }

    // // printw("\n\nGrayscale 232..255\n\n");
    // // for i in 232..255+1 {
    // //   attron(COLOR_PAIR(i));
    // //   printw("██");
    // // }

    // Wait for a key press.
    // loop {
    //     let c = getch();
    //     println!("{:?}\r", c);
    //     if c == 10 {
    //         break;
    //     }
    // }

    // Terminate ncurses.
    endwin();
}
<commit_msg>Switch to stable, remove feature(slice_patterns)<commit_after>#![allow(dead_code)]

mod core;
mod toolkit;
mod view;

#[macro_use]
extern crate bitflags;

#[macro_use]
extern crate log;
extern crate env_logger;

extern crate glob;
extern crate oniguruma;
extern crate unicode_width;

extern crate ncurses;

extern crate rustc_serialize;
extern crate weakjson;
extern crate plist;

use ncurses::*;

use core::Core;
use core::bindings::Key;
use view::window::Window;
use toolkit::*;
use view::theme::PALETTE;

fn main() {

    setlocale(LcCategory::all, "en_US.utf-8");

    initscr();

    noecho();
    keypad(stdscr, true);
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
    raw();

    start_color();
    use_default_colors();

    for (i, &(ref fg, ref bg)) in PALETTE.iter().enumerate() {
        init_pair(i as i16, fg.to_term(), bg.to_term());
    }

    let mut window = Window::new(Core::load());
    window.render(Canvas::screen());
    loop {
        if let Some(key) = Key::from_keycode(getch()) {
            if key == Key::Enter {
                break;
            }
            window.on_keypress(Canvas::screen(), key);
        }
    }

    // println!("{:?}", window);

    // core.package_repository.get_keymap("default/Default (OSX).sublime-keymap");
    // core.package_repository.get_keymap("default/Default
    // (Windows).sublime-keymap");
    // core.package_repository.get_keymap("default/Default (Linux).sublime-keymap");

    // info!("hello world!");

    // menu.render(Canvas { x1: 0, y1: 0, x2: 80, y2: 20});

    // Start ncurses.

    // mousemask(ALL_MOUSE_EVENTS as u64, None);

    // attron(COLOR_PAIR(150));

    // mvaddstr(10, 10, "hello world");
    // clear();
    // /* Print to the back buffer. */
    // // attron(COLOR_PAIR(0));
    // // printw("Базовый 0..15\n\n");
    // // for i in 0..15+1 {
    // //   attron(COLOR_PAIR(i));
    // //   printw("██");
    // // }

    // // printw("\n\nColor cube 16..231\n\n");
    // // for i in 16..231+1 {
    // //   attron(COLOR_PAIR(i));
    // //   printw("██");
    // // }

    // // printw("\n\nGrayscale 232..255\n\n");
    // // for i in 232..255+1 {
    // //   attron(COLOR_PAIR(i));
    // //   printw("██");
    // // }

    // Wait for a key press.
    // loop {
    //     let c = getch();
    //     println!("{:?}\r", c);
    //     if c == 10 {
    //         break;
    //     }
    // }

    // Terminate ncurses.
    endwin();
}
<|endoftext|>


<commit_before>// Spec found at https://tools.ietf.org/html/rfc959

/*
 * FIXME Filezilla says: Le serveur ne supporte pas les caractères non-ASCII.
 */

#![feature(proc_macro, conservative_impl_trait, generators)]

extern crate bytes;
#[macro_use]
extern crate cfg_if;
extern crate futures_await as futures;
extern crate time;
extern crate tokio_core;
extern crate tokio_io;

mod cmd; // FIXME: rename this module.
mod codec;
mod ftp;

use std::env;
use std::fs::{DirEntry, File, Metadata, read_dir};
use std::io::{self, Read};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;

use futures::{Sink, Stream};
use futures::prelude::{async, await};
use futures::stream::SplitSink;
use tokio_core::reactor::{Core, Handle};
use tokio_core::net::{TcpListener, TcpStream};
use tokio_io::AsyncRead;
use tokio_io::codec::Framed;

use cmd::{Command, TransferType};
use codec::{FtpCodec, StringCodec};
use ftp::{Answer, ResultCode};

const MONTHS: [&'static str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

type DataWriter = SplitSink<Framed<TcpStream, StringCodec>>;
type Writer = SplitSink<Framed<TcpStream, FtpCodec>>;

cfg_if! {
    if #[cfg(windows)] {
        fn get_file_info(meta: &Metadata) -> (time::Tm, u64) {
            use std::os::windows::prelude::*;
            (time::at(time::Timespec::new(meta.last_write_time())), meta.file_size())
        }
    } else {
        fn get_file_info(meta: &Metadata) -> (time::Tm, u64) {
            use std::os::unix::prelude::*;
            (time::at(time::Timespec::new(meta.mtime(), 0)), meta.size())
        }
    }
}

fn add_file_info(entry: DirEntry, out: &mut String) {
    let path = entry.path(); // TODO: handle error.
    let extra = if path.is_dir() { "/" } else { "" };
    let is_dir = if path.is_dir() { "d" } else { "-" };

    let meta = ::std::fs::metadata(&path).unwrap(); // TODO: handle error.
    let (time, file_size) = get_file_info(&meta);
    let path = path.to_str().unwrap().split("/").last().unwrap(); // TODO: handle error.
    let file_str = format!("{} {} {} {} {}:{} {}{}\r\n",
                           is_dir,
                           file_size,
                           MONTHS[time.tm_mon as usize],
                           time.tm_mday,
                           time.tm_hour,
                           time.tm_min,
                           path,
                           extra);
    out.push_str(&file_str);
    println!("==> {:?}", &file_str);
}

#[allow(dead_code)]
struct Client {
    address: String, // TODO: remove this?
    cwd: PathBuf,
    data_port: Option<u16>,
    data_writer: Option<DataWriter>,
    handle: Handle,
    name: Option<String>,
    server_root: PathBuf,
    transfer_type: TransferType,
    writer: Writer,
}

impl Client {
    fn new(address: String, handle: Handle, writer: Writer, server_root: PathBuf) -> Client {
        Client {
            address,
            cwd: PathBuf::from(""),
            data_port: None,
            data_writer: None,
            handle,
            name: None,
            server_root,
            transfer_type: TransferType::Ascii,
            writer,
        }
    }

    #[async]
    fn handle_cmd(mut self, cmd: Command) -> Result<Self, ()> {
        println!("Received command: {:?}", cmd);
        match cmd {
            Command::Auth =>
                self = await!(self.send(Answer::new(ResultCode::CommandNotImplemented, "Not implemented")))?
            ,
            Command::Cwd(directory) => self = await!(self.cwd(directory))?,
            Command::List(path) => self = await!(self.list(path))?,
            Command::Pasv => self = await!(self.pasv())?,
            Command::Port(port) => {
                self.data_port = Some(port);
                self = await!(self.send(Answer::new(ResultCode::Ok, &format!("Data port is now {}", port))))?;
            },
            Command::Pwd => {
                let message = format!("\"/{}\" ", self.cwd.to_str().unwrap());
                self = await!(self.send(Answer::new(ResultCode::PATHNAMECreated, &message)))?;
            }, // TODO: handle error.
            Command::Quit => self = await!(self.quit())?,
            Command::Retr(file) => self = await!(self.retr(file))?,
            Command::Syst => {
                self = await!(self.send(Answer::new(ResultCode::Ok, "I won't tell!")))?;
            },
            Command::Type(typ) => {
                self.transfer_type = typ;
                self = await!(self.send(Answer::new(ResultCode::Ok, "Transfer type changed successfully")))?;
            },
            Command::Unknown =>
                self = await!(self.send(Answer::new(ResultCode::UnknownCommand, "Not implemented")))?
            ,
            Command::User(content) => {
                if content.is_empty() {
                    self = await!(self.send(Answer::new(ResultCode::InvalidParameterOrArgument, "Invalid username")))?;
                } else {
                    self.name = Some(content.to_owned());
                    self = await!(self.send(Answer::new(ResultCode::UserloggedIn, &format!("Welcome {}!", content))))?;
                }
            }
        }
        Ok(self)
    }

    fn complete_path(self, path: PathBuf) -> Result<(PathBuf, Self), io::Error> {
        let directory = self.server_root.join(if path.has_root() {
            path.iter().skip(1).collect()
        } else {
            path
        });
        let dir = directory.canonicalize();
        println!("===> {:?} {:?}", dir, directory);
        if let Ok(ref dir) = dir {
            if !dir.starts_with(&self.server_root) {
                return Err(io::ErrorKind::PermissionDenied.into());
            }
        }
        dir.map(|dir| (dir, self))
    }

    #[async]
    fn cwd(mut self, directory: String) -> Result<Self, ()> {
        let mut directory = PathBuf::from(directory);
        let path = self.cwd.join(&directory);
        let (dir, new_self) = self.complete_path(path).unwrap(); // TODO: handle error.
        self = new_self;
        self.cwd = dir.strip_prefix(&self.server_root).unwrap().to_path_buf(); // TODO: handle error.
        println!("switched to {:?}", self.cwd);
        self = await!(self.send(Answer::new(ResultCode::Ok,
                                            &format!("Directory changed to \"{}\"", directory.display()))))?;
        Ok(self)
    }

    #[async]
    fn list(mut self, path: Option<PathBuf>) -> Result<Self, ()> {
        if self.data_writer.is_some() {
            let ori = path.as_ref().unwrap_or(&self.cwd).to_path_buf();
            let directory = PathBuf::from(&ori);
            let (dir, new_self) = self.complete_path(directory).unwrap(); // TODO: handle error.
            self = new_self;
            self = await!(self.send(Answer::new(ResultCode::DataConnectionAlreadyOpen,
                                                "Starting to list directory...")))?;
            let mut out = String::new();
            for entry in read_dir(dir).unwrap() { // TODO: handle error.
                add_file_info(entry.unwrap(), &mut out); // TODO: handle error.
            }
            self = await!(self.send_data(out))?;
            println!("-> and done!");
        } else {
            self = await!(self.send(Answer::new(ResultCode::ConnectionClosed, "No opened data connection")))?;
        }
        if self.data_writer.is_some() {
            self.data_writer = None;
            self = await!(self.send(Answer::new(ResultCode::ClosingDataConnection, "Transfer done")))?;
        }
        Ok(self)
    }

    #[async]
    fn pasv(mut self) -> Result<Self, ()> {
        let port =
            if let Some(port) = self.data_port {
                port
            } else {
                0
            };
        if self.data_writer.is_some() {
            self = await!(self.send(Answer::new(ResultCode::DataConnectionAlreadyOpen, "Already listening...")))?;
            return Ok(self);
        }

        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
        let listener = TcpListener::bind(&addr, &self.handle).unwrap(); // TODO: handle error
        let port = listener.local_addr().unwrap().port(); // TODO: handle error.

        self = await!(self.send(Answer::new(ResultCode::EnteringPassiveMode,
                              &format!("127,0,0,1,{},{}", port >> 8, port & 0xFF))))?;

        println!("Waiting clients on port {}...", port);
        // TODO: use into_future() instead of for loop?
        #[async]
        for (stream, _rest) in listener.incoming().map_err(|_| ()) {
            let (writer, _reader) = stream.framed(StringCodec).split();
            self.data_writer = Some(writer);
            break;
        }
        Ok(self)
    }

    #[async]
    fn quit(mut self) -> Result<Self, ()> {
        if self.data_writer.is_some() {
            unimplemented!();
        } else {
            self = await!(self.send(Answer::new(ResultCode::ServiceClosingControlConnection, "Closing connection...")))?;
            self.writer.close().unwrap(); // TODO: handle error.
        }
        Ok(self)
    }

    #[async]
    fn retr(mut self, path: PathBuf) -> Result<Self, ()> {
        // TODO: check if multiple data connection can be opened at the same time.
        if self.data_writer.is_some() {
            let path = self.cwd.join(path);
            let (dir, new_self) = self.complete_path(path).unwrap(); // TODO: handle error.
            self = new_self;
            if dir.is_file() {
                self = await!(self.send(Answer::new(ResultCode::DataConnectionAlreadyOpen, "Starting to send file...")))?;
                let mut file = File::open(dir).unwrap(); // TODO: handle error.
                let mut out = String::new();
                file.read_to_string(&mut out).unwrap(); // TODO: handle error.
                self = await!(self.send_data(out))?;
                println!("-> file transfer done!");
            } else {
                self = await!(self.send(Answer::new(ResultCode::LocalErrorInProcessing,
                                      &format!("\"{}\" doesn't exist", dir.to_str().unwrap()))))?;
            }
        } else {
            self = await!(self.send(Answer::new(ResultCode::ConnectionClosed, "No opened data connection")))?;
        }
        if self.data_writer.is_some() {
            self.data_writer = None;
            self = await!(self.send(Answer::new(ResultCode::ClosingDataConnection, "Transfer done")))?;
        }
        Ok(self)
    }

    #[async]
    fn send(mut self, answer: Answer) -> Result<Self, ()> {
        self.writer = await!(self.writer.send(answer)).map_err(|_| ())?;
        Ok(self)
    }

    #[async]
    fn send_data(mut self, data: String) -> Result<Self, ()> {
        if let Some(mut writer) = self.data_writer {
            self.data_writer = Some(await!(writer.send(data)).map_err(|_| ())?);
        }
        Ok(self)
    }
}

#[async]
fn handle_client(stream: TcpStream, handle: Handle, address: String, server_root: PathBuf) -> Result<(), ()> {
    let (writer, reader) = stream.framed(FtpCodec).split();
    let writer = await!(writer.send(Answer::new(ResultCode::ServiceReadyForNewUser, "Welcome to this FTP server!")))
        .map_err(|_| ())?;
    let mut client = Client::new(address, handle, writer, server_root);
    #[async]
    for cmd in reader.map_err(|_| ()) {
        client = await!(client.handle_cmd(cmd))?;
    }
    println!("Client closed");
    Ok(())
}

#[async]
fn server(handle: Handle, server_root: PathBuf) -> io::Result<()> {
    let port = 1234;
    let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
    let listener = TcpListener::bind(&addr, &handle).unwrap();

    println!("Waiting clients on port {}...", port);
    #[async]
    for (stream, addr) in listener.incoming() {
        let address = format!("[address : {}]", addr);
        println!("New client: {}", address);
        handle.spawn(handle_client(stream, handle.clone(), address, server_root.clone()));
        println!("Waiting another client...");
    }
    Ok(())
}

fn main() {
    let mut core = Core::new()
        .expect("Cannot create tokio Core");
    let handle = core.handle();

    let server_root = env::current_dir().unwrap(); // TODO: handle error.

    core.run(server(handle, server_root))
        .expect("Run tokio server");
}
<commit_msg>Fix relative path in LIST<commit_after>// Spec found at https://tools.ietf.org/html/rfc959

/*
 * FIXME Filezilla says: Le serveur ne supporte pas les caractères non-ASCII.
 */

#![feature(proc_macro, conservative_impl_trait, generators)]

extern crate bytes;
#[macro_use]
extern crate cfg_if;
extern crate futures_await as futures;
extern crate time;
extern crate tokio_core;
extern crate tokio_io;

mod cmd; // FIXME: rename this module.
mod codec;
mod ftp;

use std::env;
use std::fs::{DirEntry, File, Metadata, read_dir};
use std::io::{self, Read};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;

use futures::{Sink, Stream};
use futures::prelude::{async, await};
use futures::stream::SplitSink;
use tokio_core::reactor::{Core, Handle};
use tokio_core::net::{TcpListener, TcpStream};
use tokio_io::AsyncRead;
use tokio_io::codec::Framed;

use cmd::{Command, TransferType};
use codec::{FtpCodec, StringCodec};
use ftp::{Answer, ResultCode};

const MONTHS: [&'static str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

type DataWriter = SplitSink<Framed<TcpStream, StringCodec>>;
type Writer = SplitSink<Framed<TcpStream, FtpCodec>>;

cfg_if! {
    if #[cfg(windows)] {
        fn get_file_info(meta: &Metadata) -> (time::Tm, u64) {
            use std::os::windows::prelude::*;
            (time::at(time::Timespec::new(meta.last_write_time())), meta.file_size())
        }
    } else {
        fn get_file_info(meta: &Metadata) -> (time::Tm, u64) {
            use std::os::unix::prelude::*;
            (time::at(time::Timespec::new(meta.mtime(), 0)), meta.size())
        }
    }
}

fn add_file_info(entry: DirEntry, out: &mut String) {
    let path = entry.path(); // TODO: handle error.
    let extra = if path.is_dir() { "/" } else { "" };
    let is_dir = if path.is_dir() { "d" } else { "-" };

    let meta = ::std::fs::metadata(&path).unwrap(); // TODO: handle error.
    let (time, file_size) = get_file_info(&meta);
    let path = path.to_str().unwrap().split("/").last().unwrap(); // TODO: handle error.
    let file_str = format!("{} {} {} {} {}:{} {}{}\r\n",
                           is_dir,
                           file_size,
                           MONTHS[time.tm_mon as usize],
                           time.tm_mday,
                           time.tm_hour,
                           time.tm_min,
                           path,
                           extra);
    out.push_str(&file_str);
    println!("==> {:?}", &file_str);
}

#[allow(dead_code)]
struct Client {
    address: String, // TODO: remove this?
    cwd: PathBuf,
    data_port: Option<u16>,
    data_writer: Option<DataWriter>,
    handle: Handle,
    name: Option<String>,
    server_root: PathBuf,
    transfer_type: TransferType,
    writer: Writer,
}

impl Client {
    fn new(address: String, handle: Handle, writer: Writer, server_root: PathBuf) -> Client {
        Client {
            address,
            cwd: PathBuf::from(""),
            data_port: None,
            data_writer: None,
            handle,
            name: None,
            server_root,
            transfer_type: TransferType::Ascii,
            writer,
        }
    }

    #[async]
    fn handle_cmd(mut self, cmd: Command) -> Result<Self, ()> {
        println!("Received command: {:?}", cmd);
        match cmd {
            Command::Auth =>
                self = await!(self.send(Answer::new(ResultCode::CommandNotImplemented, "Not implemented")))?
            ,
            Command::Cwd(directory) => self = await!(self.cwd(directory))?,
            Command::List(path) => self = await!(self.list(path))?,
            Command::Pasv => self = await!(self.pasv())?,
            Command::Port(port) => {
                self.data_port = Some(port);
                self = await!(self.send(Answer::new(ResultCode::Ok, &format!("Data port is now {}", port))))?;
            },
            Command::Pwd => {
                let message = format!("\"/{}\" ", self.cwd.to_str().unwrap());
                self = await!(self.send(Answer::new(ResultCode::PATHNAMECreated, &message)))?;
            }, // TODO: handle error.
            Command::Quit => self = await!(self.quit())?,
            Command::Retr(file) => self = await!(self.retr(file))?,
            Command::Syst => {
                self = await!(self.send(Answer::new(ResultCode::Ok, "I won't tell!")))?;
            },
            Command::Type(typ) => {
                self.transfer_type = typ;
                self = await!(self.send(Answer::new(ResultCode::Ok, "Transfer type changed successfully")))?;
            },
            Command::Unknown =>
                self = await!(self.send(Answer::new(ResultCode::UnknownCommand, "Not implemented")))?
            ,
            Command::User(content) => {
                if content.is_empty() {
                    self = await!(self.send(Answer::new(ResultCode::InvalidParameterOrArgument, "Invalid username")))?;
                } else {
                    self.name = Some(content.to_owned());
                    self = await!(self.send(Answer::new(ResultCode::UserloggedIn, &format!("Welcome {}!", content))))?;
                }
            }
        }
        Ok(self)
    }

    fn complete_path(self, path: PathBuf) -> Result<(PathBuf, Self), io::Error> {
        let directory = self.server_root.join(if path.has_root() {
            path.iter().skip(1).collect()
        } else {
            path
        });
        let dir = directory.canonicalize();
        println!("===> {:?} {:?}", dir, directory);
        if let Ok(ref dir) = dir {
            if !dir.starts_with(&self.server_root) {
                return Err(io::ErrorKind::PermissionDenied.into());
            }
        }
        dir.map(|dir| (dir, self))
    }

    #[async]
    fn cwd(mut self, directory: String) -> Result<Self, ()> {
        let mut directory = PathBuf::from(directory);
        let path = self.cwd.join(&directory);
        let (dir, new_self) = self.complete_path(path).unwrap(); // TODO: handle error.
        self = new_self;
        self.cwd = dir.strip_prefix(&self.server_root).unwrap().to_path_buf(); // TODO: handle error.
        println!("switched to {:?}", self.cwd);
        self = await!(self.send(Answer::new(ResultCode::Ok,
                                            &format!("Directory changed to \"{}\"", directory.display()))))?;
        Ok(self)
    }

    #[async]
    fn list(mut self, path: Option<PathBuf>) -> Result<Self, ()> {
        if self.data_writer.is_some() {
            let path = self.cwd.join(path.unwrap_or_default());
            let directory = PathBuf::from(&path);
            let (dir, new_self) = self.complete_path(directory).unwrap(); // TODO: handle error.
            self = new_self;
            self = await!(self.send(Answer::new(ResultCode::DataConnectionAlreadyOpen,
                                                "Starting to list directory...")))?;
            let mut out = String::new();
            for entry in read_dir(dir).unwrap() { // TODO: handle error.
                add_file_info(entry.unwrap(), &mut out); // TODO: handle error.
            }
            self = await!(self.send_data(out))?;
            println!("-> and done!");
        } else {
            self = await!(self.send(Answer::new(ResultCode::ConnectionClosed, "No opened data connection")))?;
        }
        if self.data_writer.is_some() {
            self.data_writer = None;
            self = await!(self.send(Answer::new(ResultCode::ClosingDataConnection, "Transfer done")))?;
        }
        Ok(self)
    }

    #[async]
    fn pasv(mut self) -> Result<Self, ()> {
        let port =
            if let Some(port) = self.data_port {
                port
            } else {
                0
            };
        if self.data_writer.is_some() {
            self = await!(self.send(Answer::new(ResultCode::DataConnectionAlreadyOpen, "Already listening...")))?;
            return Ok(self);
        }

        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
        let listener = TcpListener::bind(&addr, &self.handle).unwrap(); // TODO: handle error
        let port = listener.local_addr().unwrap().port(); // TODO: handle error.

        self = await!(self.send(Answer::new(ResultCode::EnteringPassiveMode,
                              &format!("127,0,0,1,{},{}", port >> 8, port & 0xFF))))?;

        println!("Waiting clients on port {}...", port);
        // TODO: use into_future() instead of for loop?
        #[async]
        for (stream, _rest) in listener.incoming().map_err(|_| ()) {
            let (writer, _reader) = stream.framed(StringCodec).split();
            self.data_writer = Some(writer);
            break;
        }
        Ok(self)
    }

    #[async]
    fn quit(mut self) -> Result<Self, ()> {
        if self.data_writer.is_some() {
            unimplemented!();
        } else {
            self = await!(self.send(Answer::new(ResultCode::ServiceClosingControlConnection, "Closing connection...")))?;
            self.writer.close().unwrap(); // TODO: handle error.
        }
        Ok(self)
    }

    #[async]
    fn retr(mut self, path: PathBuf) -> Result<Self, ()> {
        // TODO: check if multiple data connection can be opened at the same time.
        if self.data_writer.is_some() {
            let path = self.cwd.join(path);
            let (dir, new_self) = self.complete_path(path).unwrap(); // TODO: handle error.
            self = new_self;
            if dir.is_file() {
                self = await!(self.send(Answer::new(ResultCode::DataConnectionAlreadyOpen, "Starting to send file...")))?;
                let mut file = File::open(dir).unwrap(); // TODO: handle error.
                let mut out = String::new();
                file.read_to_string(&mut out).unwrap(); // TODO: handle error.
                self = await!(self.send_data(out))?;
                println!("-> file transfer done!");
            } else {
                self = await!(self.send(Answer::new(ResultCode::LocalErrorInProcessing,
                                      &format!("\"{}\" doesn't exist", dir.to_str().unwrap()))))?;
            }
        } else {
            self = await!(self.send(Answer::new(ResultCode::ConnectionClosed, "No opened data connection")))?;
        }
        if self.data_writer.is_some() {
            self.data_writer = None;
            self = await!(self.send(Answer::new(ResultCode::ClosingDataConnection, "Transfer done")))?;
        }
        Ok(self)
    }

    #[async]
    fn send(mut self, answer: Answer) -> Result<Self, ()> {
        self.writer = await!(self.writer.send(answer)).map_err(|_| ())?;
        Ok(self)
    }

    #[async]
    fn send_data(mut self, data: String) -> Result<Self, ()> {
        if let Some(mut writer) = self.data_writer {
            self.data_writer = Some(await!(writer.send(data)).map_err(|_| ())?);
        }
        Ok(self)
    }
}

#[async]
fn handle_client(stream: TcpStream, handle: Handle, address: String, server_root: PathBuf) -> Result<(), ()> {
    let (writer, reader) = stream.framed(FtpCodec).split();
    let writer = await!(writer.send(Answer::new(ResultCode::ServiceReadyForNewUser, "Welcome to this FTP server!")))
        .map_err(|_| ())?;
    let mut client = Client::new(address, handle, writer, server_root);
    #[async]
    for cmd in reader.map_err(|_| ()) {
        client = await!(client.handle_cmd(cmd))?;
    }
    println!("Client closed");
    Ok(())
}

#[async]
fn server(handle: Handle, server_root: PathBuf) -> io::Result<()> {
    let port = 1234;
    let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
    let listener = TcpListener::bind(&addr, &handle).unwrap();

    println!("Waiting clients on port {}...", port);
    #[async]
    for (stream, addr) in listener.incoming() {
        let address = format!("[address : {}]", addr);
        println!("New client: {}", address);
        handle.spawn(handle_client(stream, handle.clone(), address, server_root.clone()));
        println!("Waiting another client...");
    }
    Ok(())
}

fn main() {
    let mut core = Core::new()
        .expect("Cannot create tokio Core");
    let handle = core.handle();

    let server_root = env::current_dir().unwrap(); // TODO: handle error.

    core.run(server(handle, server_root))
        .expect("Run tokio server");
}
<|endoftext|>


<commit_before>//! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust.
//
// Copyright (c) 2015-2020 tealdeer developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be
// copied, modified, or distributed except according to those terms.

#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::similar_names)]
#![allow(clippy::module_name_repetitions)]

#[cfg(feature = "logging")]
extern crate env_logger;

use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::process;

use ansi_term::{Color, Style};
use app_dirs::AppInfo;
use docopt::Docopt;
#[cfg(not(target_os = "windows"))]
use pager::Pager;
use serde_derive::Deserialize;

mod cache;
mod config;
mod error;
mod formatter;
mod tokenizer;
mod types;

use crate::cache::Cache;
use crate::config::{get_config_path, make_default_config, Config, MAX_CACHE_AGE};
use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError};
use crate::formatter::print_lines;
use crate::tokenizer::Tokenizer;
use crate::types::{ColorOptions, OsType};

const NAME: &str = "tealdeer";
const APP_INFO: AppInfo = AppInfo {
    name: NAME,
    author: NAME,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const USAGE: &str = "
Usage:

    tldr [options] <command>...
    tldr [options]

Options:

    -h --help           Show this screen
    -v --version        Show version information
    -l --list           List all commands in the cache
    -f --render <file>  Render a specific markdown file
    -o --os <type>      Override the operating system [linux, osx, sunos, windows]
    -u --update         Update the local cache
    -c --clear-cache    Clear the local cache
    -p --pager          Use a pager to page output
    -m --markdown       Display the raw markdown instead of rendering it
    -q --quiet          Suppress informational messages
    --config-path       Show config file path
    --seed-config       Create a basic config
    --color <when>      Control when to use color [always, auto, never] [default: auto]

Examples:

    $ tldr tar
    $ tldr --list

To control the cache:

    $ tldr --update
    $ tldr --clear-cache

To render a local file (for testing):

    $ tldr --render /path/to/file.md
";
const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz";
#[cfg(not(target_os = "windows"))]
const PAGER_COMMAND: &str = "less -R";

#[derive(Debug, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
struct Args {
    arg_command: Option<Vec<String>>,
    flag_help: bool,
    flag_version: bool,
    flag_list: bool,
    flag_render: Option<String>,
    flag_os: Option<OsType>,
    flag_update: bool,
    flag_clear_cache: bool,
    flag_pager: bool,
    flag_quiet: bool,
    flag_config_path: bool,
    flag_seed_config: bool,
    flag_markdown: bool,
    flag_color: ColorOptions,
}

/// Print page by path
fn print_page(path: &Path, enable_markdown: bool, config: &Config) -> Result<(), String> {
    // Open file
    let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?;
    let reader = BufReader::new(file);

    if enable_markdown {
        // Print the raw markdown of the file.
        for line in reader.lines() {
            println!("{}", line.unwrap());
        }
    } else {
        // Create tokenizer and print output
        let mut tokenizer = Tokenizer::new(reader);
        print_lines(&mut tokenizer, &config);
    };

    Ok(())
}

/// Set up display pager
#[cfg(not(target_os = "windows"))]
fn configure_pager() {
    Pager::with_default_pager(PAGER_COMMAND).setup();
}

#[cfg(target_os = "windows")]
fn configure_pager() {
    eprintln!("Warning: -p / --pager flag not available on Windows!");
}

fn should_update_cache(args: &Args, config: &Config) -> bool {
    args.flag_update
        || (config.updates.auto_update
            && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval))
}

/// Check the cache for freshness
fn check_cache(args: &Args, enable_styles: bool) {
    match Cache::last_update() {
        Some(ago) if ago > MAX_CACHE_AGE => {
            if args.flag_quiet {
                return;
            }

            // Only use color if enabled
            let warning_style = if enable_styles {
                Style::new().fg(Color::Yellow)
            } else {
                Style::default()
            };

            eprintln!(
                "{}",
                warning_style.paint(format!(
                    "The cache hasn't been updated for more than {} days.\n\
                         You should probably run `tldr --update` soon.",
                    MAX_CACHE_AGE.as_secs() / 24 / 3600
                ))
            );
        }
        Some(_) => {}
        None => {
            eprintln!("Cache not found. Please run `tldr --update`.");
            process::exit(1);
        }
    };
}

/// Clear the cache
fn clear_cache(quietly: bool) {
    Cache::clear().unwrap_or_else(|e| {
        match e {
            CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
                eprintln!("Could not delete cache: {}", msg)
            }
        };
        process::exit(1);
    });
    if !quietly {
        println!("Successfully deleted cache.");
    }
}

/// Update the cache
fn update_cache(cache: &Cache, quietly: bool) {
    cache.update().unwrap_or_else(|e| {
        match e {
            CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
                eprintln!("Could not update cache: {}", msg)
            }
        };
        process::exit(1);
    });
    if !quietly {
        println!("Successfully updated cache.");
    }
}

/// Show the config path
fn show_config_path() {
    match get_config_path() {
        Ok(config_file_path) => {
            println!("Config path is: {}", config_file_path.to_str().unwrap());
        }
        Err(ConfigError(msg)) => {
            eprintln!("Could not look up config_path: {}", msg);
            process::exit(1);
        }
        Err(_) => {
            eprintln!("Unknown error");
            process::exit(1);
        }
    }
}

/// Create seed config file and exit
fn create_config_and_exit() {
    match make_default_config() {
        Ok(config_file_path) => {
            println!(
                "Successfully created seed config file here: {}",
                config_file_path.to_str().unwrap()
            );
            process::exit(0);
        }
        Err(ConfigError(msg)) => {
            eprintln!("Could not create seed config: {}", msg);
            process::exit(1);
        }
        Err(_) => {
            eprintln!("Unknown error");
            process::exit(1);
        }
    }
}

#[cfg(feature = "logging")]
fn init_log() {
    env_logger::init();
}

#[cfg(not(feature = "logging"))]
fn init_log() {}

#[cfg(target_os = "linux")]
fn get_os() -> OsType {
    OsType::Linux
}

#[cfg(any(
    target_os = "macos",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "dragonfly"
))]
fn get_os() -> OsType {
    OsType::OsX
}

#[cfg(target_os = "windows")]
fn get_os() -> OsType {
    OsType::Windows
}

#[cfg(not(any(
    target_os = "linux",
    target_os = "macos",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "dragonfly",
    target_os = "windows"
)))]
fn get_os() -> OsType {
    OsType::Other
}

fn main() {
    // Initialize logger
    init_log();

    // Parse arguments
    let args: Args = Docopt::new(USAGE)
        .and_then(|d| d.deserialize())
        .unwrap_or_else(|e| e.exit());

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

    // Show version and exit
    if args.flag_version {
        let os = get_os();
        println!("{} v{} ({})", NAME, VERSION, os);
        process::exit(0);
    }

    // Show config file and path, pass through
    if args.flag_config_path {
        show_config_path();
    }

    // Create a basic config and exit
    if args.flag_seed_config {
        create_config_and_exit();
    }

    // Determine the usage of styles
    #[cfg(target_os = "windows")]
    let ansi_support = ansi_term::enable_ansi_support().is_ok();
    #[cfg(not(target_os = "windows"))]
    let ansi_support = true;

    let enable_styles = match args.flag_color {
        ColorOptions::Always => ansi_support,
        ColorOptions::Auto => ansi_support,
        ColorOptions::Never => false,
    };

    // Look up config file, if none is found fall back to default config.
    let config = match Config::load(enable_styles) {
        Ok(config) => config,
        Err(ConfigError(msg)) => {
            eprintln!("Could not load config: {}", msg);
            process::exit(1);
        }
        Err(e) => {
            eprintln!("Could not load config: {}", e);
            process::exit(1);
        }
    };

    if args.flag_pager || config.display.use_pager {
        configure_pager();
    }

    // Specify target OS
    let os: OsType = match args.flag_os {
        Some(os) => os,
        None => get_os(),
    };

    // Initialize cache
    let cache = Cache::new(ARCHIVE_URL, os);

    // Clear cache, pass through
    if args.flag_clear_cache {
        clear_cache(args.flag_quiet);
    }

    // Update cache, pass through
    let cache_updated = if should_update_cache(&args, &config) {
        update_cache(&cache, args.flag_quiet);
        true
    } else {
        false
    };

    // Render local file and exit
    if let Some(ref file) = args.flag_render {
        let path = PathBuf::from(file);
        if let Err(msg) = print_page(&path, args.flag_markdown, &config) {
            eprintln!("{}", msg);
            process::exit(1);
        } else {
            process::exit(0);
        };
    }

    // List cached commands and exit
    if args.flag_list {
        if !cache_updated {
            // Check cache for freshness
            check_cache(&args, enable_styles);
        }

        // Get list of pages
        let pages = cache.list_pages().unwrap_or_else(|e| {
            match e {
                CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
                    eprintln!("Could not get list of pages: {}", msg)
                }
            }
            process::exit(1);
        });

        // Print pages
        println!("{}", pages.join("\n"));
        process::exit(0);
    }

    // Show command from cache
    if let Some(ref command) = args.arg_command {
        let command = command.join("-");

        if !cache_updated {
            // Check cache for freshness
            check_cache(&args, enable_styles);
        }

        // Search for command in cache
        if let Some(path) = cache.find_page(&command) {
            if let Err(msg) = print_page(&path, args.flag_markdown, &config) {
                eprintln!("{}", msg);
                process::exit(1);
            } else {
                process::exit(0);
            }
        } else {
            if !args.flag_quiet {
                println!("Page {} not found in cache", &command);
                println!("Try updating with `tldr --update`, or submit a pull request to:");
                println!("https://github.com/tldr-pages/tldr");
            }
            process::exit(1);
        }
    }

    // Some flags can be run without a command.
    if !(args.flag_update || args.flag_clear_cache || args.flag_config_path) {
        eprintln!("{}", USAGE);
        process::exit(1);
    }
}

#[cfg(test)]
mod test {
    use crate::{Args, OsType, USAGE};
    use docopt::{Docopt, Error};

    fn test_helper(argv: &[&str]) -> Result<Args, Error> {
        Docopt::new(USAGE).and_then(|d| d.argv(argv.iter()).deserialize())
    }

    #[test]
    fn test_docopt_os_case_insensitive() {
        let argv = vec!["cp", "--os", "LiNuX"];
        let os = test_helper(&argv).unwrap().flag_os.unwrap();
        assert_eq!(OsType::Linux, os);
    }

    #[test]
    fn test_docopt_expect_error() {
        let argv = vec!["cp", "--os", "lindows"];
        assert!(!test_helper(&argv).is_ok());
    }
}
<commit_msg>Disable styling for `--color auto` when `NO_COLOR` is set<commit_after>//! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust.
//
// Copyright (c) 2015-2020 tealdeer developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be
// copied, modified, or distributed except according to those terms.

#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::similar_names)]
#![allow(clippy::module_name_repetitions)]

#[cfg(feature = "logging")]
extern crate env_logger;

use std::env;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::process;

use ansi_term::{Color, Style};
use app_dirs::AppInfo;
use docopt::Docopt;
#[cfg(not(target_os = "windows"))]
use pager::Pager;
use serde_derive::Deserialize;

mod cache;
mod config;
mod error;
mod formatter;
mod tokenizer;
mod types;

use crate::cache::Cache;
use crate::config::{get_config_path, make_default_config, Config, MAX_CACHE_AGE};
use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError};
use crate::formatter::print_lines;
use crate::tokenizer::Tokenizer;
use crate::types::{ColorOptions, OsType};

const NAME: &str = "tealdeer";
const APP_INFO: AppInfo = AppInfo {
    name: NAME,
    author: NAME,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const USAGE: &str = "
Usage:

    tldr [options] <command>...
    tldr [options]

Options:

    -h --help           Show this screen
    -v --version        Show version information
    -l --list           List all commands in the cache
    -f --render <file>  Render a specific markdown file
    -o --os <type>      Override the operating system [linux, osx, sunos, windows]
    -u --update         Update the local cache
    -c --clear-cache    Clear the local cache
    -p --pager          Use a pager to page output
    -m --markdown       Display the raw markdown instead of rendering it
    -q --quiet          Suppress informational messages
    --config-path       Show config file path
    --seed-config       Create a basic config
    --color <when>      Control when to use color [always, auto, never] [default: auto]

Examples:

    $ tldr tar
    $ tldr --list

To control the cache:

    $ tldr --update
    $ tldr --clear-cache

To render a local file (for testing):

    $ tldr --render /path/to/file.md
";
const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz";
#[cfg(not(target_os = "windows"))]
const PAGER_COMMAND: &str = "less -R";

#[derive(Debug, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
struct Args {
    arg_command: Option<Vec<String>>,
    flag_help: bool,
    flag_version: bool,
    flag_list: bool,
    flag_render: Option<String>,
    flag_os: Option<OsType>,
    flag_update: bool,
    flag_clear_cache: bool,
    flag_pager: bool,
    flag_quiet: bool,
    flag_config_path: bool,
    flag_seed_config: bool,
    flag_markdown: bool,
    flag_color: ColorOptions,
}

/// Print page by path
fn print_page(path: &Path, enable_markdown: bool, config: &Config) -> Result<(), String> {
    // Open file
    let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?;
    let reader = BufReader::new(file);

    if enable_markdown {
        // Print the raw markdown of the file.
        for line in reader.lines() {
            println!("{}", line.unwrap());
        }
    } else {
        // Create tokenizer and print output
        let mut tokenizer = Tokenizer::new(reader);
        print_lines(&mut tokenizer, &config);
    };

    Ok(())
}

/// Set up display pager
#[cfg(not(target_os = "windows"))]
fn configure_pager() {
    Pager::with_default_pager(PAGER_COMMAND).setup();
}

#[cfg(target_os = "windows")]
fn configure_pager() {
    eprintln!("Warning: -p / --pager flag not available on Windows!");
}

fn should_update_cache(args: &Args, config: &Config) -> bool {
    args.flag_update
        || (config.updates.auto_update
            && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval))
}

/// Check the cache for freshness
fn check_cache(args: &Args, enable_styles: bool) {
    match Cache::last_update() {
        Some(ago) if ago > MAX_CACHE_AGE => {
            if args.flag_quiet {
                return;
            }

            // Only use color if enabled
            let warning_style = if enable_styles {
                Style::new().fg(Color::Yellow)
            } else {
                Style::default()
            };

            eprintln!(
                "{}",
                warning_style.paint(format!(
                    "The cache hasn't been updated for more than {} days.\n\
                         You should probably run `tldr --update` soon.",
                    MAX_CACHE_AGE.as_secs() / 24 / 3600
                ))
            );
        }
        Some(_) => {}
        None => {
            eprintln!("Cache not found. Please run `tldr --update`.");
            process::exit(1);
        }
    };
}

/// Clear the cache
fn clear_cache(quietly: bool) {
    Cache::clear().unwrap_or_else(|e| {
        match e {
            CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
                eprintln!("Could not delete cache: {}", msg)
            }
        };
        process::exit(1);
    });
    if !quietly {
        println!("Successfully deleted cache.");
    }
}

/// Update the cache
fn update_cache(cache: &Cache, quietly: bool) {
    cache.update().unwrap_or_else(|e| {
        match e {
            CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
                eprintln!("Could not update cache: {}", msg)
            }
        };
        process::exit(1);
    });
    if !quietly {
        println!("Successfully updated cache.");
    }
}

/// Show the config path
fn show_config_path() {
    match get_config_path() {
        Ok(config_file_path) => {
            println!("Config path is: {}", config_file_path.to_str().unwrap());
        }
        Err(ConfigError(msg)) => {
            eprintln!("Could not look up config_path: {}", msg);
            process::exit(1);
        }
        Err(_) => {
            eprintln!("Unknown error");
            process::exit(1);
        }
    }
}

/// Create seed config file and exit
fn create_config_and_exit() {
    match make_default_config() {
        Ok(config_file_path) => {
            println!(
                "Successfully created seed config file here: {}",
                config_file_path.to_str().unwrap()
            );
            process::exit(0);
        }
        Err(ConfigError(msg)) => {
            eprintln!("Could not create seed config: {}", msg);
            process::exit(1);
        }
        Err(_) => {
            eprintln!("Unknown error");
            process::exit(1);
        }
    }
}

#[cfg(feature = "logging")]
fn init_log() {
    env_logger::init();
}

#[cfg(not(feature = "logging"))]
fn init_log() {}

#[cfg(target_os = "linux")]
fn get_os() -> OsType {
    OsType::Linux
}

#[cfg(any(
    target_os = "macos",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "dragonfly"
))]
fn get_os() -> OsType {
    OsType::OsX
}

#[cfg(target_os = "windows")]
fn get_os() -> OsType {
    OsType::Windows
}

#[cfg(not(any(
    target_os = "linux",
    target_os = "macos",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "dragonfly",
    target_os = "windows"
)))]
fn get_os() -> OsType {
    OsType::Other
}

fn main() {
    // Initialize logger
    init_log();

    // Parse arguments
    let args: Args = Docopt::new(USAGE)
        .and_then(|d| d.deserialize())
        .unwrap_or_else(|e| e.exit());

    // Show version and exit
    if args.flag_version {
        let os = get_os();
        println!("{} v{} ({})", NAME, VERSION, os);
        process::exit(0);
    }

    // Show config file and path, pass through
    if args.flag_config_path {
        show_config_path();
    }

    // Create a basic config and exit
    if args.flag_seed_config {
        create_config_and_exit();
    }

    // Determine the usage of styles
    #[cfg(target_os = "windows")]
    let ansi_support = ansi_term::enable_ansi_support().is_ok();
    #[cfg(not(target_os = "windows"))]
    let ansi_support = true;

    let enable_styles = match args.flag_color {
        // Always use styling as long as there is `ansi_support`
        ColorOptions::Always => ansi_support,
        // Disable if:
        // * NO_COLOR env var is set to anything: https://no-color.org/
        ColorOptions::Auto => ansi_support && env::var_os("NO_COLOR").is_none(),
        // Disable styling
        ColorOptions::Never => false,
    };

    // Look up config file, if none is found fall back to default config.
    let config = match Config::load(enable_styles) {
        Ok(config) => config,
        Err(ConfigError(msg)) => {
            eprintln!("Could not load config: {}", msg);
            process::exit(1);
        }
        Err(e) => {
            eprintln!("Could not load config: {}", e);
            process::exit(1);
        }
    };

    if args.flag_pager || config.display.use_pager {
        configure_pager();
    }

    // Specify target OS
    let os: OsType = match args.flag_os {
        Some(os) => os,
        None => get_os(),
    };

    // Initialize cache
    let cache = Cache::new(ARCHIVE_URL, os);

    // Clear cache, pass through
    if args.flag_clear_cache {
        clear_cache(args.flag_quiet);
    }

    // Update cache, pass through
    let cache_updated = if should_update_cache(&args, &config) {
        update_cache(&cache, args.flag_quiet);
        true
    } else {
        false
    };

    // Render local file and exit
    if let Some(ref file) = args.flag_render {
        let path = PathBuf::from(file);
        if let Err(msg) = print_page(&path, args.flag_markdown, &config) {
            eprintln!("{}", msg);
            process::exit(1);
        } else {
            process::exit(0);
        };
    }

    // List cached commands and exit
    if args.flag_list {
        if !cache_updated {
            // Check cache for freshness
            check_cache(&args, enable_styles);
        }

        // Get list of pages
        let pages = cache.list_pages().unwrap_or_else(|e| {
            match e {
                CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
                    eprintln!("Could not get list of pages: {}", msg)
                }
            }
            process::exit(1);
        });

        // Print pages
        println!("{}", pages.join("\n"));
        process::exit(0);
    }

    // Show command from cache
    if let Some(ref command) = args.arg_command {
        let command = command.join("-");

        if !cache_updated {
            // Check cache for freshness
            check_cache(&args, enable_styles);
        }

        // Search for command in cache
        if let Some(path) = cache.find_page(&command) {
            if let Err(msg) = print_page(&path, args.flag_markdown, &config) {
                eprintln!("{}", msg);
                process::exit(1);
            } else {
                process::exit(0);
            }
        } else {
            if !args.flag_quiet {
                println!("Page {} not found in cache", &command);
                println!("Try updating with `tldr --update`, or submit a pull request to:");
                println!("https://github.com/tldr-pages/tldr");
            }
            process::exit(1);
        }
    }

    // Some flags can be run without a command.
    if !(args.flag_update || args.flag_clear_cache || args.flag_config_path) {
        eprintln!("{}", USAGE);
        process::exit(1);
    }
}

#[cfg(test)]
mod test {
    use crate::{Args, OsType, USAGE};
    use docopt::{Docopt, Error};

    fn test_helper(argv: &[&str]) -> Result<Args, Error> {
        Docopt::new(USAGE).and_then(|d| d.argv(argv.iter()).deserialize())
    }

    #[test]
    fn test_docopt_os_case_insensitive() {
        let argv = vec!["cp", "--os", "LiNuX"];
        let os = test_helper(&argv).unwrap().flag_os.unwrap();
        assert_eq!(OsType::Linux, os);
    }

    #[test]
    fn test_docopt_expect_error() {
        let argv = vec!["cp", "--os", "lindows"];
        assert!(!test_helper(&argv).is_ok());
    }
}
<|endoftext|>