Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  accountant.rs   Sprache: Rust

 

//! An account for backups that ensures sanity of the stored/restored data.

use std::collections::{HashMap, HashSet};

use tracing::{trace, warn};

use crate::{
    common::{ConversationId, DistributionListId, GroupIdentity, MessageId, ThreemaId, keys::PublicKey},
    model::{
        contact::Contact,
        distribution_list::DistributionList,
        group::GroupWithProfilePicture,
        message::{MessageState, MessageStateBody, PollClose, PollOpen},
        poll::{Poll, PollId},
        settings::Settings,
    },
    protobuf,
};

#[derive(Default)]
struct EncounteredPoll {
    /// Whether we have already encountered the [`PollOpen`] message.
    encountered_open: bool,

    /// Whether we have already encountered the [`PollClose`] message.
    encountered_close: bool,
}

/// Result of checking whether a poll has been encountered before.
pub(super) enum EncounteredPollCheck {
    /// The poll was not encountered before.
    Unknown,

    /// The poll was already referenced by a previous message of the same type (i.e., a [`PollOpen`] or
    /// [`PollClose`]).
    ReferencedBefore,

    /// Everything is Ok, i.e., the poll was encountered before and has been referenced for the first time for
    /// this type.
    Ok,
}

struct EncounteredPolls(HashMap<(ConversationId, PollId), EncounteredPoll>);
impl EncounteredPolls {
    fn new() -> Self {
        Self(HashMap::new())
    }

    fn check_and_add(&mut self, poll: &Poll) -> bool {
        // Bail if the poll has been encountered before.
        if self.0.contains_key(&(poll.conversation, poll.id)) {
            return false;
        }

        // Add the poll.
        let _ = self
            .0
            .insert((poll.conversation, poll.id), EncounteredPoll::default());
        true
    }

    #[must_use]
    fn check_open(&mut self, conversation: ConversationId, poll_open: &PollOpen) -> EncounteredPollCheck {
        // Bail if the referenced poll has not been encountered before.
        let Some(poll) = self.0.get_mut(&(conversation, poll_open.id)) else {
            return EncounteredPollCheck::Unknown;
        };

        // Bail if the poll was already opened before.
        if poll.encountered_open {
            return EncounteredPollCheck::ReferencedBefore;
        }

        // Mark the poll as opened and return Ok.
        poll.encountered_open = true;
        EncounteredPollCheck::Ok
    }

    #[must_use]
    fn check_close(&mut self, conversation: ConversationId, poll_close: &PollClose) -> EncounteredPollCheck {
        // Bail if the referenced poll has not been encountered before.
        let Some(poll) = self.0.get_mut(&(conversation, poll_close.id)) else {
            return EncounteredPollCheck::Unknown;
        };

        // Bail if the poll was already closed before.
        if poll.encountered_close {
            return EncounteredPollCheck::ReferencedBefore;
        }

        // Mark the poll as closed and return Ok.
        poll.encountered_close = true;
        EncounteredPollCheck::Ok
    }
}

/// Tracks data encountered during backup creation or restoration to enforce protocol invariance.
pub(super) struct BackupAccountant {
    user_identity: ThreemaId,

    /// A map of encountered contacts to their acquaintance level.
    ///
    /// Note: The public keys are stored separately for efficient lookup.
    contact_acquaintance_levels: HashMap<ThreemaId, protobuf::d2d_sync::contact::AcquaintanceLevel>,

    /// A set of all the encountered contact public keys.
    ///
    /// Note: This is used to efficiently check whether we have encountered a contact's public key before.
    contact_public_keys: HashSet<PublicKey>,

    /// A list of all the user's distribution lists with its members (not including the user).
    distribution_lists: HashMap<DistributionListId, HashSet<ThreemaId>>,

    /// A list of all the user's groups with its members (not including the creator or the user).
    groups: HashMap<GroupIdentity, HashSet<ThreemaId>>,

    /// A set of poll events that have been encountered before.
    polls: EncounteredPolls,

    message_ids: HashSet<(ConversationId, MessageId)>,
}
impl BackupAccountant {
    pub(super) fn new(user_identity: ThreemaId) -> Self {
        Self {
            user_identity,
            contact_acquaintance_levels: HashMap::new(),
            contact_public_keys: HashSet::new(),
            distribution_lists: HashMap::new(),
            groups: HashMap::new(),
            polls: EncounteredPolls::new(),
            message_ids: HashSet::new(),
        }
    }

    /// Sanitize settings by removing any references to the user's own identity.
    pub(super) fn sanitize_settings(&self, mut settings: Settings) -> Settings {
        settings.blocked_identities.retain(|identity| {
            if *identity == self.user_identity {
                warn!("Dropping the user's identity from blocked identities");
                false
            } else {
                true
            }
        });
        settings.exclude_from_sync_identities.retain(|identity| {
            if *identity == self.user_identity {
                warn!("Dropping the user's identity from identities excluded from sync");
                false
            } else {
                true
            }
        });
        settings
    }

