mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
Bug 1540894 - Vendor Dogear v0.2.3. r=tcsc
Differential Revision: https://phabricator.services.mozilla.com/D26274 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
ec5fcb2581
commit
55eaa1eb23
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -357,7 +357,7 @@ name = "bookmark_sync"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"dogear 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"dogear 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"moz_task 0.1.0",
|
||||
@ -914,7 +914,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dogear"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -3597,7 +3597,7 @@ dependencies = [
|
||||
"checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c"
|
||||
"checksum dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "88972de891f6118092b643d85a0b28e0678e0f948d7f879aa32f2d5aafe97d2a"
|
||||
"checksum docopt 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d8acd393692c503b168471874953a2531df0e9ab77d0b6bbc582395743300a4a"
|
||||
"checksum dogear 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bcecbcd636b901efb0b61eea73972bda173c02c98a07fc66dd76e8ee1421ffbf"
|
||||
"checksum dogear 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d54506b6b209740d0a7a35ca5976db1ad2ed1aa168acc3561efc6a84fa95afe"
|
||||
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
||||
"checksum dtoa-short 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "068d4026697c1a18f0b0bb8cfcad1b0c151b90d8edb9bf4c235ad68128920d1d"
|
||||
"checksum dwrote 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c31c624339dab99c223a4b26c2e803b7c248adaca91549ce654c76f39a03f5c8"
|
||||
|
2
third_party/rust/dogear/.cargo-checksum.json
vendored
2
third_party/rust/dogear/.cargo-checksum.json
vendored
@ -1 +1 @@
|
||||
{"files":{"Cargo.toml":"f427f0dba2855a2e32ccaf3258e6517a90c2b69f5b7a9c34d8669d4a83fb84e7","LICENSE":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4","README.md":"303ea5ec53d4e86f2c321056e8158e31aa061353a99e52de3d76859d40919efc","src/driver.rs":"10ecce90c6dee4e7b0ecd87f6d4f10c3cb825b544c6413416167248755097ab2","src/error.rs":"c6e661a7b94119dc8770c482681e97e644507e37f0ba32c04cc3a0b43e7b0077","src/guid.rs":"0330e6e893a550e478c8ac678114ebc112add97cb1d5d803d65cda6588ce7ba5","src/lib.rs":"ef42d0d3b234ffb6e459550f36a5f9220a0dd5fd09867affc7f8f9fe0b5430f2","src/merge.rs":"2d94c9507725de7477d7dc4ca372c721e770d049f57ff4c14a2006e346231a40","src/store.rs":"612d90ea0614aa7cc943c4ac0faaee35c155f57b553195ac28518ae7c0b8ebb1","src/tests.rs":"e5a3a1b9b4cefda9b871348a739f2d66e05a940ad14fb72515cea373f9f3be8b","src/tree.rs":"17d5640e42dcbd979f4e5cc8c52c4b2b634f80596a3504baa40b2e6b55f213b8"},"package":"bcecbcd636b901efb0b61eea73972bda173c02c98a07fc66dd76e8ee1421ffbf"}
|
||||
{"files":{"CODE_OF_CONDUCT.md":"e85149c44f478f164f7d5f55f6e66c9b5ae236d4a11107d5e2a93fe71dd874b9","Cargo.toml":"9b2fd48cc14073599c7d3a50f74fe6aa9896a862c72651f69e06a1ec37e43c6d","LICENSE":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4","README.md":"303ea5ec53d4e86f2c321056e8158e31aa061353a99e52de3d76859d40919efc","src/driver.rs":"10ecce90c6dee4e7b0ecd87f6d4f10c3cb825b544c6413416167248755097ab2","src/error.rs":"c6e661a7b94119dc8770c482681e97e644507e37f0ba32c04cc3a0b43e7b0077","src/guid.rs":"0330e6e893a550e478c8ac678114ebc112add97cb1d5d803d65cda6588ce7ba5","src/lib.rs":"ef42d0d3b234ffb6e459550f36a5f9220a0dd5fd09867affc7f8f9fe0b5430f2","src/merge.rs":"460c6af8ba3b680d072528e9ee27ea62559911b1cce5a511abc3d698bc7b3da3","src/store.rs":"612d90ea0614aa7cc943c4ac0faaee35c155f57b553195ac28518ae7c0b8ebb1","src/tests.rs":"f341d811eb648e8482dd2eb108e110850453e7e0deeccc09610fa234332f3921","src/tree.rs":"b6ba6275716dff398ff81dfcbbc47fa3af59555e452d92b5cb035b73b88f733d"},"package":"6d54506b6b209740d0a7a35ca5976db1ad2ed1aa168acc3561efc6a84fa95afe"}
|
25
third_party/rust/dogear/CODE_OF_CONDUCT.md
vendored
Normal file
25
third_party/rust/dogear/CODE_OF_CONDUCT.md
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Community Participation Guidelines
|
||||
|
||||
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
|
||||
For more details, please read the
|
||||
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
|
||||
|
||||
## How to Report
|
||||
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
|
||||
|
||||
## Project Specific Etiquette
|
||||
|
||||
### Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
Project maintainers who do not follow or enforce Mozilla's Participation Guidelines in good
|
||||
faith may face temporary or permanent repercussions.
|
2
third_party/rust/dogear/Cargo.toml
vendored
2
third_party/rust/dogear/Cargo.toml
vendored
@ -13,7 +13,7 @@
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "dogear"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
authors = ["Lina Cambridge <lina@mozilla.com>"]
|
||||
exclude = ["/.travis/**", ".travis.yml"]
|
||||
description = "A library for merging bookmark trees."
|
||||
|
256
third_party/rust/dogear/src/merge.rs
vendored
256
third_party/rust/dogear/src/merge.rs
vendored
@ -35,7 +35,7 @@ enum StructureChange {
|
||||
}
|
||||
|
||||
/// Records structure change counters for telemetry.
|
||||
#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Default, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct StructureCounts {
|
||||
/// Remote non-folder change wins over local deletion.
|
||||
pub remote_revives: usize,
|
||||
@ -59,7 +59,7 @@ pub struct StructureCounts {
|
||||
type MatchingDupes<'t> = (HashMap<Guid, Node<'t>>, HashMap<Guid, Node<'t>>);
|
||||
|
||||
/// Represents an accepted local or remote deletion.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Deletion<'t> {
|
||||
pub guid: &'t Guid,
|
||||
pub local_level: i64,
|
||||
@ -886,7 +886,33 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
match (local_node.needs_merge, remote_node.needs_merge) {
|
||||
(true, true) => {
|
||||
// The item changed locally and remotely.
|
||||
let newer_side = if local_node.age < remote_node.age {
|
||||
let item = if local_node.is_user_content_root() {
|
||||
// For roots, we always prefer the local side for item
|
||||
// changes, like the title (bug 1432614).
|
||||
ConflictResolution::Local
|
||||
} else {
|
||||
// For other items, we check the validity to decide
|
||||
// which side to take.
|
||||
match remote_node.validity {
|
||||
Validity::Valid | Validity::Reupload => {
|
||||
// If the remote item is valid, or valid but needs
|
||||
// reupload, compare timestamps to decide which side is
|
||||
// newer.
|
||||
if local_node.age < remote_node.age {
|
||||
ConflictResolution::Local
|
||||
} else {
|
||||
ConflictResolution::Remote
|
||||
}
|
||||
}
|
||||
// If the remote item must be replaced, take the local
|
||||
// side. This _loses remote changes_, but we can't
|
||||
// apply those changes, anyway.
|
||||
Validity::Replace => ConflictResolution::Local,
|
||||
}
|
||||
};
|
||||
// For children, it's easier: we always use the newer side, even
|
||||
// if we're taking local changes for the item.
|
||||
let children = if local_node.age < remote_node.age {
|
||||
// The local change is newer, so merge local children first,
|
||||
// followed by remaining unmerged remote children.
|
||||
ConflictResolution::Local
|
||||
@ -895,16 +921,7 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
// children first, then remaining local children.
|
||||
ConflictResolution::Remote
|
||||
};
|
||||
if local_node.is_user_content_root() {
|
||||
// For roots, we always prefer the local side for item
|
||||
// changes, like the title (bug 1432614), but prefer the
|
||||
// newer side for children.
|
||||
(ConflictResolution::Local, newer_side)
|
||||
} else {
|
||||
// For all other items, we prefer the newer side for the
|
||||
// item and children.
|
||||
(newer_side, newer_side)
|
||||
}
|
||||
(item, children)
|
||||
}
|
||||
|
||||
(true, false) => {
|
||||
@ -916,15 +933,20 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
|
||||
(false, true) => {
|
||||
// The item changed remotely, but not locally.
|
||||
if local_node.is_user_content_root() {
|
||||
// For roots, we ignore remote item changes, but prefer
|
||||
// the remote side for children.
|
||||
(ConflictResolution::Unchanged, ConflictResolution::Remote)
|
||||
let item = if local_node.is_user_content_root() {
|
||||
// For roots, we ignore remote item changes.
|
||||
ConflictResolution::Unchanged
|
||||
} else {
|
||||
// For other items, we prefer the remote side for the item
|
||||
// and children.
|
||||
(ConflictResolution::Remote, ConflictResolution::Remote)
|
||||
}
|
||||
match remote_node.validity {
|
||||
Validity::Valid | Validity::Reupload => ConflictResolution::Remote,
|
||||
// And, for invalid remote items, we must reupload the
|
||||
// local side. This _loses remote changes_, but we can't
|
||||
// apply those changes, anyway.
|
||||
Validity::Replace => ConflictResolution::Local,
|
||||
}
|
||||
};
|
||||
// For children, we always use the remote side.
|
||||
(item, ConflictResolution::Remote)
|
||||
}
|
||||
|
||||
(false, false) => {
|
||||
@ -984,57 +1006,25 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
remote_parent_node: Node<'t>,
|
||||
remote_node: Node<'t>,
|
||||
) -> Result<StructureChange> {
|
||||
if remote_node.is_user_content_root() {
|
||||
if let Some(local_node) = self.local_tree.node_for_guid(&remote_node.guid) {
|
||||
let local_parent_node = local_node
|
||||
.parent()
|
||||
.expect("Can't check for structure changes without local parent");
|
||||
if remote_parent_node.guid != local_parent_node.guid {
|
||||
return Ok(StructureChange::Moved);
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
|
||||
if !remote_node_is_syncable(&remote_node) {
|
||||
// If the remote node is known to be non-syncable, we unconditionally
|
||||
// delete it from the server, even if it's syncable locally.
|
||||
self.delete_remotely.insert(remote_node.guid.clone());
|
||||
if remote_node.is_folder() {
|
||||
// If the remote node is a folder, we also need to walk its descendants
|
||||
// and reparent any syncable descendants, and descendants that only
|
||||
// exist remotely, to the merged node.
|
||||
self.relocate_remote_orphans_to_merged_node(merged_node, remote_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// delete it, even if it's syncable or moved locally.
|
||||
return self.delete_remote_node(merged_node, remote_node);
|
||||
}
|
||||
|
||||
if !self.local_tree.is_deleted(&remote_node.guid) {
|
||||
if let Some(local_node) = self.local_tree.node_for_guid(&remote_node.guid) {
|
||||
if !local_node.is_syncable() {
|
||||
// The remote node is syncable, but the local node is non-syncable.
|
||||
// For consistency with Desktop, we unconditionally delete the
|
||||
// node from the server.
|
||||
self.delete_remotely.insert(remote_node.guid.clone());
|
||||
if remote_node.is_folder() {
|
||||
self.relocate_remote_orphans_to_merged_node(merged_node, remote_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// The remote node is syncable, but the local node is
|
||||
// non-syncable. Unconditionally delete it.
|
||||
return self.delete_remote_node(merged_node, remote_node);
|
||||
}
|
||||
if local_node.validity == Validity::Replace
|
||||
&& remote_node.validity == Validity::Replace
|
||||
{
|
||||
// The nodes are invalid on both sides, so we can't apply or reupload
|
||||
// a valid copy. Delete the item from the server.
|
||||
self.delete_remotely.insert(remote_node.guid.clone());
|
||||
if remote_node.is_folder() {
|
||||
self.relocate_remote_orphans_to_merged_node(merged_node, remote_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// The nodes are invalid on both sides, so we can't apply
|
||||
// or reupload a valid copy. Delete it.
|
||||
return self.delete_remote_node(merged_node, remote_node);
|
||||
}
|
||||
let local_parent_node = local_node
|
||||
.parent()
|
||||
@ -1043,20 +1033,24 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
return Ok(StructureChange::Moved);
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
} else {
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
if remote_node.validity == Validity::Replace {
|
||||
// The remote node is invalid and doesn't exist locally, so we
|
||||
// can't reupload a valid copy. We must delete it.
|
||||
return self.delete_remote_node(merged_node, remote_node);
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
|
||||
if remote_node.validity == Validity::Replace {
|
||||
// If the remote node is invalid, and deleted locally, unconditionally
|
||||
// delete the item from the server.
|
||||
self.delete_remotely.insert(remote_node.guid.clone());
|
||||
if remote_node.is_folder() {
|
||||
self.relocate_remote_orphans_to_merged_node(merged_node, remote_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// The remote node is invalid and deleted locally, so we can't
|
||||
// reupload a valid copy. Delete it.
|
||||
return self.delete_remote_node(merged_node, remote_node);
|
||||
}
|
||||
|
||||
if remote_node.is_user_content_root() {
|
||||
// If the remote node is a content root, don't delete it locally.
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
|
||||
if remote_node.needs_merge {
|
||||
@ -1094,12 +1088,7 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
|
||||
// Take the local deletion and relocate any new remote descendants to the
|
||||
// merged node.
|
||||
self.delete_remotely.insert(remote_node.guid.clone());
|
||||
if remote_node.is_folder() {
|
||||
self.relocate_remote_orphans_to_merged_node(merged_node, remote_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
Ok(StructureChange::Deleted)
|
||||
self.delete_remote_node(merged_node, remote_node)
|
||||
}
|
||||
|
||||
/// Checks if a local node is remotely moved or deleted, and reparents any
|
||||
@ -1113,55 +1102,28 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
local_parent_node: Node<'t>,
|
||||
local_node: Node<'t>,
|
||||
) -> Result<StructureChange> {
|
||||
if local_node.is_user_content_root() {
|
||||
if let Some(remote_node) = self.remote_tree.node_for_guid(&local_node.guid) {
|
||||
let remote_parent_node = remote_node
|
||||
.parent()
|
||||
.expect("Can't check for structure changes without remote parent");
|
||||
if remote_parent_node.guid != local_parent_node.guid {
|
||||
return Ok(StructureChange::Moved);
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
|
||||
if !local_node.is_syncable() {
|
||||
// If the local node is known to be non-syncable, we unconditionally
|
||||
// delete it from the local tree, even if it's syncable remotely.
|
||||
self.delete_locally.insert(local_node.guid.clone());
|
||||
if local_node.is_folder() {
|
||||
self.relocate_local_orphans_to_merged_node(merged_node, local_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// delete it, even if it's syncable or moved remotely.
|
||||
return self.delete_local_node(merged_node, local_node);
|
||||
}
|
||||
|
||||
if !self.remote_tree.is_deleted(&local_node.guid) {
|
||||
if let Some(remote_node) = self.remote_tree.node_for_guid(&local_node.guid) {
|
||||
if !remote_node_is_syncable(&remote_node) {
|
||||
// The local node is syncable, but the remote node is non-syncable.
|
||||
// This can happen if we applied an orphaned left pane query in a
|
||||
// previous sync, and later saw the left pane root on the server.
|
||||
// Since we now have the complete subtree, we can remove the item.
|
||||
self.delete_locally.insert(local_node.guid.clone());
|
||||
if remote_node.is_folder() {
|
||||
self.relocate_local_orphans_to_merged_node(merged_node, local_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// The local node is syncable, but the remote node is not.
|
||||
// This can happen if we applied an orphaned left pane
|
||||
// query in a previous sync, and later saw the left pane
|
||||
// root on the server. Since we now have the complete
|
||||
// subtree, we can remove it.
|
||||
return self.delete_local_node(merged_node, local_node);
|
||||
}
|
||||
if remote_node.validity == Validity::Replace
|
||||
&& local_node.validity == Validity::Replace
|
||||
{
|
||||
// The nodes are invalid on both sides, so we can't apply or reupload
|
||||
// a valid copy. Delete the item from Places.
|
||||
self.delete_locally.insert(local_node.guid.clone());
|
||||
if local_node.is_folder() {
|
||||
self.relocate_local_orphans_to_merged_node(merged_node, local_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// The nodes are invalid on both sides, so we can't replace
|
||||
// the local copy with a remote one. Delete it.
|
||||
return self.delete_local_node(merged_node, local_node);
|
||||
}
|
||||
// Otherwise, either both nodes are valid; or the remote node
|
||||
// is invalid but the local node is valid, so we can reupload a
|
||||
@ -1174,20 +1136,27 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
if local_node.validity == Validity::Replace {
|
||||
// The local node is invalid and doesn't exist remotely, so
|
||||
// we can't replace the local copy. Delete it.
|
||||
return self.delete_local_node(merged_node, local_node);
|
||||
}
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
|
||||
if local_node.validity == Validity::Replace {
|
||||
// If the local node is invalid, and deleted remotely, unconditionally
|
||||
// delete the item from Places.
|
||||
self.delete_locally.insert(local_node.guid.clone());
|
||||
if local_node.is_folder() {
|
||||
self.relocate_local_orphans_to_merged_node(merged_node, local_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
return Ok(StructureChange::Deleted);
|
||||
// The local node is invalid and deleted remotely, so we can't
|
||||
// replace the local copy. Delete it.
|
||||
return self.delete_local_node(merged_node, local_node);
|
||||
}
|
||||
|
||||
if local_node.is_user_content_root() {
|
||||
// If the local node is a content root, don't delete it remotely.
|
||||
return Ok(StructureChange::Unchanged);
|
||||
}
|
||||
|
||||
// See `check_for_local_structure_change_of_remote_node` for an
|
||||
// explanation of how we decide to take or ignore a deletion.
|
||||
if local_node.needs_merge {
|
||||
if !local_node.is_folder() {
|
||||
trace!(
|
||||
@ -1214,26 +1183,21 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
|
||||
// Take the remote deletion and relocate any new local descendants to the
|
||||
// merged node.
|
||||
self.delete_locally.insert(local_node.guid.clone());
|
||||
if local_node.is_folder() {
|
||||
self.relocate_local_orphans_to_merged_node(merged_node, local_node)?;
|
||||
}
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
Ok(StructureChange::Deleted)
|
||||
self.delete_local_node(merged_node, local_node)
|
||||
}
|
||||
|
||||
/// Takes a local deletion for a remote node by marking the node as deleted,
|
||||
/// and relocating all remote descendants that aren't also locally deleted
|
||||
/// to the closest surviving ancestor. We do this to avoid data loss if
|
||||
/// the user adds a bookmark to a folder on another device, and deletes
|
||||
/// that folder locally.
|
||||
/// Marks a remote node as deleted, and relocates all remote descendants
|
||||
/// that aren't also locally deleted to the merged node. This avoids data
|
||||
/// loss if the user adds a bookmark to a folder on another device, and
|
||||
/// deletes that folder locally.
|
||||
///
|
||||
/// This is the inverse of `relocate_local_orphans_to_merged_node`.
|
||||
fn relocate_remote_orphans_to_merged_node(
|
||||
/// This is the inverse of `delete_local_node`.
|
||||
fn delete_remote_node(
|
||||
&mut self,
|
||||
merged_node: &mut MergedNode<'t>,
|
||||
remote_node: Node<'t>,
|
||||
) -> Result<()> {
|
||||
) -> Result<StructureChange> {
|
||||
self.delete_remotely.insert(remote_node.guid.clone());
|
||||
for remote_child_node in remote_node.children() {
|
||||
if self.merged_guids.contains(&remote_child_node.guid) {
|
||||
trace!(
|
||||
@ -1277,19 +1241,20 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
Ok(StructureChange::Deleted)
|
||||
}
|
||||
|
||||
/// Takes a remote deletion for a local node by marking the node as deleted,
|
||||
/// and relocating all local descendants that aren't also remotely deleted
|
||||
/// to the closest surviving ancestor.
|
||||
/// Marks a local node as deleted, and relocates all local descendants
|
||||
/// that aren't also remotely deleted to the merged node.
|
||||
///
|
||||
/// This is the inverse of `relocate_remote_orphans_to_merged_node`.
|
||||
fn relocate_local_orphans_to_merged_node(
|
||||
/// This is the inverse of `delete_remote_node`.
|
||||
fn delete_local_node(
|
||||
&mut self,
|
||||
merged_node: &mut MergedNode<'t>,
|
||||
local_node: Node<'t>,
|
||||
) -> Result<()> {
|
||||
) -> Result<StructureChange> {
|
||||
self.delete_locally.insert(local_node.guid.clone());
|
||||
for local_child_node in local_node.children() {
|
||||
if self.merged_guids.contains(&local_child_node.guid) {
|
||||
trace!(
|
||||
@ -1333,7 +1298,8 @@ impl<'t, D: Driver> Merger<'t, D> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
self.structure_counts.merged_deletions += 1;
|
||||
Ok(StructureChange::Deleted)
|
||||
}
|
||||
|
||||
/// Finds all children of a local folder with similar content as children of
|
||||
|
178
third_party/rust/dogear/src/tests.rs
vendored
178
third_party/rust/dogear/src/tests.rs
vendored
@ -20,7 +20,10 @@ use crate::driver::Driver;
|
||||
use crate::error::{ErrorKind, Result};
|
||||
use crate::guid::{Guid, ROOT_GUID, UNFILED_GUID};
|
||||
use crate::merge::{Merger, StructureCounts};
|
||||
use crate::tree::{Builder, Content, IntoTree, Item, Kind, Tree, Validity};
|
||||
use crate::tree::{
|
||||
Builder, Content, DivergedParent, DivergedParentGuid, IntoTree, Item, Kind, Problem, Problems,
|
||||
Tree, Validity,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
@ -2567,3 +2570,176 @@ fn cycle() {
|
||||
err => assert!(false, "Wrong error kind for cycle: {:?}", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reupload_replace() {
|
||||
before_each();
|
||||
|
||||
let mut local_tree = nodes!({
|
||||
("menu________", Folder, {
|
||||
("bookmarkAAAA", Bookmark)
|
||||
}),
|
||||
("toolbar_____", Folder, {
|
||||
("folderBBBBBB", Folder, {
|
||||
("bookmarkCCCC", Bookmark[validity = Validity::Replace])
|
||||
}),
|
||||
("folderDDDDDD", Folder, {
|
||||
("bookmarkEEEE", Bookmark[validity = Validity::Replace])
|
||||
})
|
||||
}),
|
||||
("unfiled_____", Folder),
|
||||
("mobile______", Folder, {
|
||||
("bookmarkFFFF", Bookmark[validity = Validity::Replace]),
|
||||
("folderGGGGGG", Folder),
|
||||
("bookmarkHHHH", Bookmark[validity = Validity::Replace])
|
||||
})
|
||||
})
|
||||
.into_tree()
|
||||
.unwrap();
|
||||
local_tree.note_deleted("bookmarkIIII".into());
|
||||
|
||||
let mut remote_tree = nodes!({
|
||||
("menu________", Folder, {
|
||||
("bookmarkAAAA", Bookmark[validity = Validity::Replace])
|
||||
}),
|
||||
("toolbar_____", Folder, {
|
||||
("bookmarkJJJJ", Bookmark[validity = Validity::Replace]),
|
||||
("folderBBBBBB", Folder, {
|
||||
("bookmarkCCCC", Bookmark[validity = Validity::Replace])
|
||||
}),
|
||||
("folderDDDDDD", Folder)
|
||||
}),
|
||||
("unfiled_____", Folder, {
|
||||
("bookmarkKKKK", Bookmark[validity = Validity::Reupload])
|
||||
}),
|
||||
("mobile______", Folder, {
|
||||
("bookmarkFFFF", Bookmark),
|
||||
("folderGGGGGG", Folder, {
|
||||
("bookmarkIIII", Bookmark[validity = Validity::Replace])
|
||||
})
|
||||
})
|
||||
})
|
||||
.into_tree()
|
||||
.unwrap();
|
||||
remote_tree.note_deleted("bookmarkEEEE".into());
|
||||
|
||||
let mut merger = Merger::new(&local_tree, &remote_tree);
|
||||
let merged_root = merger.merge().unwrap();
|
||||
assert!(merger.subsumes(&local_tree));
|
||||
assert!(merger.subsumes(&remote_tree));
|
||||
|
||||
let expected_tree = nodes!({
|
||||
("menu________", Folder, {
|
||||
// A is invalid remotely and valid locally, so replace.
|
||||
("bookmarkAAAA", Bookmark[needs_merge = true])
|
||||
}),
|
||||
// Toolbar has new children.
|
||||
("toolbar_____", Folder[needs_merge = true], {
|
||||
// B has new children.
|
||||
("folderBBBBBB", Folder[needs_merge = true]),
|
||||
("folderDDDDDD", Folder)
|
||||
}),
|
||||
("unfiled_____", Folder, {
|
||||
// K was flagged for reupload.
|
||||
("bookmarkKKKK", Bookmark[needs_merge = true])
|
||||
}),
|
||||
("mobile______", Folder, {
|
||||
// F is invalid locally, so replace with remote. This isn't
|
||||
// possible in Firefox Desktop or Rust Places, where the local
|
||||
// tree is always valid, but we handle it for symmetry.
|
||||
("bookmarkFFFF", Bookmark),
|
||||
("folderGGGGGG", Folder[needs_merge = true])
|
||||
})
|
||||
})
|
||||
.into_tree()
|
||||
.unwrap();
|
||||
let expected_deletions = vec![
|
||||
// C is invalid on both sides, so we need to upload a tombstone.
|
||||
("bookmarkCCCC", true),
|
||||
// E is invalid locally and deleted remotely, so doesn't need a
|
||||
// tombstone.
|
||||
("bookmarkEEEE", false),
|
||||
// H is invalid locally and doesn't exist remotely, so doesn't need a
|
||||
// tombstone.
|
||||
("bookmarkHHHH", false),
|
||||
// I is deleted locally and invalid remotely, so needs a tombstone.
|
||||
("bookmarkIIII", true),
|
||||
// J doesn't exist locally and invalid remotely, so needs a tombstone.
|
||||
("bookmarkJJJJ", true),
|
||||
];
|
||||
let expected_telem = StructureCounts {
|
||||
merged_nodes: 10,
|
||||
// C is double-counted: it's deleted on both sides, so
|
||||
// `merged_deletions` is 6, even though we only have 5 expected
|
||||
// deletions.
|
||||
merged_deletions: 6,
|
||||
..StructureCounts::default()
|
||||
};
|
||||
|
||||
let merged_tree = merged_root.into_tree().unwrap();
|
||||
assert_eq!(merged_tree, expected_tree);
|
||||
|
||||
let mut deletions = merger
|
||||
.deletions()
|
||||
.map(|d| (d.guid.as_ref(), d.should_upload_tombstone))
|
||||
.collect::<Vec<(&str, bool)>>();
|
||||
deletions.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
assert_eq!(deletions, expected_deletions);
|
||||
|
||||
assert_eq!(merger.counts(), &expected_telem);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn problems() {
|
||||
let mut problems = Problems::default();
|
||||
|
||||
problems
|
||||
.note(&"bookmarkAAAA".into(), Problem::Orphan)
|
||||
.note(
|
||||
&"menu________".into(),
|
||||
Problem::MisparentedRoot(vec![DivergedParent::ByChildren("unfiled_____".into())]),
|
||||
)
|
||||
.note(&"toolbar_____".into(), Problem::MisparentedRoot(Vec::new()))
|
||||
.note(
|
||||
&"bookmarkBBBB".into(),
|
||||
Problem::DivergedParents(vec![
|
||||
DivergedParent::ByChildren("folderCCCCCC".into()),
|
||||
DivergedParentGuid::Folder("folderDDDDDD".into()).into(),
|
||||
]),
|
||||
)
|
||||
.note(
|
||||
&"bookmarkEEEE".into(),
|
||||
Problem::DivergedParents(vec![
|
||||
DivergedParent::ByChildren("folderFFFFFF".into()),
|
||||
DivergedParentGuid::NonFolder("bookmarkGGGG".into()).into(),
|
||||
]),
|
||||
)
|
||||
.note(
|
||||
&"bookmarkHHHH".into(),
|
||||
Problem::DivergedParents(vec![
|
||||
DivergedParent::ByChildren("folderIIIIII".into()),
|
||||
DivergedParent::ByChildren("folderJJJJJJ".into()),
|
||||
DivergedParentGuid::Missing("folderKKKKKK".into()).into(),
|
||||
]),
|
||||
)
|
||||
.note(&"bookmarkLLLL".into(), Problem::DivergedParents(Vec::new()));
|
||||
|
||||
let mut summary = problems.summarize().collect::<Vec<_>>();
|
||||
summary.sort_by(|a, b| a.guid().cmp(b.guid()));
|
||||
assert_eq!(
|
||||
summary
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
&[
|
||||
"bookmarkAAAA is an orphan",
|
||||
"bookmarkBBBB is in children of folderCCCCCC and has parent folderDDDDDD",
|
||||
"bookmarkEEEE is in children of folderFFFFFF and has non-folder parent bookmarkGGGG",
|
||||
"bookmarkHHHH is in children of folderIIIIII, is in children of folderJJJJJJ, and has \
|
||||
nonexistent parent folderKKKKKK",
|
||||
"bookmarkLLLL has diverged parents",
|
||||
"menu________ is a user content root, but is in children of unfiled_____",
|
||||
"toolbar_____ is a user content root",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
880
third_party/rust/dogear/src/tree.rs
vendored
880
third_party/rust/dogear/src/tree.rs
vendored
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt, mem,
|
||||
@ -46,6 +47,7 @@ pub struct Tree {
|
||||
entry_index_by_guid: HashMap<Guid, Index>,
|
||||
entries: Vec<TreeEntry>,
|
||||
deleted_guids: HashSet<Guid>,
|
||||
problems: Problems,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
@ -108,6 +110,12 @@ impl Tree {
|
||||
.get(guid)
|
||||
.map(|&index| Node(self, &self.entries[index]))
|
||||
}
|
||||
|
||||
/// Returns the structure divergences found when building the tree.
|
||||
#[inline]
|
||||
pub fn problems(&self) -> &Problems {
|
||||
&self.problems
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTree for Tree {
|
||||
@ -120,20 +128,26 @@ impl IntoTree for Tree {
|
||||
impl fmt::Display for Tree {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let root = self.root();
|
||||
let deleted_guids = self
|
||||
.deleted_guids
|
||||
.iter()
|
||||
.map(|guid| guid.as_ref())
|
||||
.collect::<Vec<&str>>();
|
||||
match deleted_guids.len() {
|
||||
0 => write!(f, "{}", root.to_ascii_string()),
|
||||
_ => write!(
|
||||
f,
|
||||
"{}\nDeleted: [{}]",
|
||||
root.to_ascii_string(),
|
||||
deleted_guids.join(",")
|
||||
),
|
||||
f.write_str(&root.to_ascii_string())?;
|
||||
if !self.deleted_guids.is_empty() {
|
||||
f.write_str("\nDeleted: [")?;
|
||||
for (i, guid) in self.deleted_guids.iter().enumerate() {
|
||||
if i != 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
f.write_str(guid.as_ref())?;
|
||||
}
|
||||
}
|
||||
if !self.problems.is_empty() {
|
||||
f.write_str("\nProblems:\n")?;
|
||||
for (i, summary) in self.problems.summarize().enumerate() {
|
||||
if i != 0 {
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
write!(f, "❗️ {}", summary)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,179 +287,6 @@ impl Builder {
|
||||
};
|
||||
ParentBuilder(self, entry_child)
|
||||
}
|
||||
|
||||
/// Returns the index of the default parent entry for reparented orphans.
|
||||
/// This is either the default folder (rule 4), or the root, if the
|
||||
/// default folder isn't set, doesn't exist, or isn't a folder (rule 5).
|
||||
fn reparent_orphans_to_default_index(&self) -> Index {
|
||||
self.reparent_orphans_to
|
||||
.as_ref()
|
||||
.and_then(|guid| self.entry_index_by_guid.get(guid))
|
||||
.cloned()
|
||||
.filter(|&parent_index| {
|
||||
let parent_entry = &self.entries[parent_index];
|
||||
parent_entry.item.is_folder()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Resolves parents for all entries. Returns a vector of resolved parents
|
||||
/// by the entry index, and a lookup table for reparented orphans.
|
||||
fn resolve(&self) -> (Vec<ResolvedParent>, HashMap<Index, Vec<Index>>) {
|
||||
let mut parents = Vec::with_capacity(self.entries.len());
|
||||
let mut reparented_orphans_by_parent: HashMap<Index, Vec<Index>> = HashMap::new();
|
||||
for (entry_index, entry) in self.entries.iter().enumerate() {
|
||||
let mut resolved_parent = match &entry.parent {
|
||||
BuilderEntryParent::Root => ResolvedParent::Root,
|
||||
BuilderEntryParent::None => {
|
||||
// The item doesn't have a `parentid` _or_ `children`.
|
||||
// Reparent to the default folder.
|
||||
let parent_index = self.reparent_orphans_to_default_index();
|
||||
ResolvedParent::ByParentGuid(parent_index)
|
||||
}
|
||||
BuilderEntryParent::Complete(index) => {
|
||||
// The item has a complete structure. This is the fast path
|
||||
// for local trees.
|
||||
ResolvedParent::Unchanged(*index)
|
||||
}
|
||||
BuilderEntryParent::Partial(parents) => match parents.as_slice() {
|
||||
[BuilderParentBy::UnknownItem(by_item), BuilderParentBy::Children(by_children)]
|
||||
| [BuilderParentBy::Children(by_children), BuilderParentBy::UnknownItem(by_item)] =>
|
||||
{
|
||||
self.entry_index_by_guid
|
||||
.get(by_item)
|
||||
.filter(|by_item| by_item == &by_children)
|
||||
.map(|&by_item| {
|
||||
// The partial structure is actually complete.
|
||||
// This is the "fast slow path" for remote
|
||||
// trees, because we add their structure in
|
||||
// two passes.
|
||||
ResolvedParent::Unchanged(by_item)
|
||||
})
|
||||
.unwrap_or_else(|| ResolvedParent::ByChildren(*by_children))
|
||||
}
|
||||
|
||||
parents => {
|
||||
// For items with zero, one, or more than two parents, we pick
|
||||
// the newest (minimum age), preferring parents from `children`
|
||||
// over `parentid` (rules 2-3).
|
||||
parents
|
||||
.iter()
|
||||
.min_by(|parent, other_parent| {
|
||||
let (parent_index, other_parent_index) =
|
||||
match (parent, other_parent) {
|
||||
(
|
||||
BuilderParentBy::Children(parent_index),
|
||||
BuilderParentBy::Children(other_parent_index),
|
||||
) => (*parent_index, *other_parent_index),
|
||||
(
|
||||
BuilderParentBy::Children(_),
|
||||
BuilderParentBy::KnownItem(_),
|
||||
) => {
|
||||
return Ordering::Less;
|
||||
}
|
||||
(
|
||||
BuilderParentBy::Children(_),
|
||||
BuilderParentBy::UnknownItem(_),
|
||||
) => {
|
||||
return Ordering::Less;
|
||||
}
|
||||
|
||||
(
|
||||
BuilderParentBy::KnownItem(parent_index),
|
||||
BuilderParentBy::KnownItem(other_parent_index),
|
||||
) => (*parent_index, *other_parent_index),
|
||||
(
|
||||
BuilderParentBy::KnownItem(_),
|
||||
BuilderParentBy::Children(_),
|
||||
) => {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
(
|
||||
BuilderParentBy::KnownItem(_),
|
||||
BuilderParentBy::UnknownItem(_),
|
||||
) => {
|
||||
return Ordering::Less;
|
||||
}
|
||||
|
||||
(
|
||||
BuilderParentBy::UnknownItem(parent_guid),
|
||||
BuilderParentBy::UnknownItem(other_parent_guid),
|
||||
) => {
|
||||
match (
|
||||
self.entry_index_by_guid.get(parent_guid),
|
||||
self.entry_index_by_guid.get(other_parent_guid),
|
||||
) {
|
||||
(Some(parent_index), Some(other_parent_index)) => {
|
||||
(*parent_index, *other_parent_index)
|
||||
}
|
||||
(Some(_), None) => return Ordering::Less,
|
||||
(None, Some(_)) => return Ordering::Greater,
|
||||
(None, None) => return Ordering::Equal,
|
||||
}
|
||||
}
|
||||
(
|
||||
BuilderParentBy::UnknownItem(_),
|
||||
BuilderParentBy::Children(_),
|
||||
) => {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
(
|
||||
BuilderParentBy::UnknownItem(_),
|
||||
BuilderParentBy::KnownItem(_),
|
||||
) => {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
};
|
||||
let parent_entry = &self.entries[parent_index];
|
||||
let other_parent_entry = &self.entries[other_parent_index];
|
||||
parent_entry.item.age.cmp(&other_parent_entry.item.age)
|
||||
})
|
||||
.and_then(|parent_from| match parent_from {
|
||||
BuilderParentBy::Children(index) => {
|
||||
Some(ResolvedParent::ByChildren(*index))
|
||||
}
|
||||
BuilderParentBy::KnownItem(index) => {
|
||||
Some(ResolvedParent::ByParentGuid(*index))
|
||||
}
|
||||
BuilderParentBy::UnknownItem(guid) => self
|
||||
.entry_index_by_guid
|
||||
.get(guid)
|
||||
.filter(|&&index| self.entries[index].item.is_folder())
|
||||
.map(|&index| ResolvedParent::ByParentGuid(index)),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// Fall back to the default folder (rule 4) or root
|
||||
// (rule 5) if we didn't find a parent.
|
||||
let parent_index = self.reparent_orphans_to_default_index();
|
||||
ResolvedParent::ByParentGuid(parent_index)
|
||||
})
|
||||
}
|
||||
},
|
||||
};
|
||||
if entry.item.guid.is_user_content_root() {
|
||||
// ...But user content roots should always be in the Places
|
||||
// root (rule 1).
|
||||
resolved_parent = match resolved_parent {
|
||||
ResolvedParent::Unchanged(parent_index) if parent_index == 0 => {
|
||||
ResolvedParent::Unchanged(parent_index)
|
||||
}
|
||||
_ => ResolvedParent::ByParentGuid(0),
|
||||
};
|
||||
}
|
||||
if let ResolvedParent::ByParentGuid(parent_index) = &resolved_parent {
|
||||
// Reparented orphans are special: since we don't know their positions,
|
||||
// we want to move them to the end of their chosen parents, after any
|
||||
// `children` (rules 3-4).
|
||||
let reparented_orphans = reparented_orphans_by_parent
|
||||
.entry(*parent_index)
|
||||
.or_default();
|
||||
reparented_orphans.push(entry_index);
|
||||
}
|
||||
parents.push(resolved_parent);
|
||||
}
|
||||
(parents, reparented_orphans_by_parent)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTree for Builder {
|
||||
@ -453,94 +294,133 @@ impl IntoTree for Builder {
|
||||
/// resolving inconsistencies like orphans, multiple parents, and
|
||||
/// parent-child disagreements.
|
||||
fn into_tree(self) -> Result<Tree> {
|
||||
// First, resolve parents for all entries. We build two data structures:
|
||||
// a vector of resolved parents, and a lookup table for reparented
|
||||
// orphaned children.
|
||||
let (parents, mut reparented_orphans_by_parent) = self.resolve();
|
||||
let mut problems = Problems::default();
|
||||
|
||||
// First, resolve parents for all entries, and build a lookup table for
|
||||
// items without a position.
|
||||
let mut parents = Vec::with_capacity(self.entries.len());
|
||||
let mut reparented_child_indices_by_parent: HashMap<Index, Vec<Index>> = HashMap::new();
|
||||
for (entry_index, entry) in self.entries.iter().enumerate() {
|
||||
let r = ResolveParent::new(&self, entry, &mut problems);
|
||||
let resolved_parent = r.resolve();
|
||||
if let ResolvedParent::ByParentGuid(parent_index) = &resolved_parent {
|
||||
// Reparented items are special: since they aren't mentioned in
|
||||
// that parent's `children`, we don't know their positions. Note
|
||||
// them for when we resolve children. We also clone the GUID,
|
||||
// since we use it for sorting, but can't access it by
|
||||
// reference once we call `self.entries.into_iter()` below.
|
||||
let reparented_child_indices = reparented_child_indices_by_parent
|
||||
.entry(*parent_index)
|
||||
.or_default();
|
||||
reparented_child_indices.push(entry_index);
|
||||
}
|
||||
parents.push(resolved_parent);
|
||||
}
|
||||
|
||||
// If any parents form cycles, abort. We haven't seen cyclic trees in
|
||||
// the wild, and breaking cycles would add complexity.
|
||||
if let Some(index) = detect_cycles(&parents) {
|
||||
return Err(ErrorKind::Cycle(self.entries[index].item.guid.clone()).into());
|
||||
}
|
||||
for reparented_orphans in reparented_orphans_by_parent.values_mut() {
|
||||
// Use a deterministic order for reparented orphans.
|
||||
reparented_orphans.sort_unstable_by(|&index, &other_index| {
|
||||
self.entries[index]
|
||||
.item
|
||||
.guid
|
||||
.cmp(&self.entries[other_index].item.guid)
|
||||
});
|
||||
}
|
||||
|
||||
// Transform our builder entries into tree entries, with resolved
|
||||
// parents and children.
|
||||
let entries = self
|
||||
.entries
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(entry_index, entry)| {
|
||||
let mut divergence = Divergence::Consistent;
|
||||
// Then, resolve children, and build a slab of entries for the tree.
|
||||
let mut entries = Vec::with_capacity(self.entries.len());
|
||||
for (entry_index, entry) in self.entries.into_iter().enumerate() {
|
||||
// Each entry is consistent, until proven otherwise!
|
||||
let mut divergence = Divergence::Consistent;
|
||||
|
||||
let parent_index = match &parents[entry_index] {
|
||||
ResolvedParent::Root => None,
|
||||
ResolvedParent::Unchanged(index) => Some(*index),
|
||||
ResolvedParent::ByChildren(index) | ResolvedParent::ByParentGuid(index) => {
|
||||
divergence = Divergence::Diverged;
|
||||
Some(*index)
|
||||
}
|
||||
};
|
||||
let parent_index = match &parents[entry_index] {
|
||||
ResolvedParent::Root => {
|
||||
// The Places root doesn't have a parent, and should always
|
||||
// be the first entry.
|
||||
assert_eq!(entry_index, 0);
|
||||
None
|
||||
}
|
||||
ResolvedParent::ByStructure(index) => {
|
||||
// The entry has a valid parent by structure, yay!
|
||||
Some(*index)
|
||||
}
|
||||
ResolvedParent::ByChildren(index) | ResolvedParent::ByParentGuid(index) => {
|
||||
// The entry has multiple parents, and we resolved one,
|
||||
// so it's diverged.
|
||||
divergence = Divergence::Diverged;
|
||||
Some(*index)
|
||||
}
|
||||
};
|
||||
|
||||
let mut child_indices = entry
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|child_index| {
|
||||
// Filter out missing children and children that moved to a
|
||||
// different parent.
|
||||
match child_index {
|
||||
BuilderEntryChild::Exists(child_index) => {
|
||||
match &parents[*child_index] {
|
||||
ResolvedParent::Root | ResolvedParent::Unchanged(_) => {
|
||||
Some(*child_index)
|
||||
}
|
||||
|
||||
ResolvedParent::ByChildren(parent_index)
|
||||
| ResolvedParent::ByParentGuid(parent_index) => {
|
||||
divergence = Divergence::Diverged;
|
||||
if *parent_index == entry_index {
|
||||
Some(*child_index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BuilderEntryChild::Missing(_) => {
|
||||
divergence = Divergence::Diverged;
|
||||
None
|
||||
// Check if the entry's children exist and agree that this entry is
|
||||
// their parent.
|
||||
let mut child_indices = Vec::with_capacity(entry.children.len());
|
||||
for child in entry.children {
|
||||
match child {
|
||||
BuilderEntryChild::Exists(child_index) => match &parents[child_index] {
|
||||
ResolvedParent::Root => {
|
||||
// The Places root can't be a child of another entry.
|
||||
unreachable!("A child can't be a top-level root");
|
||||
}
|
||||
ResolvedParent::ByStructure(parent_index) => {
|
||||
// If the child has a valid parent by structure, it
|
||||
// must be the entry. If it's not, there's a bug
|
||||
// in `ResolveParent` or `BuilderEntry`.
|
||||
assert_eq!(*parent_index, entry_index);
|
||||
child_indices.push(child_index);
|
||||
}
|
||||
ResolvedParent::ByChildren(parent_index) => {
|
||||
// If the child has multiple parents, we may have
|
||||
// resolved a different one, so check if we decided
|
||||
// to keep the child in this entry.
|
||||
divergence = Divergence::Diverged;
|
||||
if *parent_index == entry_index {
|
||||
child_indices.push(child_index);
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(mut reparented_orphans) =
|
||||
reparented_orphans_by_parent.get_mut(&entry_index)
|
||||
{
|
||||
// Add reparented orphans to the end.
|
||||
divergence = Divergence::Diverged;
|
||||
child_indices.append(&mut reparented_orphans);
|
||||
ResolvedParent::ByParentGuid(parent_index) => {
|
||||
// We should only ever prefer parents
|
||||
// `by_parent_guid` over parents `by_children` for
|
||||
// misparented user content roots. Otherwise,
|
||||
// there's a bug in `ResolveParent`.
|
||||
assert_eq!(*parent_index, 0);
|
||||
divergence = Divergence::Diverged;
|
||||
}
|
||||
},
|
||||
BuilderEntryChild::Missing(child_guid) => {
|
||||
// If the entry's `children` mentions a GUID for which
|
||||
// we don't have an entry, note it as a problem, and
|
||||
// ignore the child.
|
||||
divergence = Divergence::Diverged;
|
||||
problems.note(
|
||||
&entry.item.guid,
|
||||
Problem::MissingChild {
|
||||
child_guid: child_guid.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TreeEntry {
|
||||
item: entry.item,
|
||||
parent_index,
|
||||
child_indices,
|
||||
divergence,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Reparented items don't appear in our `children`, so we move them
|
||||
// to the end, after existing children (rules 3-4).
|
||||
if let Some(reparented_child_indices) =
|
||||
reparented_child_indices_by_parent.get(&entry_index)
|
||||
{
|
||||
divergence = Divergence::Diverged;
|
||||
child_indices.extend_from_slice(reparented_child_indices);
|
||||
}
|
||||
|
||||
entries.push(TreeEntry {
|
||||
item: entry.item,
|
||||
parent_index,
|
||||
child_indices,
|
||||
divergence,
|
||||
});
|
||||
}
|
||||
|
||||
// Now we have a consistent tree.
|
||||
Ok(Tree {
|
||||
entry_index_by_guid: self.entry_index_by_guid,
|
||||
entries,
|
||||
deleted_guids: HashSet::new(),
|
||||
problems,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -727,6 +607,7 @@ impl BuilderEntry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds an existing child index, or missing child GUID, for a builder entry.
|
||||
#[derive(Debug)]
|
||||
enum BuilderEntryChild {
|
||||
Exists(Index),
|
||||
@ -734,7 +615,7 @@ enum BuilderEntryChild {
|
||||
}
|
||||
|
||||
/// Holds one or more parents for a builder entry.
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
enum BuilderEntryParent {
|
||||
/// The entry is an orphan.
|
||||
None,
|
||||
@ -769,11 +650,321 @@ enum BuilderParentBy {
|
||||
KnownItem(Index),
|
||||
}
|
||||
|
||||
/// Resolves the parent for a builder entry.
|
||||
struct ResolveParent<'a> {
|
||||
builder: &'a Builder,
|
||||
entry: &'a BuilderEntry,
|
||||
problems: &'a mut Problems,
|
||||
}
|
||||
|
||||
impl<'a> ResolveParent<'a> {
|
||||
fn new(
|
||||
builder: &'a Builder,
|
||||
entry: &'a BuilderEntry,
|
||||
problems: &'a mut Problems,
|
||||
) -> ResolveParent<'a> {
|
||||
ResolveParent {
|
||||
builder,
|
||||
entry,
|
||||
problems,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(self) -> ResolvedParent {
|
||||
if self.entry.item.guid.is_user_content_root() {
|
||||
self.user_content_root()
|
||||
} else {
|
||||
self.item()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the parent for this builder entry. This unifies parents
|
||||
/// `by_structure`, which are known to be consistent, and parents
|
||||
/// `by_children` and `by_parent_guid`, which are consistent if they match.
|
||||
fn parent(&self) -> Cow<'a, BuilderEntryParent> {
|
||||
let parents = match &self.entry.parent {
|
||||
// Roots and orphans pass through as-is.
|
||||
BuilderEntryParent::Root => return Cow::Owned(BuilderEntryParent::Root),
|
||||
BuilderEntryParent::None => return Cow::Owned(BuilderEntryParent::None),
|
||||
BuilderEntryParent::Complete(index) => {
|
||||
// The entry is known to have a valid parent by structure. This
|
||||
// is the fast path, used for local trees in Desktop.
|
||||
return Cow::Owned(BuilderEntryParent::Complete(*index));
|
||||
}
|
||||
BuilderEntryParent::Partial(parents) => parents,
|
||||
};
|
||||
// The entry has zero, one, or many parents, recorded separately. Check
|
||||
// if it has exactly two: one `by_parent_guid`, and one `by_children`.
|
||||
let (index_by_guid, index_by_children) = match parents.as_slice() {
|
||||
[BuilderParentBy::UnknownItem(guid), BuilderParentBy::Children(index_by_children)]
|
||||
| [BuilderParentBy::Children(index_by_children), BuilderParentBy::UnknownItem(guid)] => {
|
||||
match self.builder.entry_index_by_guid.get(guid) {
|
||||
Some(&index_by_guid) => (index_by_guid, *index_by_children),
|
||||
None => return Cow::Borrowed(&self.entry.parent),
|
||||
}
|
||||
}
|
||||
[BuilderParentBy::KnownItem(index_by_guid), BuilderParentBy::Children(index_by_children)]
|
||||
| [BuilderParentBy::Children(index_by_children), BuilderParentBy::KnownItem(index_by_guid)] => {
|
||||
(*index_by_guid, *index_by_children)
|
||||
}
|
||||
// In all other cases (missing `parentid`, missing from `children`,
|
||||
// multiple parents), return all possible parents. We'll pick one
|
||||
// when we resolve the parent.
|
||||
_ => return Cow::Borrowed(&self.entry.parent),
|
||||
};
|
||||
// If the entry has matching parents `by_children` and `by_parent_guid`,
|
||||
// it has a valid parent by structure. This is the "fast slow path",
|
||||
// used for remote trees in Desktop, because their structure is built in
|
||||
// two passes. In all other cases, we have a parent-child disagreement,
|
||||
// so return all possible parents.
|
||||
if index_by_guid == index_by_children {
|
||||
Cow::Owned(BuilderEntryParent::Complete(index_by_children))
|
||||
} else {
|
||||
Cow::Borrowed(&self.entry.parent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the parent for a user content root: menu, mobile, toolbar, and
|
||||
/// unfiled. These are simpler to resolve than non-roots because they must
|
||||
/// be children of the Places root (rule 1), which is always the first
|
||||
/// entry.
|
||||
fn user_content_root(self) -> ResolvedParent {
|
||||
match self.parent().as_ref() {
|
||||
BuilderEntryParent::None => {
|
||||
// Orphaned content root. This should only happen if the content
|
||||
// root doesn't have a parent `by_parent_guid`.
|
||||
self.problems.note(&self.entry.item.guid, Problem::Orphan);
|
||||
ResolvedParent::ByParentGuid(0)
|
||||
}
|
||||
BuilderEntryParent::Root => {
|
||||
unreachable!("A user content root can't be a top-level root")
|
||||
}
|
||||
BuilderEntryParent::Complete(index) => {
|
||||
if *index == 0 {
|
||||
ResolvedParent::ByStructure(*index)
|
||||
} else {
|
||||
// Move misparented content roots to the Places root.
|
||||
let parent_guid = self.builder.entries[*index].item.guid.clone();
|
||||
self.problems.note(
|
||||
&self.entry.item.guid,
|
||||
Problem::MisparentedRoot(vec![
|
||||
DivergedParent::ByChildren(parent_guid.clone()),
|
||||
DivergedParentGuid::Folder(parent_guid).into(),
|
||||
]),
|
||||
);
|
||||
ResolvedParent::ByParentGuid(0)
|
||||
}
|
||||
}
|
||||
BuilderEntryParent::Partial(parents_by) => {
|
||||
// Ditto for content roots with multiple parents or parent-child
|
||||
// disagreements.
|
||||
self.problems.note(
|
||||
&self.entry.item.guid,
|
||||
Problem::MisparentedRoot(
|
||||
parents_by
|
||||
.iter()
|
||||
.map(|parent_by| {
|
||||
PossibleParent::new(self.builder, parent_by).summarize()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
ResolvedParent::ByParentGuid(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the parent for a top-level Places root or other item, using
|
||||
/// rules 2-5.
|
||||
fn item(self) -> ResolvedParent {
|
||||
match self.parent().as_ref() {
|
||||
BuilderEntryParent::Root => ResolvedParent::Root,
|
||||
BuilderEntryParent::None => {
|
||||
// The item doesn't have a `parentid`, and isn't mentioned in
|
||||
// any `children`. Reparent to the default folder (rule 4) or
|
||||
// Places root (rule 5).
|
||||
let parent_index = self.reparent_orphans_to_default_index();
|
||||
self.problems.note(&self.entry.item.guid, Problem::Orphan);
|
||||
ResolvedParent::ByParentGuid(parent_index)
|
||||
}
|
||||
BuilderEntryParent::Complete(index) => {
|
||||
// The item's `parentid` and parent's `children` match, so keep
|
||||
// it in its current parent.
|
||||
ResolvedParent::ByStructure(*index)
|
||||
}
|
||||
BuilderEntryParent::Partial(parents) => {
|
||||
// For items with one or more than two parents, pick the
|
||||
// youngest (minimum age).
|
||||
let possible_parents = parents
|
||||
.iter()
|
||||
.map(|parent_by| PossibleParent::new(self.builder, parent_by))
|
||||
.collect::<Vec<_>>();
|
||||
self.problems.note(
|
||||
&self.entry.item.guid,
|
||||
Problem::DivergedParents(
|
||||
possible_parents.iter().map(|p| p.summarize()).collect(),
|
||||
),
|
||||
);
|
||||
possible_parents
|
||||
.into_iter()
|
||||
.min()
|
||||
.and_then(|p| match p.parent_by {
|
||||
BuilderParentBy::Children(index) => {
|
||||
Some(ResolvedParent::ByChildren(*index))
|
||||
}
|
||||
BuilderParentBy::KnownItem(index) => {
|
||||
Some(ResolvedParent::ByParentGuid(*index))
|
||||
}
|
||||
BuilderParentBy::UnknownItem(guid) => self
|
||||
.builder
|
||||
.entry_index_by_guid
|
||||
.get(guid)
|
||||
.filter(|&&index| self.builder.entries[index].item.is_folder())
|
||||
.map(|&index| ResolvedParent::ByParentGuid(index)),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// Fall back to the default folder (rule 4) or root
|
||||
// (rule 5) if we didn't find a parent.
|
||||
let parent_index = self.reparent_orphans_to_default_index();
|
||||
ResolvedParent::ByParentGuid(parent_index)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the index of the default parent entry for reparented orphans.
|
||||
/// This is either the default folder (rule 4), or the root, if the
|
||||
/// default folder isn't set, doesn't exist, or isn't a folder (rule 5).
|
||||
fn reparent_orphans_to_default_index(&self) -> Index {
|
||||
self.builder
|
||||
.reparent_orphans_to
|
||||
.as_ref()
|
||||
.and_then(|guid| self.builder.entry_index_by_guid.get(guid))
|
||||
.cloned()
|
||||
.filter(|&parent_index| {
|
||||
let parent_entry = &self.builder.entries[parent_index];
|
||||
parent_entry.item.is_folder()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
// A possible parent for an item with conflicting parents. We use this wrapper's
|
||||
// `Ord` implementation to decide which parent is youngest.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct PossibleParent<'a> {
|
||||
builder: &'a Builder,
|
||||
parent_by: &'a BuilderParentBy,
|
||||
}
|
||||
|
||||
impl<'a> PossibleParent<'a> {
|
||||
fn new(builder: &'a Builder, parent_by: &'a BuilderParentBy) -> PossibleParent<'a> {
|
||||
PossibleParent { builder, parent_by }
|
||||
}
|
||||
|
||||
/// Returns the problem with this conflicting parent.
|
||||
fn summarize(&self) -> DivergedParent {
|
||||
let entry = match self.parent_by {
|
||||
BuilderParentBy::Children(index) => {
|
||||
return DivergedParent::ByChildren(self.builder.entries[*index].item.guid.clone());
|
||||
}
|
||||
BuilderParentBy::KnownItem(index) => &self.builder.entries[*index],
|
||||
BuilderParentBy::UnknownItem(guid) => {
|
||||
match self.builder.entry_index_by_guid.get(guid) {
|
||||
Some(index) => &self.builder.entries[*index],
|
||||
None => return DivergedParentGuid::Missing(guid.clone()).into(),
|
||||
}
|
||||
}
|
||||
};
|
||||
if entry.item.is_folder() {
|
||||
DivergedParentGuid::Folder(entry.item.guid.clone()).into()
|
||||
} else {
|
||||
DivergedParentGuid::NonFolder(entry.item.guid.clone()).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Ord for PossibleParent<'a> {
|
||||
/// Compares two possible parents to determine which is younger
|
||||
/// (`Ordering::Less`). Prefers parents from `children` over `parentid`
|
||||
/// (rule 2), and `parentid`s that reference folders over non-folders
|
||||
/// (rule 4).
|
||||
fn cmp(&self, other: &PossibleParent) -> Ordering {
|
||||
let (index, other_index) = match (&self.parent_by, &other.parent_by) {
|
||||
(BuilderParentBy::Children(index), BuilderParentBy::Children(other_index)) => {
|
||||
// Both `self` and `other` mention the item in their `children`.
|
||||
(*index, *other_index)
|
||||
}
|
||||
(BuilderParentBy::Children(_), BuilderParentBy::KnownItem(_)) => {
|
||||
// `self` mentions the item in its `children`, and the item's
|
||||
// `parentid` is `other`, so prefer `self`.
|
||||
return Ordering::Less;
|
||||
}
|
||||
(BuilderParentBy::Children(_), BuilderParentBy::UnknownItem(_)) => {
|
||||
// As above, except we don't know if `other` exists. We don't
|
||||
// need to look it up, though, because we can unconditionally
|
||||
// prefer `self`.
|
||||
return Ordering::Less;
|
||||
}
|
||||
(BuilderParentBy::KnownItem(_), BuilderParentBy::Children(_)) => {
|
||||
// The item's `parentid` is `self`, and `other` mentions the
|
||||
// item in its `children`, so prefer `other`.
|
||||
return Ordering::Greater;
|
||||
}
|
||||
(BuilderParentBy::UnknownItem(_), BuilderParentBy::Children(_)) => {
|
||||
// As above. We don't know if `self` exists, but we
|
||||
// unconditionally prefer `other`.
|
||||
return Ordering::Greater;
|
||||
}
|
||||
// Cases where `self` and `other` are `parentid`s, existing or not,
|
||||
// are academic, since it doesn't make sense for an item to have
|
||||
// multiple `parentid`s.
|
||||
_ => return Ordering::Equal,
|
||||
};
|
||||
// If both `self` and `other` are folders, compare timestamps. If one is
|
||||
// a folder, but the other isn't, we prefer the folder. If neither is a
|
||||
// folder, it doesn't matter.
|
||||
let entry = &self.builder.entries[index];
|
||||
let other_entry = &self.builder.entries[other_index];
|
||||
match (entry.item.is_folder(), other_entry.item.is_folder()) {
|
||||
(true, true) => entry.item.age.cmp(&other_entry.item.age),
|
||||
(false, true) => Ordering::Greater,
|
||||
(true, false) => Ordering::Less,
|
||||
(false, false) => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialOrd for PossibleParent<'a> {
|
||||
fn partial_cmp(&self, other: &PossibleParent) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for PossibleParent<'a> {
|
||||
fn eq(&self, other: &PossibleParent) -> bool {
|
||||
self.cmp(other) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Eq for PossibleParent<'a> {}
|
||||
|
||||
/// Describes a resolved parent for an item.
|
||||
#[derive(Debug)]
|
||||
enum ResolvedParent {
|
||||
/// The item is a top-level root, and has no parent.
|
||||
Root,
|
||||
Unchanged(Index),
|
||||
|
||||
/// The item has a valid, consistent structure.
|
||||
ByStructure(Index),
|
||||
|
||||
/// The item has multiple parents; this is the one we picked.
|
||||
ByChildren(Index),
|
||||
|
||||
/// The item has a parent-child disagreement: the folder referenced by the
|
||||
/// item's `parentid` doesn't mention the item in its `children`, the
|
||||
/// `parentid` doesn't exist at all, or the item is a misparented content
|
||||
/// root.
|
||||
ByParentGuid(Index),
|
||||
}
|
||||
|
||||
@ -781,7 +972,7 @@ impl ResolvedParent {
|
||||
fn index(&self) -> Option<Index> {
|
||||
match self {
|
||||
ResolvedParent::Root => None,
|
||||
ResolvedParent::Unchanged(index)
|
||||
ResolvedParent::ByStructure(index)
|
||||
| ResolvedParent::ByChildren(index)
|
||||
| ResolvedParent::ByParentGuid(index) => Some(*index),
|
||||
}
|
||||
@ -816,17 +1007,168 @@ fn detect_cycles(parents: &[ResolvedParent]) -> Option<Index> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Indicates if a tree entry's structure diverged.
|
||||
#[derive(Debug)]
|
||||
enum Divergence {
|
||||
/// The node's structure is already correct, and doesn't need to be
|
||||
/// reuploaded.
|
||||
/// The structure is already correct, and doesn't need to be reuploaded.
|
||||
Consistent,
|
||||
|
||||
/// The node exists in multiple parents, or is a reparented orphan.
|
||||
/// The merger should reupload the node.
|
||||
/// The node has structure problems, and should be flagged for reupload
|
||||
/// when merging.
|
||||
Diverged,
|
||||
}
|
||||
|
||||
/// Describes a structure divergence for an item in a bookmark tree. These are
|
||||
/// used for logging and validation telemetry.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Problem {
|
||||
/// The item doesn't have a `parentid`, and isn't mentioned in any folders.
|
||||
Orphan,
|
||||
|
||||
/// The item is a user content root (menu, mobile, toolbar, or unfiled),
|
||||
/// but `parent_guid` isn't the Places root.
|
||||
MisparentedRoot(Vec<DivergedParent>),
|
||||
|
||||
/// The item has diverging parents. If the vector contains more than one
|
||||
/// `DivergedParent::ByChildren`, the item has multiple parents. If the
|
||||
/// vector contains a `DivergedParent::ByParentGuid`, with or without a
|
||||
/// `DivergedParent::ByChildren`, the item has a parent-child disagreement.
|
||||
DivergedParents(Vec<DivergedParent>),
|
||||
|
||||
/// The item is mentioned in a folder's `children`, but doesn't exist or is
|
||||
/// deleted.
|
||||
MissingChild { child_guid: Guid },
|
||||
}
|
||||
|
||||
/// Describes where an invalid parent comes from.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum DivergedParent {
|
||||
/// The item appears in this folder's `children`.
|
||||
ByChildren(Guid),
|
||||
/// The `parentid` references this folder.
|
||||
ByParentGuid(DivergedParentGuid),
|
||||
}
|
||||
|
||||
impl From<DivergedParentGuid> for DivergedParent {
|
||||
fn from(d: DivergedParentGuid) -> DivergedParent {
|
||||
DivergedParent::ByParentGuid(d)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DivergedParent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
DivergedParent::ByChildren(parent_guid) => {
|
||||
write!(f, "is in children of {}", parent_guid)
|
||||
}
|
||||
DivergedParent::ByParentGuid(p) => match p {
|
||||
DivergedParentGuid::Folder(parent_guid) => write!(f, "has parent {}", parent_guid),
|
||||
DivergedParentGuid::NonFolder(parent_guid) => {
|
||||
write!(f, "has non-folder parent {}", parent_guid)
|
||||
}
|
||||
DivergedParentGuid::Missing(parent_guid) => {
|
||||
write!(f, "has nonexistent parent {}", parent_guid)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes an invalid `parentid`.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum DivergedParentGuid {
|
||||
/// Exists and is a folder.
|
||||
Folder(Guid),
|
||||
/// Exists, but isn't a folder.
|
||||
NonFolder(Guid),
|
||||
/// Doesn't exist at all.
|
||||
Missing(Guid),
|
||||
}
|
||||
|
||||
/// Records problems for all items in a tree.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Problems(HashMap<Guid, Vec<Problem>>);
|
||||
|
||||
impl Problems {
|
||||
/// Notes a problem for an item.
|
||||
pub fn note(&mut self, guid: &Guid, problem: Problem) -> &mut Problems {
|
||||
self.0.entry(guid.clone()).or_default().push(problem);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns `true` if there are no problems.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator for all problems.
|
||||
pub fn summarize(&self) -> impl Iterator<Item = ProblemSummary> {
|
||||
self.0.iter().flat_map(|(guid, problems)| {
|
||||
problems
|
||||
.iter()
|
||||
.map(move |problem| ProblemSummary(guid, problem))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A printable summary of a problem for an item.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ProblemSummary<'a>(&'a Guid, &'a Problem);
|
||||
|
||||
impl<'a> ProblemSummary<'a> {
|
||||
#[inline]
|
||||
pub fn guid(&self) -> &Guid {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn problem(&self) -> &Problem {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ProblemSummary<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let parents = match self.problem() {
|
||||
Problem::Orphan => return write!(f, "{} is an orphan", self.guid()),
|
||||
Problem::MisparentedRoot(parents) => {
|
||||
write!(f, "{} is a user content root", self.guid())?;
|
||||
if parents.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
f.write_str(", but ")?;
|
||||
parents
|
||||
}
|
||||
Problem::DivergedParents(parents) => {
|
||||
if parents.is_empty() {
|
||||
return write!(f, "{} has diverged parents", self.guid());
|
||||
}
|
||||
write!(f, "{} ", self.guid())?;
|
||||
parents
|
||||
}
|
||||
Problem::MissingChild { child_guid } => {
|
||||
return write!(f, "{} has nonexistent child {}", self.guid(), child_guid);
|
||||
}
|
||||
};
|
||||
match parents.as_slice() {
|
||||
[a] => write!(f, "{}", a)?,
|
||||
[a, b] => write!(f, "{} and {}", a, b)?,
|
||||
_ => {
|
||||
for (i, parent) in parents.iter().enumerate() {
|
||||
if i != 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
if i == parents.len() - 1 {
|
||||
f.write_str("and ")?;
|
||||
}
|
||||
write!(f, "{}", parent)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in a bookmark tree that knows its parent and children, and
|
||||
/// dereferences to its item.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@ -920,7 +1262,13 @@ impl<'t> Node<'t> {
|
||||
if children.is_empty() {
|
||||
format!("{}{} {}", prefix, kind, self.1.item)
|
||||
} else {
|
||||
format!("{}📂 {}\n{}", prefix, self.1.item, children.join("\n"))
|
||||
format!(
|
||||
"{}{} {}\n{}",
|
||||
prefix,
|
||||
kind,
|
||||
self.1.item,
|
||||
children.join("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -6,7 +6,7 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
atomic_refcell = "0.1"
|
||||
dogear = "0.2.2"
|
||||
dogear = "0.2.3"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
moz_task = { path = "../../../../xpcom/rust/moz_task" }
|
||||
|
Loading…
Reference in New Issue
Block a user