Add tests, rename Status to Header, implement redirect and proper input handling

CandyCorvid
Jul 13, 2023, 10:33 AM
WGRFJRTEXOKY7PV536O62WUE32T24VQEHFQP67GEZZ67AE37ZWUAC

Dependencies

  • [2] OSV63NXN Implement contiguous preformat blocks with alt-text
  • [3] DBKKKHC2 Initial commit
  • [4] HVFBGX2A Implement basic text/gemini
  • [5] 3SPNKI46 Improve parsing. Add modules that were missed
  • [6] ONQEIR5B WIP mime-type handling
  • [7] AOJXTWBZ Refactor read_until
  • [8] AMTMTTJT Upgrade status parsing

Change contents

  • replacement in src/response.rs at line 3
    [3.1650][3.1650:1668]()
    pub enum Status {
    [3.1650]
    [3.1668]
    pub enum Header {
  • edit in src/response.rs at line 5
    [3.1698][3.1698:1726]()
    /// has a response body
  • replacement in src/response.rs at line 143
    [3.5532][3.5532:5571]()
    impl TryFrom<raw::Header> for Status {
    [3.5532]
    [3.5571]
    impl TryFrom<raw::Header> for Header {
  • replacement in src/response.rs at line 150
    [3.5779][3.5779:6145]()
    1 => Some(Status::Input((sub, meta).into())),
    2 => Some(Status::Success((sub, meta).into())),
    3 => Some(Status::Redirect((sub, meta).into())),
    4 => Some(Status::FailTemp((sub, meta).into())),
    5 => Some(Status::FailPerm((sub, meta).into())),
    6 => Some(Status::CertRequired((sub, meta).into())),
    [3.5779]
    [3.6145]
    1 => Some(Header::Input((sub, meta).into())),
    2 => Some(Header::Success((sub, meta).into())),
    3 => Some(Header::Redirect((sub, meta).into())),
    4 => Some(Header::FailTemp((sub, meta).into())),
    5 => Some(Header::FailPerm((sub, meta).into())),
    6 => Some(Header::CertRequired((sub, meta).into())),
  • file addition: media.rs (----------)
    [3.15]
    use std::str::pattern::Pattern;
    use url::Url;
    #[derive(Clone, Debug)]
    pub enum Media<'a> {
    Gemini(Gemini<'a>),
    Text(&'a str),
    }
    impl<'a> Media<'a> {
    pub fn display(&self) {
    match self {
    Media::Gemini(g) => println!("{g:#?}"), // TODO
    Media::Text(s) => println!("{s}"),
    }
    }
    }
    #[derive(Clone, Debug)]
    pub struct Gemini<'a> {
    // TODO include lang component
    lines: Vec<Line<'a>>,
    }
    #[derive(Clone, Debug)]
    enum RawLine<'a> {
    Toggle { alt: TextLine<'a> },
    Line(Line<'a>),
    }
    #[derive(Clone, Debug)]
    pub enum Line<'a> {
    Heading {
    level: u8,
    title: TextLine<'a>,
    },
    Link {
    url: TextLine<'a>,
    description: Option<TextLine<'a>>,
    },
    Text(TextLine<'a>),
    Preformatted(Preformat<'a>),
    ListItem(TextLine<'a>),
    Quote(TextLine<'a>),
    }
    #[derive(Clone, Debug)]
    pub struct Preformat<'a> {
    alt: TextLine<'a>,
    lines: Vec<TextLine<'a>>,
    }
    fn split_trim_maybe_once<'a, P: Pattern<'a> + Copy>(
    s: &'a str,
    p: P,
    ) -> (&'a str, Option<&'a str>) {
    match s.split_once(p) {
    Some((s, rem)) => (s, Some(rem.trim_start_matches(p))),
    None => (s, None),
    }
    }
    fn string_to_preformat(string: TextLine<'_>) -> Option<TextLine<'_>> {
    let line = string.0;
    if line.starts_with("```") {
    // ignore anything after the lead chars on preformat lines
    return None;
    } else {
    // ignore any other formatting between preformat toggle lines
    return Some(string);
    }
    }
    fn string_to_line(string: TextLine<'_>) -> RawLine {
    let line = string.0;
    RawLine::Line({
    if line.starts_with("```") {
    // ignore anything after the lead chars on preformat lines
    return RawLine::Toggle {
    alt: TextLine(&line[3..]),
    };
    } else if line.starts_with("=> ") {
    let line = &line[3..];
    match line.split_once(' ') {
    Some((url, desc)) => {
    let url = url.trim_start_matches(' ');
    Line::Link {
    url: TextLine(url),
    description: Some(TextLine(desc)),
    }
    }
    None => Line::Link {
    url: TextLine(line),
    description: None,
    },
    }
    } else if line.starts_with("* ") {
    Line::ListItem(TextLine(&line[2..]))
    } else if line.starts_with("# ") {
    Line::Heading {
    level: 1,
    title: TextLine(&line[2..]),
    }
    } else if line.starts_with("## ") {
    Line::Heading {
    level: 2,
    title: TextLine(&line[3..]),
    }
    } else if line.starts_with("### ") {
    Line::Heading {
    level: 3,
    title: TextLine(&line[4..]),
    }
    } else if line.starts_with(">") {
    Line::Quote(TextLine(&line[1..]))
    } else {
    Line::Text(string)
    }
    })
    }
    fn string_to_lines(value: &str) -> Vec<Line> {
    {
    let mut outer = vec![];
    let mut preformat_block: Option<Preformat> = None;
    let lines = value.lines().map(TextLine);
    for line in lines {
    if preformat_block.is_some() {
    match string_to_preformat(line) {
    // if we hit a toggle line, switch preformatting mode
    None => {
    let Some(i) = preformat_block.take()
    else {unreachable!("This is within the is_some arm of the if")};
    outer.push(Line::Preformatted(i));
    }
    Some(p) => preformat_block.as_mut().unwrap().lines.push(p),
    }
    } else {
    match string_to_line(line) {
    // if we hit a toggle line, switch preformatting mode
    RawLine::Toggle { alt } => {
    preformat_block = Some(Preformat { alt, lines: vec![] });
    }
    RawLine::Line(l) => outer.push(l),
    }
    }
    }
    outer
    }
    }
    // guaranteed to be only a single line
    #[derive(Copy, Clone, Debug)]
    pub struct TextLine<'a>(&'a str);
    impl<'a> From<&'a str> for Gemini<'a> {
    fn from(value: &'a str) -> Self {
    Gemini {
    lines: string_to_lines(value),
    }
    }
    }
  • replacement in src/main.rs at line 9
    [3.3767][3.32:80]()
    io::{BufReader, Result as ioResult, Write},
    [3.3767]
    [3.3807]
    io::{BufReader, Read, Result as ioResult, Write},
  • edit in src/main.rs at line 16
    [3.127]
    [3.179]
    mod media;
  • edit in src/main.rs at line 27
    [3.321][3.3859:3860](),[3.3859][3.3859:3860]()
  • edit in src/main.rs at line 28
    [3.3924]
    [3.3924]
    // TODO make tests using a fake connection to a local buffer
  • replacement in src/main.rs at line 36
    [3.4103][3.4103:4121]()
    run_url(url);
    [3.4103]
    [3.4121]
    let mut url: Url = url.parse().expect("Not a valid URL");
    while let Some(new_url) = run_url(url) {
    url = new_url;
    }
  • replacement in src/main.rs at line 42
    [3.4124][3.6916:7019]()
    fn run_url(url: String) -> Option<Url> {
    let mut url: Url = url.parse().expect("Not a valid URL");
    [3.4124]
    [3.322]
    fn run_url(url: Url) -> Option<Url> {
    // must make a new connection per request
  • edit in src/main.rs at line 50
    [3.4396]
    [3.4396]
    handle_response_header(head, url, stream)
    }
  • edit in src/main.rs at line 54
    [3.4397]
    [3.7091]
    fn handle_response_header(
    head: response::Header,
    mut url: Url,
    mut stream: impl Read,
    ) -> Option<Url> {
  • replacement in src/main.rs at line 60
    [3.7108][3.7108:7148]()
    response::Status::Input(s) => {
    [3.7108]
    [3.7148]
    response::Header::Input(s) => {
  • replacement in src/main.rs at line 63
    [3.7231][3.7231:7408]()
    let input = match sensitive {
    Some(true) => rpassword::read_password(),
    _ => std::io::stdin().lines().next().expect("End of input"),
    [3.7231]
    [3.7408]
    let input = if let Some(true) = sensitive {
    // Read sensitive input
    rpassword::read_password()
    } else {
    // Read plain input
    std::io::stdin().lines().next().expect("End of input")
  • replacement in src/main.rs at line 75
    [3.7593][3.7593:7635]()
    response::Status::Success(s) => {
    [3.7593]
    [3.7635]
    response::Header::Success(s) => {
  • replacement in src/main.rs at line 78
    [3.156][3.7761:7803](),[3.7761][3.7761:7803]()
    // TODO use mime for anything
    [3.156]
    [3.7803]
    //TODO remove this debug output
  • replacement in src/main.rs at line 85
    [3.7915][3.7915:7958]()
    response::Status::Redirect(s) => {
    [3.7915]
    [3.7958]
    response::Header::Redirect(s) => {
  • replacement in src/main.rs at line 95
    [3.8338][3.8338:8381]()
    response::Status::FailTemp(s) => {
    [3.8338]
    [3.8381]
    response::Header::FailTemp(s) => {
  • replacement in src/main.rs at line 101
    [3.8570][3.8570:8613]()
    response::Status::FailPerm(s) => {
    [3.8570]
    [3.8613]
    response::Header::FailPerm(s) => {
  • replacement in src/main.rs at line 107
    [3.8784][3.8784:8831]()
    response::Status::CertRequired(s) => {
    [3.8784]
    [3.8831]
    response::Header::CertRequired(s) => {
  • edit in src/main.rs at line 125
    [3.512][3.4501:4504](),[3.9015][3.4501:4504](),[3.4501][3.4501:4504](),[3.4504][3.212:1018](),[3.1018][2.48:79](),[2.79][3.1040:1126](),[3.1040][3.1040:1126](),[3.1126][2.80:117](),[2.117][3.1162:1223](),[3.1162][3.1162:1223](),[3.1223][2.118:244](),[2.244][3.663:669](),[3.1223][3.663:669](),[3.663][3.663:669](),[3.669][3.1224:1484](),[3.1484][3.825:835](),[3.825][3.825:835](),[3.835][3.1485:1491](),[3.1491][2.245:679](),[2.679][3.1565:1618](),[3.1565][3.1565:1618](),[3.1618][2.680:721](),[2.721][3.1643:1718](),[3.1643][3.1643:1718](),[3.1718][2.722:1215](),[2.1215][3.2469:2495](),[3.2469][3.2469:2495](),[3.2688][3.2688:2710](),[3.2710][2.1216:1368](),[2.1368][3.2710:2728](),[3.2710][3.2710:2728](),[3.2728][2.1369:1548](),[2.1548][3.2830:2909](),[3.2830][3.2830:2909](),[3.2909][2.1549:1647](),[2.1647][3.2969:3048](),[3.2969][3.2969:3048](),[3.3048][2.1648:1747](),[2.1747][3.3109:3188](),[3.3109][3.3109:3188](),[3.3188][2.1748:1918](),[2.1918][3.3307:3389](),[3.3307][3.3307:3389](),[3.3389][2.1919:3059](),[2.3059][3.3806:3828](),[3.3806][3.3806:3828](),[3.3926][3.3926:3944](),[3.3944][2.3060:3102](),[2.3102][3.3982:4282](),[3.3982][3.3982:4282](),[3.4282][3.851:857](),[3.851][3.851:857]()
    }
    mod media {
    use std::str::pattern::Pattern;
    use url::Url;
    #[derive(Clone, Debug)]
    pub enum Media<'a> {
    Gemini(Gemini<'a>),
    Text(&'a str),
    }
    impl<'a> Media<'a> {
    pub fn display(&self) {
    match self {
    Media::Gemini(g) => println!("{g:#?}"), // TODO
    Media::Text(s) => println!("{s}"),
    }
    }
    }
    #[derive(Clone, Debug)]
    pub struct Gemini<'a> {
    // TODO include lang component
    lines: Vec<Line<'a>>,
    }
    #[derive(Clone, Debug)]
    enum RawLine<'a> {
    Toggle { alt: TextLine<'a> },
    Line(Line<'a>),
    }
    #[derive(Clone, Debug)]
    pub enum Line<'a> {
    Heading {
    level: u8,
    title: TextLine<'a>,
    },
    Link {
    url: TextLine<'a>,
    description: Option<TextLine<'a>>,
    },
    Text(TextLine<'a>),
    Preformatted(Preformat<'a>),
    ListItem(TextLine<'a>),
    Quote(TextLine<'a>),
    }
    #[derive(Clone, Debug)]
    pub struct Preformat<'a> {
    alt: TextLine<'a>,
    lines: Vec<TextLine<'a>>,
    }
    fn split_trim_maybe_once<'a, P: Pattern<'a> + Copy>(
    s: &'a str,
    p: P,
    ) -> (&'a str, Option<&'a str>) {
    match s.split_once(p) {
    Some((s, rem)) => (s, Some(rem.trim_start_matches(p))),
    None => (s, None),
    }
    }
    fn string_to_preformat(string: TextLine<'_>) -> Option<TextLine<'_>> {
    let line = string.0;
    if line.starts_with("```") {
    // ignore anything after the lead chars on preformat lines
    return None;
    } else {
    // ignore any other formatting between preformat toggle lines
    return Some(string);
    }
    }
    fn string_to_line(string: TextLine<'_>) -> RawLine {
    let line = string.0;
    RawLine::Line({
    if line.starts_with("```") {
    // ignore anything after the lead chars on preformat lines
    return RawLine::Toggle {
    alt: TextLine(&line[3..]),
    };
    } else if line.starts_with("=> ") {
    let line = &line[3..];
    match line.split_once(' ') {
    Some((url, desc)) => {
    let url = url.trim_start_matches(' ');
    Line::Link {
    url: TextLine(url),
    description: Some(TextLine(desc)),
    }
    }
    None => Line::Link {
    url: TextLine(line),
    description: None,
    },
    }
    } else if line.starts_with("* ") {
    Line::ListItem(TextLine(&line[2..]))
    } else if line.starts_with("# ") {
    Line::Heading {
    level: 1,
    title: TextLine(&line[2..]),
    }
    } else if line.starts_with("## ") {
    Line::Heading {
    level: 2,
    title: TextLine(&line[3..]),
    }
    } else if line.starts_with("### ") {
    Line::Heading {
    level: 3,
    title: TextLine(&line[4..]),
    }
    } else if line.starts_with(">") {
    Line::Quote(TextLine(&line[1..]))
    } else {
    Line::Text(string)
    }
    })
    }
    fn string_to_lines(value: &str) -> Vec<Line> {
    {
    let mut outer = vec![];
    let mut preformat_block: Option<Preformat> = None;
    let lines = value.lines().map(TextLine);
    for line in lines {
    if preformat_block.is_some() {
    match string_to_preformat(line) {
    // if we hit a toggle line, switch preformatting mode
    None => {
    let Some(i) = preformat_block.take()
    else {unreachable!("This is within the is_some arm of the if")};
    outer.push(Line::Preformatted(i));
    }
    Some(p) => preformat_block.as_mut().unwrap().lines.push(p),
    }
    } else {
    match string_to_line(line) {
    // if we hit a toggle line, switch preformatting mode
    RawLine::Toggle { alt } => {
    preformat_block = Some(Preformat { alt, lines: vec![] });
    }
    RawLine::Line(l) => outer.push(l),
    }
    }
    }
    outer
    }
    }
    // guaranteed to be only a single line
    #[derive(Copy, Clone, Debug)]
    pub struct TextLine<'a>(&'a str);
    impl<'a> From<&'a str> for Gemini<'a> {
    fn from(value: &'a str) -> Self {
    Gemini {
    lines: string_to_lines(value),
    }
    }
    }
  • replacement in src/main.rs at line 127
    [3.860][3.448:525](),[3.448][3.448:525]()
    fn read_response_header(stream: &mut Stream) -> ioResult<response::Status> {
    [3.860]
    [3.157]
    fn read_response_header(stream: &mut Stream) -> ioResult<response::Header> {
  • edit in src/main.rs at line 159
    [3.5853]
    #[cfg(test)]
    mod tests {
    use url::Url;
    use super::{
    handle_response_header,
    response::{header, Header},
    };
    #[test]
    fn handle_redirect_none() {
    handle_redirect("http://url.com".parse().unwrap(), None);
    }
    #[test]
    fn handle_redirect_perm() {
    handle_redirect("http://url.com".parse().unwrap(), Some(false));
    }
    #[test]
    fn handle_redirect_temp() {
    handle_redirect("http://url.com".parse().unwrap(), Some(true));
    }
    fn handle_redirect(expect_url: Url, temp: Option<bool>) {
    let head = Header::Redirect(header::Redirect {
    url: expect_url.clone(),
    temporary: temp,
    });
    let url = "gopher://abc.de".parse().unwrap();
    let mut stream: &[u8] = b"";
    let Some(url) = handle_response_header(head, url, &mut stream)
    else {panic!("expected redirect handling to return a url")};
    assert_eq!(url, expect_url);
    }
    }