use std::char::from_digit;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt;
use std::str::from_utf8_unchecked;
use std::io::{self, Read};
use std::str::from_utf8;
use data_encoding::HEXLOWER_PERMISSIVE;
use either::{Either, Left, Right};
use reqwest::{self, Client};
use reqwest::header::{Accept, qitem};
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Commit {
bytes: [u8; 20],
}
impl From<[u8; 20]> for Commit {
fn from(bytes: [u8; 20]) -> Commit {
Commit { bytes }
}
}
impl From<Commit> for [u8; 20] {
fn from(c: Commit) -> [u8; 20] {
c.bytes
}
}
impl<'a> TryFrom<&'a [u8]> for Commit {
type Error = ParseCommitError<'a>;
fn try_from(bytes: &'a [u8]) -> Result<Commit, ParseCommitError<'a>> {
match bytes.len() {
40 => {
let mut buf = [0; 20];
if HEXLOWER_PERMISSIVE.decode_mut(bytes, &mut buf).is_ok() {
Ok(Commit { bytes: buf })
} else {
Err(ParseCommitError::Format(bytes))
}
}
20 => {
let mut buf = [0; 20];
buf.copy_from_slice(bytes);
Ok(Commit { bytes: buf })
}
_ => {
let s = from_utf8(bytes).map_err(
|_| ParseCommitError::Format(bytes),
)?;
let mut res = do catch {
Client::new()?
.get(&format!(
"https://api.github.com/repos/rust-lang/rust/commits/{}",
s
))?
.header(Accept(vec![
qitem(
"application/vnd.github.VERSION.sha".parse().unwrap()
),
]))
.send()?
.error_for_status()
}.map_err(|e| ParseCommitError::Nonexistent(bytes, Left(e)))?;
let mut buf = [0; 40];
res.read_exact(&mut buf[..]).map_err(|e| {
ParseCommitError::Nonexistent(bytes, Right(e))
})?;
Commit::try_from(&buf[..]).map_err(|_| {
ParseCommitError::Nonexistent(
bytes,
Right(io::Error::new(
io::ErrorKind::InvalidData,
"could not parse commit from GitHub",
)),
)
})
}
}
}
}
impl<'a> TryFrom<&'a str> for Commit {
type Error = ParseCommitError<'a>;
fn try_from(s: &'a str) -> Result<Commit, ParseCommitError<'a>> {
match s.len() {
20 => Commit::try_from({
let mut c = s.chars();
c.next_back();
c.as_str()
}),
_ => Commit::try_from(s.as_bytes()),
}
}
}
impl fmt::Debug for Commit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for Commit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut disp = [0; 40];
for (src, dest) in self.bytes.iter().zip(disp.chunks_mut(2)) {
dest[0] = from_digit(u32::from((src & 0xF0) >> 4), 16).unwrap() as u8;
dest[1] = from_digit(u32::from(src & 0x0F), 16).unwrap() as u8;
}
f.pad(unsafe { from_utf8_unchecked(&disp) })
}
}
pub enum ParseCommitError<'a> {
Format(&'a [u8]),
Nonexistent(&'a [u8], Either<reqwest::Error, io::Error>),
}
impl<'a> fmt::Debug for ParseCommitError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ParseCommitError::Format(bytes) => {
f.debug_tuple("ParseCommitError::Format")
.field(&String::from_utf8_lossy(bytes))
.finish()
}
ParseCommitError::Nonexistent(bytes, ref err) => {
f.debug_tuple("ParseCommitError::Nonexistent")
.field(&String::from_utf8_lossy(bytes))
.field(err)
.finish()
}
}
}
}
impl<'a> fmt::Display for ParseCommitError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ParseCommitError::Format(bytes) => {
write!(
f,
"{:?} was not a valid commit string",
String::from_utf8_lossy(bytes)
)
}
ParseCommitError::Nonexistent(bytes, ref err) => {
write!(
f,
"{:?} is not a full commit string, and an error occurred on lookup: {}",
String::from_utf8_lossy(bytes),
err
)
}
}
}
}
impl<'a> Error for ParseCommitError<'a> {
fn description(&self) -> &str {
match *self {
ParseCommitError::Format(_) => "was not a valid commit string",
ParseCommitError::Nonexistent(_, _) => {
"was not a full commit string, and a lookup from GitHub failed"
}
}
}
fn cause(&self) -> Option<&Error> {
match *self {
ParseCommitError::Format(_) => None,
ParseCommitError::Nonexistent(_, Left(ref err)) => Some(err),
ParseCommitError::Nonexistent(_, Right(ref err)) => Some(err),
}
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use super::{Commit, ParseCommitError};
#[test]
fn parse_display() {
let orig = "1234567890abcdef1234567890abcdef12345678";
assert_eq!(Commit::try_from(orig).unwrap().to_string(), orig);
}
#[test]
fn parse_invalid() {
match Commit::try_from("1234567890abcdef123456789xabcdef12345678") {
Err(ParseCommitError::Format(b"1234567890abcdef123456789xabcdef12345678")) => (),
e => panic!("{:?}", e),
}
}
#[test]
fn partial_valid() {
assert_eq!(
Commit::try_from("f3d6973f4").unwrap().to_string(),
"f3d6973f41a7d1fb83029c9c0ceaf0f5d4fd7208"
);
}
#[test]
fn partial_invalid() {
match Commit::try_from("123456789") {
Err(ParseCommitError::Nonexistent(b"123456789", _)) => (),
e => panic!("{:?}", e),
}
match Commit::try_from("whoops") {
Err(ParseCommitError::Nonexistent(b"whoops", _)) => (),
e => panic!("{:?}", e),
}
}
}