    /// Check if the identity should be added.
    pub(super) fn check_and_add_contact(&mut self, contact: &Contact) -> bool {
        let identity = contact.identity;

        // Bail if the contact has the user's identity.
        if identity == self.user_identity {
            warn!("Skipping contact with the user's identity");
            return false;
        }

        // Bail if the public key already exists.
        if self.contact_public_keys.contains(&contact.public_key) {
            warn!(
                ?identity,
                "Skipping contact with same public key as previous contact"
            );
            return false;
        }

        // Bail if the identity was encountered before.
        if self.contact_acquaintance_levels.contains_key(&identity) {
            warn!(?identity, "Skipping previously encountered contact");
            return false;
        }

        // Store contact acquaintance level and public key.
        let _ = self
            .contact_acquaintance_levels
            .insert(identity, contact.acquaintance_level);
        let _ = self.contact_public_keys.insert(contact.public_key);
        trace!(?identity, "Contact passed all sanity checks");
        true
    }

    /// Sanitize a distribution list and check if it should be added.
    pub(super) fn sanitize_and_add_distribution_list(
        &mut self,
        mut distribution_list: DistributionList,
    ) -> Option<DistributionList> {
        distribution_list.members.retain(|member| {
            // Remove user from members.
            if member == &self.user_identity {
                warn!(
                    id = ?distribution_list.identity,
                    "Removing user from distribution list members"
                );
                return false;
            }

            // Remove unknown members.
            if self.contact_acquaintance_levels.contains_key(member) {
                true
            } else {
                warn!(
                    id = ?distribution_list.identity,
                    ?member,
                    "Removing unknown member from distribution list"
                );
                false
            }
        });

        // Bail if the member list is empty.
        if distribution_list.members.is_empty() {
            warn!(id = ?distribution_list.identity, "Skipping distribution list with empty member list");
            return None;
        }

        // Bail if the distribution list has been previously encountered.
        if self.distribution_lists.contains_key(&distribution_list.identity) {
            warn!(id = ?distribution_list.identity, "Skipping previously encountered distribution list");
            return None;
        }

        // All checks passed, add the distribution list.
        let _ = self
            .distribution_lists
            .insert(distribution_list.identity, distribution_list.members.clone());
        trace!(id = ?distribution_list.identity, "Distribution list passed all sanity checks");
        Some(distribution_list)
    }

    /// Sanitize the group and check if it should be added.
    ///
    /// Note: We explicitly allow groups with an empty members list (a.k.a. _notes group_).
    pub(super) fn sanitize_and_add_group(
        &mut self,
        mut group: GroupWithProfilePicture,
    ) -> Option<GroupWithProfilePicture> {
        let group_identity = group.group.identity;

        // Bail if the creator is not known.
        if !(group_identity.creator_identity == self.user_identity
            || self
                .contact_acquaintance_levels
                .contains_key(&group_identity.creator_identity))
        {
            warn!(?group_identity, "Skipping group with unknown creator");
            return None;
        }

        // Sanitize group members.
        group.group.members.retain(|member| {
            // Remove members equal to the creator.
            if member == &group.group.identity.creator_identity {
                warn!(?group_identity, "Removing creator from member list");
                return false;
            }

            // Remove members equal to the user.
            if member == &self.user_identity {
                warn!(?group_identity, "Removing user from member list");
                return false;
            }

            // Remove unknown members.
            if !self.contact_acquaintance_levels.contains_key(member) {
                warn!(?group_identity, ?member, "Removing unknown group member");
                return false;
            }

            true
        });

        // Bail if the group was encountered before.
        if self.groups.contains_key(&group_identity) {
            warn!(?group_identity, "Skipping previously encountered group");
            return None;
        }

        // All checks passed, add the group.
        let _ = self.groups.insert(group_identity, group.group.members.clone());
        trace!(?group_identity, "Group passed all sanity checks");
        Some(group)
    }

    /// Check if the poll should be added.
    pub(super) fn check_and_add_poll(&mut self, poll: &Poll) -> bool {
        let poll_id = poll.id;
        let conversation = poll.conversation;

        // Bail if the conversation is unknown.
        match conversation {
            ConversationId::Contact(contact) => {
                if !self.contact_acquaintance_levels.contains_key(&contact) {
                    warn!(
                        ?poll_id,
                        ?conversation,
                        "Skipping poll in unknown 1:1 conversation"
                    );
                    return false;
                }
            },
            ConversationId::DistributionList(_) => {
                warn!(
                    ?poll_id,
                    ?conversation,
                    "Skipping poll in distribution list (this is not supported)"
                );
                return false;
            },
            ConversationId::Group(group_identity) => {
                if !self.groups.contains_key(&group_identity) {
                    warn!(
                        ?poll_id,
                        ?conversation,
                        "Skipping poll in unknown group conversation"
                    );
                    return false;
                }
            },
        }

        // Bail if the creator is neither the user nor a known contact.
        if !(poll.creator_identity == self.user_identity
            || self
                .contact_acquaintance_levels
                .contains_key(&poll.creator_identity))
        {
            warn!(
                ?poll_id,
                ?conversation,
                creator = ?&poll.creator_identity,
                "Skipping poll with unknown creator"
            );
            return false;
        }

        // Bail if any participant is unknown.
        if let Some(participants) = poll.participants() {
            for participant in participants {
                if !(participant == self.user_identity
                    || self.contact_acquaintance_levels.contains_key(&participant))
                {
                    warn!(
                        ?poll_id,
                        ?conversation,
                        ?participant,
                        "Skipping poll with unknown participant"
                    );
                    return false;
                }
            }
        }

        // Bail if the poll has been encountered before.
        if !self.polls.check_and_add(poll) {
            warn!(?poll_id, ?conversation, "Skipping previously encountered poll");
            return false;
        }

        trace!(?poll_id, ?conversation, "Poll passed all sanity checks");
        true
    }

