2019-10-11 23:51:17 +03:00
|
|
|
/*
|
|
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
|
|
*
|
|
|
|
* This software may be used and distributed according to the terms of the
|
|
|
|
* GNU General Public License found in the LICENSE file in the root
|
|
|
|
* directory of this source tree.
|
|
|
|
*/
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::fs;
|
|
|
|
use std::io::{self, BufRead, BufReader, Read};
|
2017-10-10 17:25:36 +03:00
|
|
|
use std::path::PathBuf;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
2019-12-07 03:33:53 +03:00
|
|
|
use anyhow::{bail, Error, Result};
|
2017-07-28 04:00:19 +03:00
|
|
|
use ascii::AsciiStr;
|
2019-12-07 03:33:53 +03:00
|
|
|
use failure_ext::chain::ChainExt;
|
2017-11-27 18:56:51 +03:00
|
|
|
use futures::future;
|
2017-10-06 19:23:52 +03:00
|
|
|
use futures::stream::{self, Stream};
|
2017-11-27 18:56:51 +03:00
|
|
|
use futures_ext::{BoxFuture, BoxStream, StreamExt};
|
2019-11-15 09:02:57 +03:00
|
|
|
use thiserror::Error;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
2018-06-07 23:12:16 +03:00
|
|
|
use mercurial_types::HgChangesetId;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
2019-11-15 09:02:57 +03:00
|
|
|
#[derive(Debug, Error)]
|
2017-12-06 04:52:31 +03:00
|
|
|
pub enum ErrorKind {
|
2019-11-15 09:02:57 +03:00
|
|
|
#[error("invalid bookmarks line: {0}")]
|
2019-02-21 17:23:48 +03:00
|
|
|
InvalidBookmarkLine(String),
|
2019-11-15 09:02:57 +03:00
|
|
|
#[error("invalid hash: {0}")]
|
2019-02-21 17:23:48 +03:00
|
|
|
InvalidHash(String),
|
2017-12-06 04:52:31 +03:00
|
|
|
}
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
/// Implementation of bookmarks as they exist in stock Mercurial inside `.hg/bookmarks`.
|
|
|
|
/// The file has a list of entries:
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// <hash1> <bookmark1-name>
|
|
|
|
/// <hash2> <bookmark2-name>
|
|
|
|
/// ...
|
|
|
|
/// ```
|
|
|
|
///
|
2018-03-16 03:23:19 +03:00
|
|
|
/// Bookmark names are arbitrary bytestrings, and hashes are always HgChangesetIds.
|
2017-07-28 04:00:19 +03:00
|
|
|
///
|
|
|
|
/// This implementation is read-only -- implementing write support would require interacting with
|
|
|
|
/// the locking mechanism Mercurial uses, and generally seems like it wouldn't be very useful.
|
2017-11-27 18:56:51 +03:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct StockBookmarks {
|
2018-03-16 03:23:19 +03:00
|
|
|
bookmarks: HashMap<Vec<u8>, HgChangesetId>,
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
|
2017-11-27 18:56:51 +03:00
|
|
|
impl StockBookmarks {
|
2017-07-28 04:00:19 +03:00
|
|
|
pub fn read<P: Into<PathBuf>>(base: P) -> Result<Self> {
|
|
|
|
let base = base.into();
|
|
|
|
|
2019-11-27 22:34:50 +03:00
|
|
|
// Newer clients store bookmarks in storevfs.
|
|
|
|
let mut path = base.join("store/bookmarks");
|
|
|
|
if !path.exists() {
|
|
|
|
// Older clients store bookmarks in vfs.
|
|
|
|
path = base.join("bookmarks");
|
|
|
|
}
|
|
|
|
let file = fs::File::open(path);
|
2017-07-28 04:00:19 +03:00
|
|
|
match file {
|
|
|
|
Ok(file) => Self::from_reader(file),
|
|
|
|
Err(ref err) if err.kind() == io::ErrorKind::NotFound => {
|
|
|
|
// The .hg/bookmarks file is not guaranteed to exist. Treat it is empty if it
|
|
|
|
// doesn't.
|
2017-08-03 03:32:17 +03:00
|
|
|
Ok(StockBookmarks {
|
|
|
|
bookmarks: HashMap::new(),
|
|
|
|
})
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
Err(err) => Err(err.into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn from_reader<R: Read>(reader: R) -> Result<Self> {
|
|
|
|
let mut bookmarks = HashMap::new();
|
|
|
|
|
|
|
|
// Bookmark names might not be valid UTF-8, so use split() instead of lines().
|
|
|
|
for line in BufReader::new(reader).split(b'\n') {
|
|
|
|
let line = line?;
|
|
|
|
// <hash><space><bookmark name>, where hash is 40 bytes, the space is 1 byte
|
|
|
|
// and the bookmark name is at least 1 byte.
|
|
|
|
if line.len() < 42 || line[40] != b' ' {
|
2019-12-05 21:13:30 +03:00
|
|
|
bail!(ErrorKind::InvalidBookmarkLine(
|
2017-12-08 00:53:05 +03:00
|
|
|
String::from_utf8_lossy(line.as_ref()).into_owned(),
|
|
|
|
));
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
let bmname = &line[41..];
|
|
|
|
let hash_slice = &line[..40];
|
2018-09-07 00:22:36 +03:00
|
|
|
let hash = AsciiStr::from_ascii(&hash_slice).chain_err(ErrorKind::InvalidHash(
|
2017-12-06 04:52:31 +03:00
|
|
|
String::from_utf8_lossy(hash_slice).into_owned(),
|
|
|
|
))?;
|
2017-07-28 04:00:19 +03:00
|
|
|
bookmarks.insert(
|
|
|
|
bmname.into(),
|
2018-09-07 00:22:36 +03:00
|
|
|
HgChangesetId::from_ascii_str(hash).chain_err(ErrorKind::InvalidHash(
|
2017-12-06 04:52:31 +03:00
|
|
|
String::from_utf8_lossy(hash_slice).into_owned(),
|
|
|
|
))?,
|
2017-07-28 04:00:19 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-11-27 18:56:51 +03:00
|
|
|
Ok(StockBookmarks { bookmarks })
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
|
2019-05-23 21:57:45 +03:00
|
|
|
pub fn get(&self, name: &dyn AsRef<[u8]>) -> BoxFuture<Option<HgChangesetId>, Error> {
|
2017-07-28 04:00:19 +03:00
|
|
|
let value = match self.bookmarks.get(name.as_ref()) {
|
2018-12-05 16:56:00 +03:00
|
|
|
Some(hash) => Some(*hash),
|
2017-07-28 04:00:19 +03:00
|
|
|
None => None,
|
|
|
|
};
|
2017-11-27 18:56:51 +03:00
|
|
|
Box::new(future::result(Ok(value)))
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
|
2018-03-22 22:20:53 +03:00
|
|
|
pub fn keys(&self) -> BoxStream<Vec<u8>, Error> {
|
2017-07-28 04:00:19 +03:00
|
|
|
// collect forces evaluation early, so that the stream can safely outlive self
|
2017-10-06 19:23:52 +03:00
|
|
|
stream::iter_ok(
|
2017-07-28 04:00:19 +03:00
|
|
|
self.bookmarks
|
|
|
|
.keys()
|
|
|
|
.map(|k| Ok(k.to_vec()))
|
|
|
|
.collect::<Vec<_>>(),
|
2019-02-21 17:23:48 +03:00
|
|
|
)
|
|
|
|
.and_then(|x| x)
|
|
|
|
.boxify()
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
2019-05-23 21:57:45 +03:00
|
|
|
use assert_matches::assert_matches;
|
|
|
|
use failure_ext::{err_downcast, err_downcast_ref};
|
2017-07-28 04:00:19 +03:00
|
|
|
use futures::Future;
|
2018-03-22 22:20:53 +03:00
|
|
|
|
2018-06-07 23:12:16 +03:00
|
|
|
use mercurial_types_mocks::nodehash::*;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
fn assert_bookmark_get(
|
|
|
|
bookmarks: &StockBookmarks,
|
2019-05-23 21:57:45 +03:00
|
|
|
key: &dyn AsRef<[u8]>,
|
2018-03-16 03:23:19 +03:00
|
|
|
expected: Option<HgChangesetId>,
|
2017-07-28 04:00:19 +03:00
|
|
|
) {
|
|
|
|
let expected = match expected {
|
2018-12-05 16:56:00 +03:00
|
|
|
Some(hash) => Some(hash),
|
2017-07-28 04:00:19 +03:00
|
|
|
None => None,
|
|
|
|
};
|
|
|
|
assert_eq!(bookmarks.get(key).wait().unwrap(), expected);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse() {
|
|
|
|
let disk_bookmarks = b"\
|
|
|
|
1111111111111111111111111111111111111111 abc\n\
|
|
|
|
2222222222222222222222222222222222222222 def\n\
|
|
|
|
1111111111111111111111111111111111111111 test123\n";
|
|
|
|
let reader = Cursor::new(&disk_bookmarks[..]);
|
|
|
|
|
|
|
|
let bookmarks = StockBookmarks::from_reader(reader).unwrap();
|
2018-03-22 22:20:53 +03:00
|
|
|
assert_bookmark_get(&bookmarks, &"abc", Some(HgChangesetId::new(ONES_HASH)));
|
|
|
|
assert_bookmark_get(&bookmarks, &"def", Some(HgChangesetId::new(TWOS_HASH)));
|
|
|
|
assert_bookmark_get(&bookmarks, &"test123", Some(HgChangesetId::new(ONES_HASH)));
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// Bookmarks that aren't present
|
|
|
|
assert_bookmark_get(&bookmarks, &"abcdef", None);
|
|
|
|
|
|
|
|
// keys should return all the keys here
|
|
|
|
let mut list = bookmarks.keys().collect().wait().unwrap();
|
|
|
|
list.sort();
|
|
|
|
assert_eq!(list, vec![&b"abc"[..], &b"def"[..], &b"test123"[..]]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test a bunch of invalid bookmark lines
|
|
|
|
#[test]
|
|
|
|
fn test_invalid() {
|
|
|
|
let reader = Cursor::new(&b"111\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2018-09-07 00:22:31 +03:00
|
|
|
err_downcast!(bookmarks.unwrap_err(), e: ErrorKind => e).unwrap(),
|
2017-12-06 04:52:31 +03:00
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// no space or bookmark name
|
|
|
|
let reader = Cursor::new(&b"1111111111111111111111111111111111111111\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2018-09-07 00:22:31 +03:00
|
|
|
err_downcast!(bookmarks.unwrap_err(), e: ErrorKind => e).unwrap(),
|
2017-12-06 04:52:31 +03:00
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// no bookmark name
|
|
|
|
let reader = Cursor::new(&b"1111111111111111111111111111111111111111 \n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2018-09-07 00:22:31 +03:00
|
|
|
err_downcast!(bookmarks.unwrap_err(), e: ErrorKind => e).unwrap(),
|
2017-12-06 04:52:31 +03:00
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// no space after hash
|
|
|
|
let reader = Cursor::new(&b"1111111111111111111111111111111111111111ab\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2018-09-07 00:22:31 +03:00
|
|
|
err_downcast!(bookmarks.unwrap_err(), e: ErrorKind => e).unwrap(),
|
2017-12-06 04:52:31 +03:00
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// short hash
|
|
|
|
let reader = Cursor::new(&b"111111111111111111111111111111111111111 1ab\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-12-06 04:52:31 +03:00
|
|
|
let err = bookmarks.unwrap_err();
|
2018-09-07 00:22:31 +03:00
|
|
|
match err_downcast_ref!(err, err: ErrorKind => err) {
|
|
|
|
Some(ok @ ErrorKind::InvalidHash(..)) => println!("OK: {:?}", ok),
|
|
|
|
Some(bad) => panic!("other ErrorKind error: {:?}", bad),
|
|
|
|
None => panic!("other error: {:?}", err),
|
2017-12-06 04:52:31 +03:00
|
|
|
};
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// non-ASCII
|
|
|
|
let reader = Cursor::new(&b"111111111111111111111111111111111111111\xff test\n"[..]);
|
2017-12-06 04:52:31 +03:00
|
|
|
let err = StockBookmarks::from_reader(reader).unwrap_err();
|
2018-09-07 00:22:31 +03:00
|
|
|
match err_downcast_ref!(err, err: ErrorKind => err) {
|
|
|
|
Some(ok @ ErrorKind::InvalidHash(..)) => println!("OK: {:?}", ok),
|
|
|
|
Some(bad) => panic!("other ErrorKind error: {:?}", bad),
|
|
|
|
None => panic!("other error: {:?}", err),
|
2017-12-06 04:52:31 +03:00
|
|
|
};
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// not a valid hex string
|
|
|
|
let reader = Cursor::new(&b"abcdefgabcdefgabcdefgabcdefgabcdefgabcde test\n"[..]);
|
2017-12-06 04:52:31 +03:00
|
|
|
let err = StockBookmarks::from_reader(reader).unwrap_err();
|
2018-09-07 00:22:31 +03:00
|
|
|
match err_downcast_ref!(err, err: ErrorKind => err) {
|
|
|
|
Some(ok @ ErrorKind::InvalidHash(..)) => println!("OK: {:?}", ok),
|
|
|
|
Some(bad) => panic!("other ErrorKind error: {:?}", bad),
|
|
|
|
None => panic!("other error: {:?}", err),
|
2017-12-06 04:52:31 +03:00
|
|
|
};
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
}
|