milo-parser 0.5.0

A fast and embeddable HTTP/1.1 parser.
Documentation
use std::env;
use std::ffi::c_void;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process::ExitCode;

use milo_parser::{CALLBACK_ACTIVE_ALL, Parser, STATE_ERROR};

struct Options {
  file: Option<PathBuf>,
  mode: Mode,
}

#[derive(Clone, Copy, PartialEq)]
enum Mode {
  Autodetect,
  Request,
  Response,
}

struct Context {
  output: Vec<String>,
}

enum RunError {
  Usage(String),
  Io(String),
}

fn print_usage() {
  eprintln!("Usage: milo-parser [-f|--file PATH] [-o|--request | -i|--response]");
}

fn parse_args() -> Result<Options, String> {
  let mut args = env::args().skip(1);
  let mut options = Options {
    file: None,
    mode: Mode::Autodetect,
  };

  while let Some(arg) = args.next() {
    match arg.as_str() {
      "-f" | "--file" => {
        let path = args.next().ok_or_else(|| format!("Missing value for {arg}"))?;
        options.file = Some(PathBuf::from(path));
      }
      "-o" | "--request" => {
        if options.mode != Mode::Autodetect {
          return Err("Cannot use both request and response mode".into());
        }

        options.mode = Mode::Request;
      }
      "-i" | "--response" => {
        if options.mode != Mode::Autodetect {
          return Err("Cannot use both request and response mode".into());
        }

        options.mode = Mode::Response;
      }
      "-h" | "--help" => {
        print_usage();
        std::process::exit(0);
      }
      _ => return Err(format!("Unknown argument: {arg}")),
    }
  }

  Ok(options)
}

fn read_input(options: &Options) -> io::Result<Vec<u8>> {
  match &options.file {
    Some(path) => fs::read(path),
    None => {
      let mut input = Vec::new();
      io::stdin().read_to_end(&mut input)?;
      Ok(input)
    }
  }
}

fn quote(value: &str) -> String {
  let mut quoted = String::with_capacity(value.len() + 2);
  quoted.push('"');

  for c in value.chars() {
    match c {
      '"' => quoted.push_str("\\\""),
      '\\' => quoted.push_str("\\\\"),
      '\n' => quoted.push_str("\\n"),
      '\r' => quoted.push_str("\\r"),
      '\t' => quoted.push_str("\\t"),
      _ => quoted.push(c),
    }
  }

  quoted.push('"');
  quoted
}

fn append_output(parser: &mut Parser, line: String) {
  let mut context = unsafe { Box::from_raw(parser.context as *mut Context) };
  context.output.push(line);
  let _ = Box::into_raw(context);
}

fn event(parser: &mut Parser, offset: usize, size: usize, name: &str) {
  append_output(parser, format!("offset={offset} size={size} event={name}"));
}

fn on_error(parser: &mut Parser, offset: usize, size: usize) {
  append_output(
    parser,
    format!(
      "offset={offset} size={size} event=error error={} description={}",
      parser.error_code_str(),
      quote(parser.error_description_str())
    ),
  );
}

fn on_finish(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "finish"); }

fn on_message_start(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "message_start"); }

fn on_message_complete(parser: &mut Parser, offset: usize, size: usize) {
  event(parser, offset, size, "message_complete");
}

fn on_request(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "request"); }

fn on_response(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "response"); }

fn on_reset(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "reset"); }

fn on_method(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "method"); }

fn on_url(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "url"); }

fn on_protocol(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "protocol"); }

fn on_version(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "version"); }

fn on_status(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "status"); }

fn on_reason(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "reason"); }

fn on_header_name(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "header_name"); }

fn on_header_value(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "header_value"); }

fn on_headers(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "headers"); }

fn on_connect(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "connect"); }

fn on_upgrade(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "upgrade"); }

fn on_chunk_length(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "chunk_length"); }

fn on_chunk_extension_name(parser: &mut Parser, offset: usize, size: usize) {
  event(parser, offset, size, "chunk_extension_name");
}

fn on_chunk_extension_value(parser: &mut Parser, offset: usize, size: usize) {
  event(parser, offset, size, "chunk_extension_value");
}

fn on_chunk(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "chunk"); }

fn on_body(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "body"); }

fn on_data(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "data"); }

fn on_trailer_name(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "trailer_name"); }

fn on_trailer_value(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "trailer_value"); }

fn on_trailers(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "trailers"); }

fn on_state_change(parser: &mut Parser, offset: usize, size: usize) { event(parser, offset, size, "state_change"); }

fn create_parser(mode: Mode) -> Parser {
  let mut parser = Parser::new();
  let context = Box::new(Context { output: Vec::new() });
  parser.context = Box::into_raw(context) as *mut c_void;
  parser.active_callbacks = CALLBACK_ACTIVE_ALL;

  match mode {
    Mode::Autodetect => {
      parser.autodetect = true;
    }
    Mode::Request => {
      parser.autodetect = false;
      parser.is_request = true;
    }
    Mode::Response => {
      parser.autodetect = false;
      parser.is_request = false;
    }
  }

  parser.callbacks.on_error = on_error;
  parser.callbacks.on_finish = on_finish;
  parser.callbacks.on_message_start = on_message_start;
  parser.callbacks.on_message_complete = on_message_complete;
  parser.callbacks.on_request = on_request;
  parser.callbacks.on_response = on_response;
  parser.callbacks.on_reset = on_reset;
  parser.callbacks.on_method = on_method;
  parser.callbacks.on_url = on_url;
  parser.callbacks.on_protocol = on_protocol;
  parser.callbacks.on_version = on_version;
  parser.callbacks.on_status = on_status;
  parser.callbacks.on_reason = on_reason;
  parser.callbacks.on_header_name = on_header_name;
  parser.callbacks.on_header_value = on_header_value;
  parser.callbacks.on_headers = on_headers;
  parser.callbacks.on_connect = on_connect;
  parser.callbacks.on_upgrade = on_upgrade;
  parser.callbacks.on_chunk_length = on_chunk_length;
  parser.callbacks.on_chunk_extension_name = on_chunk_extension_name;
  parser.callbacks.on_chunk_extension_value = on_chunk_extension_value;
  parser.callbacks.on_chunk = on_chunk;
  parser.callbacks.on_body = on_body;
  parser.callbacks.on_data = on_data;
  parser.callbacks.on_trailer_name = on_trailer_name;
  parser.callbacks.on_trailer_value = on_trailer_value;
  parser.callbacks.on_trailers = on_trailers;
  parser.callbacks.on_state_change = on_state_change;

  parser
}

fn run() -> Result<bool, RunError> {
  let options = parse_args().map_err(RunError::Usage)?;
  let input = read_input(&options).map_err(|e| RunError::Io(e.to_string()))?;
  let mut parser = create_parser(options.mode);

  parser.parse(input.as_ptr(), input.len());

  if parser.state != STATE_ERROR {
    parser.finish();
  }

  let failed = parser.state == STATE_ERROR;
  let context = unsafe { Box::from_raw(parser.context as *mut Context) };

  for line in &context.output {
    println!("{line}");
  }

  Ok(failed)
}

fn main() -> ExitCode {
  match run() {
    Ok(false) => ExitCode::SUCCESS,
    Ok(true) => ExitCode::from(1),
    Err(RunError::Usage(error)) => {
      eprintln!("{error}");
      print_usage();
      ExitCode::from(2)
    }
    Err(RunError::Io(error)) => {
      eprintln!("{error}");
      ExitCode::from(1)
    }
  }
}