    /// Sanitize and check if the message should be added, validating its conversation and reactions.
    pub(super) fn sanitize_and_add_message(&mut self, mut message: MessageState) -> Option<MessageState> {
        let conversation_id = message.conversation_id;
        let message_id = message.message_id;

        // Check if the conversation is valid.
        match conversation_id {
            ConversationId::Contact(contact) => {
                if self
                    .contact_acquaintance_levels
                    .get(&contact)
                    .is_none_or(|acquaintance_level| {
                        *acquaintance_level != protobuf::d2d_sync::contact::AcquaintanceLevel::Direct
                    })
                {
                    warn!(
                        ?message_id,
                        ?conversation_id,
                        "Skipping message in unknown 1:1 conversation"
                    );
                    return None;
                }
            },
            ConversationId::DistributionList(distribution_list) => {
                if !self.distribution_lists.contains_key(&distribution_list) {
                    warn!(
                        ?message_id,
                        ?conversation_id,
                        "Skipping message in unknown distribution list"
                    );
                    return None;
                }
            },
            ConversationId::Group(group_identity) => {
                if !self.groups.contains_key(&group_identity) {
                    warn!(
                        ?message_id,
                        ?conversation_id,
                        "Skipping message in unknown group conversation"
                    );
                    return None;
                }
            },
        }

        // Sanitize reactions.
        message.reactions.retain(|reaction| {
            if reaction.creator_identity == self.user_identity
                || self
                    .contact_acquaintance_levels
                    .contains_key(&reaction.creator_identity)
            {
                true
            } else {
                warn!(
                    ?message_id,
                    ?conversation_id,
                    ?reaction,
                    "Removing reaction from unknown sender"
                );
                false
            }
        });

        // Run body-specific checks.
        match &message.body {
            MessageStateBody::PollOpen(poll_open) => {
                let poll_id = poll_open.id;
                match self.polls.check_open(conversation_id, poll_open) {
                    EncounteredPollCheck::Unknown => {
                        warn!(
                            ?message_id,
                            ?conversation_id,
                            ?poll_id,
                            "Skipping poll-open message for unknown poll state"
                        );
                        return None;
                    },
                    EncounteredPollCheck::ReferencedBefore => {
                        warn!(
                            ?message_id,
                            ?conversation_id,
                            ?poll_id,
                            "Skipping poll-open message for a poll state that was already marked as opened \
                             before"
                        );
                        return None;
                    },
                    EncounteredPollCheck::Ok => {
                        // All fine, continue.
                    },
                }
            },

            MessageStateBody::PollClose(poll_close) => {
                let poll_id = poll_close.id;

                #[expect(clippy::match_same_arms, reason = "Different comments")]
                match self.polls.check_close(conversation_id, poll_close) {
                    EncounteredPollCheck::Unknown => {
                        // This is fine, we might not have been in conversation when receiving the poll-open
                        // message.
                    },
                    EncounteredPollCheck::ReferencedBefore => {
                        warn!(
                            ?message_id,
                            ?conversation_id,
                            ?poll_id,
                            "Skipping poll-close message for a poll state that was already marked as closed \
                             before"
                        );
                        return None;
                    },
                    EncounteredPollCheck::Ok => {
                        // All fine, continue.
                    },
                }
            },

            MessageStateBody::Deleted(_) => {
                if !message.reactions.is_empty() {
                    warn!(
                        ?message_id,
                        ?conversation_id,
                        "Removing all reactions from deleted message"
                    );
                    message.reactions.clear();
                }
            },

            MessageStateBody::Text(_) | MessageStateBody::Location(_) => {
                // No checks required.
            },
        }

        // Bail if the message ID was encountered before.
        if !self.message_ids.insert((conversation_id, message_id)) {
            warn!(
                ?message_id,
                ?conversation_id,
                "Skipping already encountered message"
            );
            return None;
        }

        // Everything passed, return true.
        trace!(
            ?message_id,
            ?conversation_id,
            "Sanitized message passed all checks"
        );
        Some(message)
    }
}

Messung
CHZG
859710094

¤ Dauer der Verarbeitung: 0.7 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104850
#Domains=416977