This commit is contained in:
Lina Cambridge 2018-08-03 21:53:21 -07:00
Коммит b4caed92c8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B875E59B3AACA8EE
9 изменённых файлов: 1555 добавлений и 0 удалений

7
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
/target
**/*.rs.bk
/target
**/*.rs.bk
Cargo.lock

12
.travis.yml Normal file
Просмотреть файл

@ -0,0 +1,12 @@
language: rust
rust:
- nightly
sudo: false
before_script:
- rustup component add rustfmt-preview
script:
- cargo fmt --all -- --check
- cargo build
- cargo test

10
Cargo.toml Normal file
Просмотреть файл

@ -0,0 +1,10 @@
[package]
name = "vellum"
version = "0.1.0"
authors = ["Lina Cambridge <lina@mozilla.com>"]
[dependencies]
failure = "0.1.1"
failure_derive = "0.1.1"
lazy_static = "1.0"
log = "0.4"

7
README.md Normal file
Просмотреть файл

@ -0,0 +1,7 @@
# Vellum
**Vellum** is a Rust port of the [bookmark merger](https://searchfox.org/mozilla-central/rev/e9d2dce0820fa2616174396459498bcb96ecf812/toolkit/components/places/SyncedBookmarksMirror.jsm) from Firefox Desktop.
## Requirements
* Rust 1.2.8 or higher

12
rustfmt.toml Normal file
Просмотреть файл

@ -0,0 +1,12 @@
indent_style = "Visual"
binop_separator = "Back"
condense_wildcard_suffixes = true
where_single_line = true
format_strings = true
imports_indent = "Visual"
merge_imports = true
match_block_trailing_comma = true
newline_style = "Unix"
reorder_impl_items = true
use_field_init_shorthand = true
wrap_comments = true

58
src/error.rs Normal file
Просмотреть файл

@ -0,0 +1,58 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::{fmt, result};
use failure::{Backtrace, Context, Fail};
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub struct Error(Context<ErrorKind>);
impl Fail for Error {
#[inline]
fn cause(&self) -> Option<&Fail> {
self.0.cause()
}
#[inline]
fn backtrace(&self) -> Option<&Backtrace> {
self.0.backtrace()
}
}
impl fmt::Display for Error {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl Error {
#[inline]
pub fn kind(&self) -> &ErrorKind {
&*self.0.get_context()
}
}
impl From<ErrorKind> for Error {
#[inline]
fn from(kind: ErrorKind) -> Error {
Error(Context::new(kind))
}
}
impl From<Context<ErrorKind>> for Error {
#[inline]
fn from(inner: Context<ErrorKind>) -> Error {
Error(inner)
}
}
#[derive(Debug, Fail)]
pub enum ErrorKind {
#[fail(display = "{}", _0)]
ConsistencyError(&'static str),
}

26
src/lib.rs Normal file
Просмотреть файл

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
mod error;
mod merge;
mod tree;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

1151
src/merge.rs Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

272
src/tree.rs Normal file
Просмотреть файл

@ -0,0 +1,272 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::{cmp::{Eq, PartialEq},
collections::HashSet,
fmt,
hash::{Hash, Hasher},
iter};
/// Synced item kinds. Each corresponds to a Sync record type.
#[derive(Eq, Hash, PartialEq)]
pub enum Kind {
Bookmark,
Query,
Folder,
Livemark,
Separator,
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
Kind::Bookmark => "Bookmark",
Kind::Query => "Query",
Kind::Folder => "Folder",
Kind::Livemark => "Livemark",
Kind::Separator => "Separator",
})
}
}
/// A complete, rooted tree with tombstones.
pub struct Tree {
pub deleted_guids: HashSet<String>,
}
impl Tree {
pub fn is_deleted(&self, guid: &str) -> bool {
false
}
pub fn node_for_guid(&self, guid: &str) -> Option<&Node> {
None
}
pub fn parent_node_for(&self, child_node: &Node) -> &Node {
unimplemented!();
}
pub fn guids(&self) -> impl Iterator<Item = &str> {
iter::empty::<&str>()
}
}
/// A node in a local or remote bookmark tree.
pub struct Node {
pub guid: String,
pub age: u64,
pub kind: Kind,
pub needs_merge: bool,
pub level: u64,
pub is_syncable: bool,
pub children: Vec<Box<Node>>,
}
impl Node {
#[inline]
pub fn is_folder(&self) -> bool {
self.kind == Kind::Folder
}
#[inline]
pub fn newer_than(&self, other: &Node) -> bool {
self.age < other.age
}
pub fn has_compatible_kind(&self, remote_node: &Node) -> bool {
match (&self.kind, &remote_node.kind) {
// Bookmarks and queries are interchangeable, as simply changing the URL
// can cause it to flip kinds.
(Kind::Bookmark, Kind::Query) => true,
(Kind::Query, Kind::Bookmark) => true,
// A local folder can become a livemark, as the remote may have synced
// as a folder before the annotation was added. However, we don't allow
// a local livemark to "downgrade" to a folder. See bug 632287.
(Kind::Folder, Kind::Livemark) => true,
(local_kind, remote_kind) => local_kind == remote_kind,
}
}
}
impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let info = if self.needs_merge {
format!("({}; Age = {}ms; Unmerged", self.kind, self.age)
} else {
format!("({}; Age = {}ms", self.kind, self.age)
};
write!(f, "{} ({})", self.guid, info)
}
}
/// A node in a merged bookmark tree. Holds the local node, remote node,
/// merged children, and a merge state indicating which side to prefer.
pub struct MergedNode<'t> {
pub guid: String,
pub local_node: Option<&'t Node>,
pub remote_node: Option<&'t Node>,
pub merge_state: MergeState,
pub merged_children: Vec<Box<MergedNode<'t>>>,
}
impl<'t> MergedNode<'t> {
pub fn new(guid: String,
local_node: Option<&'t Node>,
remote_node: Option<&'t Node>,
merge_state: MergeState)
-> MergedNode<'t>
{
MergedNode { guid,
local_node,
remote_node,
merge_state,
merged_children: Vec::new(), }
}
}
impl<'t> fmt::Display for MergedNode<'t> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}", self.guid, self.merge_state)
}
}
#[derive(Clone)]
pub enum ValueState {
Local,
Remote,
}
impl fmt::Display for ValueState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
ValueState::Local => "Value: Local",
ValueState::Remote => "Value: Remote",
})
}
}
#[derive(Clone)]
pub enum StructureState {
Local,
Remote,
New,
}
impl fmt::Display for StructureState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
StructureState::Local => "Structure: Local",
StructureState::Remote => "Structure: Remote",
StructureState::New => "Structure: New",
})
}
}
/// The merge state indicates which node we should prefer when reconciling
/// with Places. Recall that a merged node may point to a local node, remote
/// node, or both.
#[derive(Clone)]
pub struct MergeState(ValueState, StructureState);
impl MergeState {
/// A local merge state means no changes: we keep the local value and
/// structure state. This could mean that the item doesn't exist on the
/// server yet, or that it has newer local changes that we should
/// upload.
///
/// It's an error for a merged node to have a local merge state without a
/// local node. Deciding the value state for the merged node asserts
/// this.
pub fn local() -> MergeState {
MergeState(ValueState::Local, StructureState::Local)
}
/// A remote merge state means we should update Places with new value and
/// structure state from the mirror. The item might not exist locally yet,
/// or might have newer remote changes that we should apply.
///
/// As with local, a merged node can't have a remote merge state without a
/// remote node.
pub fn remote() -> MergeState {
MergeState(ValueState::Remote, StructureState::Remote)
}
/// Takes an existing value state, and a new structure state. We use the new
/// merge state to resolve conflicts caused by moving local items out of a
/// remotely deleted folder, or remote items out of a locally deleted
/// folder.
///
/// Applying a new merged node bumps its local change counter, so that the
/// merged structure is reuploaded to the server.
pub fn new(old: MergeState) -> MergeState {
MergeState(old.0, StructureState::New)
}
}
impl fmt::Display for MergeState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}; {}", self.0, self.1)
}
}
/// Content info for an item in the local or remote tree. This is used to dedupe
/// NEW local items to remote items that don't exist locally.
pub struct Content {
pub title: String,
pub url_href: String,
pub position: i64,
}
/// A lookup key for a node and its content. This is used to match nodes with
/// different GUIDs and similar content.
///
/// - Bookmarks must have the same title and URL.
/// - Queries must have the same title and query URL.
/// - Folders and livemarks must have the same title.
/// - Separators must have the same position within their parents.
pub struct ContentDupeKey<'t>(&'t Node, &'t Content);
impl<'t> ContentDupeKey<'t> {
pub fn new(node: &'t Node, content: &'t Content) -> ContentDupeKey<'t> {
ContentDupeKey(node, content)
}
}
impl<'t> PartialEq for ContentDupeKey<'t> {
fn eq(&self, other: &ContentDupeKey) -> bool {
if self.0.kind != other.0.kind {
false
} else {
match self.0.kind {
Kind::Bookmark | Kind::Query => {
self.1.title == other.1.title && self.1.url_href == other.1.url_href
},
Kind::Folder | Kind::Livemark => self.1.title == other.1.title,
Kind::Separator => self.1.position == other.1.position,
}
}
}
}
impl<'t> Eq for ContentDupeKey<'t> {}
impl<'t> Hash for ContentDupeKey<'t> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.kind.hash(state);
match self.0.kind {
Kind::Bookmark | Kind::Query => {
self.1.title.hash(state);
self.1.url_href.hash(state);
},
Kind::Folder | Kind::Livemark => {
self.1.title.hash(state);
},
Kind::Separator => {
self.1.position.hash(state);
},
}
}
}