Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-remote-helper src protocol.rs
use thiserror::Error;

#[derive(Debug, Error)]
pub(super) enum Error {
    #[error("invalid command `{0}`")]
    InvalidCommand(String),
}

#[derive(Debug, PartialEq, Eq)]
pub(super) enum Command {
    Capabilities,
    List,
    ListForPush,
    Fetch { oid: String, refstr: String },
    Push(String),
    Option { key: String, value: Option<String> },
}

#[derive(Debug, PartialEq, Eq)]
pub(super) enum Line {
    Valid(Command),
    Blank,
}

impl Command {
    pub(super) fn parse_line(line: &str) -> Result<Line, Error> {
        let line = line.trim();
        if line.is_empty() {
            return Ok(Line::Blank);
        }

        // Split the command verb from the rest of the line.
        let (cmd, args) = line.split_once(' ').unwrap_or((line, ""));
        let args = args.trim();

        match cmd {
            "capabilities" => Ok(Line::Valid(Command::Capabilities)),
            "list" => {
                if args == "for-push" {
                    Ok(Line::Valid(Command::ListForPush))
                } else if args.is_empty() {
                    Ok(Line::Valid(Command::List))
                } else {
                    Err(Error::InvalidCommand(line.to_owned()))
                }
            }
            "fetch" => {
                // fetch <oid> <name>
                // Use split_whitespace to handle multiple spaces between OID and Ref,
                // which is permitted.
                let mut parts = args.split_whitespace();
                let oid = parts
                    .next()
                    .ok_or_else(|| Error::InvalidCommand(line.to_owned()))?;
                let refstr = parts
                    .next()
                    .ok_or_else(|| Error::InvalidCommand(line.to_owned()))?;
                Ok(Line::Valid(Command::Fetch {
                    oid: oid.to_owned(),
                    refstr: refstr.to_owned(),
                }))
            }
            "push" => Ok(Line::Valid(Command::Push(args.to_owned()))),
            "option" => {
                // option <key> [value]
                // Use split_once to preserve whitespace in the value.
                let (key, val) = args.split_once(' ').unwrap_or((args, ""));
                let value = if val.is_empty() {
                    None
                } else {
                    Some(val.to_owned())
                };
                Ok(Line::Valid(Command::Option {
                    key: key.to_owned(),
                    value,
                }))
            }
            _ => Err(Error::InvalidCommand(line.to_owned())),
        }
    }
}

mod io {
    use std::io::{self, prelude::*};

    use super::*;

    pub(crate) struct LineReader<R: Read> {
        inner: io::BufReader<R>,
    }

    impl<R: Read> LineReader<R> {
        pub(crate) fn new(reader: R) -> Self {
            Self {
                inner: io::BufReader::new(reader),
            }
        }

        pub(crate) fn read_line(&mut self) -> io::Result<Result<Line, Error>> {
            let mut line = String::new();
            if self.inner.read_line(&mut line)? == 0 {
                // EOF reached
                return Ok(Ok(Line::Blank));
            }
            Ok(Command::parse_line(&line))
        }
    }

    impl<R: Read> Iterator for LineReader<R> {
        type Item = io::Result<Result<Line, Error>>;

        fn next(&mut self) -> Option<Self::Item> {
            match self.read_line() {
                Ok(line) => Some(Ok(line)),
                Err(e) => Some(Err(e)),
            }
        }
    }
}

pub(crate) use io::*;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_capabilities() {
        assert_eq!(
            Command::parse_line("capabilities").unwrap(),
            Line::Valid(Command::Capabilities)
        );
    }

    #[test]
    fn test_list() {
        assert_eq!(
            Command::parse_line("list").unwrap(),
            Line::Valid(Command::List)
        );
    }

    #[test]
    fn test_list_for_push() {
        assert_eq!(
            Command::parse_line("list for-push").unwrap(),
            Line::Valid(Command::ListForPush)
        );
    }

    #[test]
    fn test_fetch() {
        assert_eq!(
            Command::parse_line("fetch oid ref").unwrap(),
            Line::Valid(Command::Fetch {
                oid: "oid".to_owned(),
                refstr: "ref".to_owned()
            })
        );
    }

    #[test]
    fn test_fetch_whitespace() {
        assert_eq!(
            Command::parse_line("fetch   oid     ref").unwrap(),
            Line::Valid(Command::Fetch {
                oid: "oid".to_owned(),
                refstr: "ref".to_owned()
            })
        );
    }

    #[test]
    fn test_push() {
        assert_eq!(
            Command::parse_line("push src:dst").unwrap(),
            Line::Valid(Command::Push("src:dst".to_owned()))
        );
    }

    #[test]
    fn test_push_force() {
        assert_eq!(
            Command::parse_line("push +src:dst").unwrap(),
            Line::Valid(Command::Push("+src:dst".to_owned()))
        );
    }

    #[test]
    fn test_push_delete() {
        assert_eq!(
            Command::parse_line("push :dst").unwrap(),
            Line::Valid(Command::Push(":dst".to_owned()))
        );
    }

    #[test]
    fn test_option() {
        assert_eq!(
            Command::parse_line("option verbosity 2").unwrap(),
            Line::Valid(Command::Option {
                key: "verbosity".to_owned(),
                value: Some("2".to_owned())
            })
        );
    }

    #[test]
    fn test_option_whitespace_preservation() {
        assert_eq!(
            Command::parse_line("option patch.message Fix:  whitespace").unwrap(),
            Line::Valid(Command::Option {
                key: "patch.message".to_owned(),
                value: Some("Fix:  whitespace".to_owned())
            })
        );
    }

    #[test]
    fn test_empty() {
        assert_eq!(Command::parse_line("").unwrap(), Line::Blank);
        assert_eq!(Command::parse_line("   ").unwrap(), Line::Blank);
    }

    #[test]
    fn test_invalid() {
        assert!(Command::parse_line("invalid command").is_err());
        assert!(Command::parse_line("list invalid").is_err());
    }
}