diff --git a/src/issue.rs b/src/issue.rs index 0118e10..c206674 100644 --- a/src/issue.rs +++ b/src/issue.rs @@ -48,6 +48,10 @@ pub enum IssueError { ChronoParseError(#[from] chrono::format::ParseError), #[error("Failed to parse issue")] IssueParseError, + #[error("invalid escape character {escape:?} in tag file {filename:?}")] + TagInvalidEscape { escape: String, filename: String }, + #[error("invalid trailing escape character ',' in tag file {filename:?}")] + TagTrailingEscape { filename: String }, #[error("Failed to parse state")] StateParseError, #[error("Failed to run git")] @@ -212,23 +216,6 @@ impl Issue { Ok(dependencies) } - fn read_tags(tags_direntry: &std::fs::DirEntry) -> Result, IssueError> { - if !tags_direntry.metadata()?.is_dir() { - eprintln!("issue has old-style tags file"); - return Err(IssueError::StdIoError(std::io::Error::from( - std::io::ErrorKind::NotADirectory, - ))); - } - let mut tags = Vec::::new(); - for direntry in tags_direntry.path().read_dir()? { - if let Ok(direntry) = direntry { - tags.push(String::from(direntry.file_name().to_string_lossy())); - } - } - tags.sort(); - Ok(tags) - } - /// Add a new Comment to the Issue. Commits. pub fn add_comment( &mut self, @@ -536,6 +523,60 @@ impl Issue { Ok(()) } + fn read_tags(tags_direntry: &std::fs::DirEntry) -> Result, IssueError> { + if !tags_direntry.metadata()?.is_dir() { + eprintln!("issue has old-style tags file"); + return Err(IssueError::IssueParseError); + } + let mut tags = Vec::::new(); + for direntry in tags_direntry.path().read_dir()? { + if let Ok(direntry) = direntry { + let tag = Issue::tag_from_filename(&direntry.file_name().to_string_lossy())?; + tags.push(tag); + } + } + tags.sort(); + Ok(tags) + } + + /// Perform un-escape on a filename to make it into a tag: + /// ",0" => "," + /// ",1" => "/" + fn tag_from_filename(filename: &str) -> Result { + let mut tag = String::new(); + let mut token_iter = filename.split(','); + let Some(start) = token_iter.next() else { + return Err(IssueError::StdIoError(std::io::Error::from( + std::io::ErrorKind::NotFound, + ))); + }; + tag.push_str(start); + for token in token_iter { + match token.chars().nth(0) { + Some('0') => { + tag.push(','); + tag.push_str(&token[1..]); + } + Some('1') => { + tag.push('/'); + tag.push_str(&token[1..]); + } + Some(bogus) => { + return Err(IssueError::TagInvalidEscape { + escape: String::from(bogus), + filename: String::from(filename), + }); + } + None => { + return Err(IssueError::TagTrailingEscape { + filename: String::from(filename), + }); + } + } + } + Ok(tag) + } + fn commit_tags(&self, commit_message: &str) -> Result<(), IssueError> { let mut tags_filename = self.dir.clone(); tags_filename.push("tags"); @@ -562,6 +603,64 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn parse_tag_0() { + assert_eq!( + Issue::tag_from_filename("hello").unwrap(), + String::from("hello") + ); + } + + #[test] + fn parse_tag_1() { + assert_eq!( + Issue::tag_from_filename("hello,0world").unwrap(), + String::from("hello,world") + ); + } + + #[test] + fn parse_tag_2() { + assert_eq!( + Issue::tag_from_filename("hello,1world").unwrap(), + String::from("hello/world") + ); + } + + #[test] + fn parse_tag_3() { + assert_eq!( + Issue::tag_from_filename(",0hello,1world,0").unwrap(), + String::from(",hello/world,") + ); + } + + #[test] + fn parse_tag_4() { + // std::io::Error does not impl PartialEq :-( + let filename = "hello,"; + match Issue::tag_from_filename(filename) { + Ok(tag) => panic!( + "tag_from_filename() accepted invalid input {:?} and returned {:?}", + filename, tag + ), + Err(_e) => (), + } + } + + #[test] + fn parse_tag_5() { + // std::io::Error does not impl PartialEq :-( + let filename = "hello,world"; + match Issue::tag_from_filename(filename) { + Ok(tag) => panic!( + "tag_from_filename() accepted invalid input {:?} and returned {:?}", + filename, tag + ), + Err(_e) => (), + } + } + #[test] fn read_issue_0() { let issue_dir = std::path::Path::new("test/0000/3943fc5c173fdf41c0a22251593cd476/");