/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Support for converting Mononoke data structures into in-memory blobs.

use anyhow::Result;
use async_trait::async_trait;
use blobstore::Blobstore;
use blobstore::BlobstoreBytes;
use blobstore::Storable;
use bytes::Bytes;
use context::CoreContext;

use crate::typed_hash::BasenameSuffixSkeletonManifestId;
use crate::typed_hash::BlobstoreKey;
use crate::typed_hash::ChangesetId;
use crate::typed_hash::ContentChunkId;
use crate::typed_hash::ContentId;
use crate::typed_hash::ContentMetadataV2Id;
use crate::typed_hash::DeletedManifestV2Id;
use crate::typed_hash::FastlogBatchId;
use crate::typed_hash::FileUnodeId;
use crate::typed_hash::FsnodeId;
use crate::typed_hash::ManifestUnodeId;
use crate::typed_hash::RawBundle2Id;
use crate::typed_hash::RedactionKeyListId;
use crate::typed_hash::SkeletonManifestId;

/// A serialized blob in memory.
#[derive(Clone)]
pub struct Blob<Id: BlobstoreKey> {
    id: Id,
    data: Bytes,
}

impl<Id: BlobstoreKey> Blob<Id> {
    pub fn new(id: Id, data: Bytes) -> Self {
        Self { id, data }
    }

    #[inline]
    pub fn len(&self) -> usize {
        self.data.len()
    }

    #[inline]
    pub fn is_empty(&self) -> bool {
        self.data.is_empty()
    }

    pub fn id(&self) -> &Id {
        &self.id
    }

    pub fn data(&self) -> &Bytes {
        &self.data
    }
}

#[async_trait]
impl<K: BlobstoreKey + Copy + Send + Sync + 'static> Storable for Blob<K> {
    type Key = K;

    async fn store<'a, B: Blobstore>(
        self,
        ctx: &'a CoreContext,
        blobstore: &'a B,
    ) -> Result<Self::Key> {
        let id = *self.id();
        let bytes = self.into();
        blobstore.put(ctx, id.blobstore_key(), bytes).await?;
        Ok(id)
    }
}

pub type ChangesetBlob = Blob<ChangesetId>;
pub type ContentBlob = Blob<ContentId>;
pub type ContentChunkBlob = Blob<ContentChunkId>;
pub type RawBundle2Blob = Blob<RawBundle2Id>;
pub type FileUnodeBlob = Blob<FileUnodeId>;
pub type ManifestUnodeBlob = Blob<ManifestUnodeId>;
pub type DeletedManifestV2Blob = Blob<DeletedManifestV2Id>;
pub type FsnodeBlob = Blob<FsnodeId>;
pub type SkeletonManifestBlob = Blob<SkeletonManifestId>;
pub type ContentMetadataV2Blob = Blob<ContentMetadataV2Id>;
pub type FastlogBatchBlob = Blob<FastlogBatchId>;
pub type RedactionKeyListBlob = Blob<RedactionKeyListId>;
pub type BasenameSuffixSkeletonManifestBlob = Blob<BasenameSuffixSkeletonManifestId>;

impl<Id: BlobstoreKey> From<Blob<Id>> for BlobstoreBytes {
    #[inline]
    fn from(blob: Blob<Id>) -> BlobstoreBytes {
        BlobstoreBytes::from_bytes(blob.data)
    }
}

pub trait BlobstoreValue: Sized + Send {
    type Key: BlobstoreKey;
    fn into_blob(self) -> Blob<Self::Key>;
    fn from_blob(blob: Blob<Self::Key>) -> Result<Self>;
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;

use anyhow::bail;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use bytes::Bytes;
use fbthrift::compact_protocol;
use quickcheck::Arbitrary;
use quickcheck::Gen;
use smallvec::SmallVec;
use sorted_vector_map::SortedVectorMap;

use crate::blob::Blob;
use crate::blob::BlobstoreValue;
use crate::blob::ChangesetBlob;
use crate::datetime::DateTime;
use crate::errors::ErrorKind;
use crate::file_change::BasicFileChange;
use crate::file_change::FileChange;
use crate::hash::GitSha1;
use crate::path;
use crate::path::MPath;
use crate::thrift;
use crate::typed_hash::ChangesetId;
use crate::typed_hash::ChangesetIdContext;
use crate::ContentId;

const ARBITRARY_SHRINK_FACTOR: usize = 3;

/// A struct callers can use to build up a `BonsaiChangeset`.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct BonsaiChangesetMut {
    pub parents: Vec<ChangesetId>,
    pub author: String,
    pub author_date: DateTime,
    pub committer: Option<String>,
    // XXX should committer date always be recorded? If so, it should probably be a
    // monotonically increasing value:
    // max(author date, max(committer date of parents) + epsilon)
    pub committer_date: Option<DateTime>,
    pub message: String,
    pub hg_extra: SortedVectorMap<String, Vec<u8>>,
    pub git_extra_headers: Option<SortedVectorMap<SmallVec<[u8; 24]>, Bytes>>,
    pub file_changes: SortedVectorMap<MPath, FileChange>,
    pub is_snapshot: bool,
    pub git_tree_hash: Option<GitSha1>,
    pub git_annotated_tag: Option<BonsaiAnnotatedTag>,
}

impl Default for BonsaiChangesetMut {
    fn default() -> Self {
        Self {
            parents: Vec::new(),
            author: String::default(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::default(),
            hg_extra: SortedVectorMap::default(),
            git_extra_headers: None,
            file_changes: SortedVectorMap::default(),
            is_snapshot: false,
            git_tree_hash: None,
            git_annotated_tag: None,
        }
    }
}

impl BonsaiChangesetMut {
    /// Create from a thrift `BonsaiChangeset`.
    fn from_thrift(tc: thrift::BonsaiChangeset) -> Result<Self> {
        Ok(BonsaiChangesetMut {
            parents: tc
                .parents
                .into_iter()
                .map(ChangesetId::from_thrift)
                .collect::<Result<_>>()?,
            author: tc.author,
            author_date: DateTime::from_thrift(
                tc.author_date
                    .ok_or_else(|| Error::msg("missing author date field"))?,
            )?,
            committer: tc.committer,
            committer_date: tc.committer_date.map(DateTime::from_thrift).transpose()?,
            message: tc.message,
            hg_extra: tc.hg_extra,
            git_extra_headers: tc
                .git_extra_headers
                .map(|extra| extra.into_iter().map(|(k, v)| (k.0, v)).collect()),
            file_changes: tc
                .file_changes
                .into_iter()
                .map(|(f, fc_opt)| {
                    let mpath = MPath::from_thrift(f)?;
                    let fc_opt = FileChange::from_thrift(fc_opt, &mpath)?;
                    Ok((mpath, fc_opt))
                })
                .collect::<Result<_>>()?,
            is_snapshot: tc.snapshot_state.is_some(),
            git_tree_hash: tc
                .git_tree_hash
                .map(|hash| GitSha1::from_bytes(hash.0))
                .transpose()
                .context("Invalid SHA1 hash for git tree hash")?,
            git_annotated_tag: tc
                .git_annotated_tag
                .map(BonsaiAnnotatedTag::from_thrift)
                .transpose()
                .context("Invalid annotated tag")?,
        })
    }

    /// Convert into a thrift `BonsaiChangeset`.
    fn into_thrift(self) -> thrift::BonsaiChangeset {
        thrift::BonsaiChangeset {
            parents: self
                .parents
                .into_iter()
                .map(|parent| parent.into_thrift())
                .collect(),
            author: self.author,
            author_date: Some(self.author_date.into_thrift()),
            committer: self.committer,
            committer_date: self.committer_date.map(DateTime::into_thrift),
            message: self.message,
            hg_extra: self.hg_extra,
            git_extra_headers: self.git_extra_headers.map(|extra| {
                extra
                    .into_iter()
                    .map(|(k, v)| (thrift::small_binary(k), v))
                    .collect()
            }),
            file_changes: self
                .file_changes
                .into_iter()
                .map(|(f, c)| (f.into_thrift(), c.into_thrift()))
                .collect(),
            snapshot_state: self.is_snapshot.then_some(thrift::SnapshotState {}),
            git_tree_hash: self.git_tree_hash.map(|hash| hash.into_thrift()),
            git_annotated_tag: self.git_annotated_tag.map(BonsaiAnnotatedTag::into_thrift),
        }
    }

    /// Compute the changeset id for the `BonsaiChangeset`.
    fn changeset_id(&self) -> ChangesetId {
        let thrift = self.clone().into_thrift();
        let data = compact_protocol::serialize(&thrift);
        let mut context = ChangesetIdContext::new();
        context.update(&data);
        context.finish()
    }

    /// Freeze this instance and turn it into a `BonsaiChangeset`.
    pub fn freeze(self) -> Result<BonsaiChangeset> {
        self.verify()?;
        let id = self.changeset_id();
        Ok(BonsaiChangeset { inner: self, id })
    }

    /// Verify that this will form a valid `BonsaiChangeset`.
    ///
    /// Note that this doesn't (and can't) make any checks that require referring to data
    /// that's external to this changeset. For example, a changeset that deletes a file that
    /// doesn't exist in its parent is invalid. Instead, it only checks for internal consistency.
    pub fn verify(&self) -> Result<()> {
        // Check that the copy info ID refers to a parent in the parent set.
        for (path, fc) in &self.file_changes {
            if let Some((copy_from_path, copy_from_id)) = fc.copy_from() {
                if !self.parents.contains(copy_from_id) {
                    bail!(ErrorKind::InvalidBonsaiChangeset(format!(
                        "copy information for path '{}' (from '{}') has parent {} which isn't \
                             recognized",
                        path, copy_from_path, copy_from_id
                    )));
                }
            }
        }

        // Check that the list of file changes doesn't have any path conflicts.
        path::check_pcf(
            self.file_changes
                .iter()
                .map(|(path, change)| (path, change.is_changed())),
        )
        .with_context(|| ErrorKind::InvalidBonsaiChangeset("invalid file change list".into()))?;

        // Check that this changeset has untracked/missing files only if it is a snapshot
        if !self.is_snapshot {
            for (_, fc) in &self.file_changes {
                match fc {
                    FileChange::UntrackedDeletion | FileChange::UntrackedChange(_) => {
                        bail!(ErrorKind::InvalidBonsaiChangeset(
                            "untracked changes present in non-snapshot changeset".to_string()
                        ));
                    }
                    FileChange::Deletion | FileChange::Change(_) => {}
                }
            }
        }

        // A changeset that represents a git tree should not have any file changes or parents
        if self.git_tree_hash.is_some()
            && !(self.parents.is_empty() && self.file_changes.is_empty())
        {
            bail!(ErrorKind::InvalidBonsaiChangeset("bonsai changeset representing a git tree should not have any parents or file changes".to_string()))
        }

        // If the changeset is a git annotated tag, it should not have any parents or file changes
        if self.git_annotated_tag.is_some()
            && !(self.parents.is_empty() && self.file_changes.is_empty())
        {
            bail!(ErrorKind::InvalidBonsaiChangeset("bonsai changeset representing a git annotated tag should not have any parents or file changes".to_string()))
        }

        // The changeset is either a git tree or a git annotated tag, but it cannot be both
        if self.git_tree_hash.is_some() && self.git_annotated_tag.is_some() {
            bail!(ErrorKind::InvalidBonsaiChangeset("bonsai changeset cannot represent both git tree and git annotated tag at the same time".to_string()))
        }

        Ok(())
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct BonsaiChangeset {
    /// The changeset data.
    ///
    /// This is immutable while the changeset is frozen.  Use `into_mut` to
    /// thaw the changeset and make it mutable again.
    inner: BonsaiChangesetMut,

    /// The changeset id.
    ///
    /// `BonsaiChangeset` is immutable, so this is computed once, either when
    /// the changeset is frozen, or when it is loaded.
    id: ChangesetId,
}

impl BonsaiChangeset {
    pub fn from_bytes(data: impl AsRef<[u8]>) -> Result<Self> {
        let data = data.as_ref();
        let id = {
            let mut context = ChangesetIdContext::new();
            context.update(data);
            context.finish()
        };
        let thrift_tc = compact_protocol::deserialize(data)
            .with_context(|| ErrorKind::BlobDeserializeError("BonsaiChangeset".into()))?;
        let bcs = Self::from_thrift_with_id(thrift_tc, id)?;
        Ok(bcs)
    }

    fn from_thrift_with_id(tc: thrift::BonsaiChangeset, id: ChangesetId) -> Result<Self> {
        let catch_block = || -> Result<_> {
            let inner = BonsaiChangesetMut::from_thrift(tc)?;
            inner.verify()?;
            Ok(BonsaiChangeset { inner, id })
        };

        catch_block().with_context(|| {
            ErrorKind::InvalidThrift("BonsaiChangeset".into(), "Invalid changeset".into())
        })
    }

    /// Get the parents for this changeset. The order of parents is significant.
    pub fn parents<'a>(&'a self) -> impl Iterator<Item = ChangesetId> + 'a {
        self.inner.parents.iter().cloned()
    }

    #[inline]
    pub fn is_merge(&self) -> bool {
        self.inner.parents.len() > 1
    }

    /// Get the files changed in this changeset. The items returned are guaranteed
    /// to be in depth-first traversal order: once all the changes to a particular
    /// tree have been applied, it will never be referred to again.
    pub fn file_changes(&self) -> impl ExactSizeIterator<Item = (&MPath, &FileChange)> {
        self.inner.file_changes.iter()
    }

    /// File changes, but untracked and tracked changes are merged together for easy handling
    pub fn simplified_file_changes(
        &self,
    ) -> impl Iterator<Item = (&MPath, Option<&BasicFileChange>)> {
        self.inner
            .file_changes
            .iter()
            .map(|(path, fc)| (path, fc.simplify()))
    }

    pub fn file_changes_map(&self) -> &SortedVectorMap<MPath, FileChange> {
        &self.inner.file_changes
    }

    /// Get the author for this changeset.
    pub fn author(&self) -> &str {
        &self.inner.author
    }

    /// Get the author date (time and timezone) for this changeset.
    pub fn author_date(&self) -> &DateTime {
        &self.inner.author_date
    }

    /// Get the committer for this changeset.
    pub fn committer(&self) -> Option<&str> {
        match self.inner.committer {
            Some(ref c) => Some(c.as_str()),
            None => None,
        }
    }

    /// Get the committer date (time and timezone) for this changeset.
    pub fn committer_date(&self) -> Option<&DateTime> {
        self.inner.committer_date.as_ref()
    }

    /// Get the commit message.
    pub fn message(&self) -> &str {
        &self.inner.message
    }

    /// Get the hg_extra fields for this message.
    pub fn hg_extra(&self) -> impl Iterator<Item = (&str, &[u8])> {
        self.inner
            .hg_extra
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_slice()))
    }

    /// Get the git_extra_headers fields for this message.
    pub fn git_extra_headers(&self) -> Option<impl Iterator<Item = (&[u8], &[u8])>> {
        self.inner
            .git_extra_headers
            .as_ref()
            .map(|extra| extra.iter().map(|(k, v)| (k.as_slice(), v.as_ref())))
    }

    /// Get the git tree hash corresponding to the current changeset
    pub fn git_tree_hash(&self) -> Option<&GitSha1> {
        self.inner.git_tree_hash.as_ref()
    }

    /// Get the changeset ID of this changeset.
    pub fn get_changeset_id(&self) -> ChangesetId {
        self.id
    }

    /// Whether this changeset is a snapshot
    pub fn is_snapshot(&self) -> bool {
        self.inner.is_snapshot
    }

    /// Allow mutating this instance of `BonsaiChangeset`.
    pub fn into_mut(self) -> BonsaiChangesetMut {
        self.inner
    }
}

impl BlobstoreValue for BonsaiChangeset {
    type Key = ChangesetId;

    fn into_blob(self) -> ChangesetBlob {
        let id = self.id;
        let thrift = self.inner.into_thrift();
        let data = compact_protocol::serialize(&thrift);
        Blob::new(id, data)
    }

    fn from_blob(blob: Blob<Self::Key>) -> Result<Self> {
        let thrift_tc = compact_protocol::deserialize(blob.data().as_ref())
            .with_context(|| ErrorKind::BlobDeserializeError("BonsaiChangeset".into()))?;
        let bcs = Self::from_thrift_with_id(thrift_tc, *blob.id())?;
        Ok(bcs)
    }
}

impl Arbitrary for BonsaiChangeset {
    fn arbitrary(g: &mut Gen) -> Self {
        // In the future Mononoke would like to support changesets with more parents than 2.
        // Start testing that now.
        let size = g.size();
        let num_parents = usize::arbitrary(g) % 8;
        let parents: Vec<_> = (0..num_parents)
            .map(|_| ChangesetId::arbitrary(g))
            .collect();
        let num_changes = usize::arbitrary(g) % size;
        let file_changes: BTreeMap<_, _> = (0..num_changes)
            .map(|_| {
                let fc_opt = if *g.choose(&[0, 1, 2]).unwrap() < 1 {
                    FileChange::arbitrary_from_parents(g, &parents)
                } else {
                    FileChange::Deletion
                };
                // XXX be smarter about generating paths here?
                (MPath::arbitrary(g), fc_opt)
            })
            .collect();

        if path::check_pcf(
            file_changes
                .iter()
                .map(|(path, change)| (path, change.is_changed())),
        )
        .is_err()
        {
            // This is rare but is definitely possible. Retry in this case.
            Self::arbitrary(g)
        } else {
            BonsaiChangesetMut {
                parents,
                file_changes: file_changes.into(),
                author: String::arbitrary(g),
                author_date: DateTime::arbitrary(g),
                committer: Option::<String>::arbitrary(g),
                committer_date: Option::<DateTime>::arbitrary(g),
                message: String::arbitrary(g),
                hg_extra: SortedVectorMap::arbitrary(g),
                git_extra_headers: None,
                is_snapshot: bool::arbitrary(g),
                git_tree_hash: None,
                git_annotated_tag: None,
            }
            .freeze()
            .expect("generated bonsai changeset must be valid")
        }
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        let cs = self.clone().inner;
        let iter = (
            cs.parents.clone(),
            cs.file_changes.clone(),
            cs.hg_extra.clone(),
            cs.git_tree_hash.clone(),
        )
            .shrink()
            .map(move |(parents, file_changes, hg_extra, git_tree_hash)| {
                BonsaiChangesetMut {
                    parents,
                    file_changes,
                    author: cs.author.clone(),
                    author_date: cs.author_date,
                    committer: cs.committer.clone(),
                    committer_date: cs.committer_date,
                    message: cs.message.clone(),
                    hg_extra,
                    git_extra_headers: cs.git_extra_headers.clone().map(|extra| {
                        extra
                            .into_iter()
                            .enumerate()
                            .filter_map(|(idx, val)| {
                                if idx % ARBITRARY_SHRINK_FACTOR == 0 {
                                    Some(val)
                                } else {
                                    None
                                }
                            })
                            .collect()
                    }),
                    is_snapshot: cs.is_snapshot,
                    git_tree_hash,
                    git_annotated_tag: cs.git_annotated_tag.clone(),
                }
                .freeze()
                .expect("shrunken bonsai changeset must be valid")
            });
        Box::new(iter)
    }
}

/// Target of an annotated tag imported from Git into Bonsai format.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum BonsaiAnnotatedTagTarget {
    /// Tag target as a Commit, Tree or yet another Tag.
    Changeset(ChangesetId),
    /// Tag target as raw file contents, i.e. Blob
    Content(ContentId),
}

impl BonsaiAnnotatedTagTarget {
    /// Create from a thrift `BonsaiAnnotatedTagTarget`.
    fn from_thrift(thrift_tag: thrift::BonsaiAnnotatedTagTarget) -> Result<Self> {
        match thrift_tag {
            thrift::BonsaiAnnotatedTagTarget::Changeset(id) => Ok(
                BonsaiAnnotatedTagTarget::Changeset(ChangesetId::from_thrift(id)?),
            ),
            thrift::BonsaiAnnotatedTagTarget::Content(id) => Ok(BonsaiAnnotatedTagTarget::Content(
                ContentId::from_thrift(id)?,
            )),
            thrift::BonsaiAnnotatedTagTarget::UnknownField(x) => bail!(ErrorKind::InvalidThrift(
                "BonsaiAnnotatedTagTarget".into(),
                format!("unknown bonsai annotated tag target field: {}", x)
            )),
        }
    }

    /// Convert into a thrift `BonsaiAnnotatedTagTarget`.
    fn into_thrift(self) -> thrift::BonsaiAnnotatedTagTarget {
        match self {
            BonsaiAnnotatedTagTarget::Changeset(id) => {
                thrift::BonsaiAnnotatedTagTarget::Changeset(id.into_thrift())
            }
            BonsaiAnnotatedTagTarget::Content(id) => {
                thrift::BonsaiAnnotatedTagTarget::Content(id.into_thrift())
            }
        }
    }
}

impl Arbitrary for BonsaiAnnotatedTagTarget {
    fn arbitrary(g: &mut Gen) -> Self {
        match u8::arbitrary(g) % 2 {
            0 => BonsaiAnnotatedTagTarget::Changeset(ChangesetId::arbitrary(g)),
            _ => BonsaiAnnotatedTagTarget::Content(ContentId::arbitrary(g)),
        }
    }
}

/// Bonsai counterpart of a git annotated tag. Used for referring to commits, tree, or other tags.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct BonsaiAnnotatedTag {
    // The target of the annotated tag
    pub target: BonsaiAnnotatedTagTarget,
    // The signature included with the tag
    pub pgp_signature: Option<Bytes>,
}

impl BonsaiAnnotatedTag {
    /// Create from a thrift `BonsaiAnnotatedTag`.
    fn from_thrift(thrift_tag: thrift::BonsaiAnnotatedTag) -> Result<Self> {
        Ok(Self {
            target: BonsaiAnnotatedTagTarget::from_thrift(thrift_tag.target)?,
            pgp_signature: thrift_tag.pgp_signature,
        })
    }

    /// Convert into a thrift `BonsaiAnnotatedTag`.
    fn into_thrift(self) -> thrift::BonsaiAnnotatedTag {
        thrift::BonsaiAnnotatedTag {
            target: self.target.into_thrift(),
            pgp_signature: self.pgp_signature,
        }
    }
}

impl Arbitrary for BonsaiAnnotatedTag {
    fn arbitrary(g: &mut Gen) -> Self {
        Self {
            target: BonsaiAnnotatedTagTarget::arbitrary(g),
            pgp_signature: Option::<Vec<u8>>::arbitrary(g).map(Bytes::from),
        }
    }
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use quickcheck::quickcheck;
    use sorted_vector_map::sorted_vector_map;

    use super::*;
    use crate::file_change::FileType;
    use crate::hash::Blake2;
    use crate::typed_hash::ContentId;

    quickcheck! {
        fn thrift_roundtrip(cs: BonsaiChangeset) -> bool {
            let thrift_cs = cs.inner.clone().into_thrift();
            let cs2 = BonsaiChangeset::from_thrift_with_id(thrift_cs, cs.id)
                .expect("thrift roundtrips should always be valid");
            cs == cs2
        }

        fn blob_roundtrip(cs: BonsaiChangeset) -> bool {
            let blob = cs.clone().into_blob();
            let cs2 = BonsaiChangeset::from_blob(blob)
                .expect("blob roundtrips should always be valid");
            cs == cs2
        }
    }

    #[test]
    fn fixed_blob() {
        let tc = BonsaiChangesetMut {
            parents: vec![ChangesetId::from_byte_array([3; 32])],
            author: "foo".into(),
            author_date: DateTime::from_timestamp(1234567890, 36800).unwrap(),
            committer: Some("bar".into()),
            committer_date: Some(DateTime::from_timestamp(1500000000, -36800).unwrap()),
            message: "Commit message".into(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: None,
            git_tree_hash: None,
            file_changes: sorted_vector_map![
                MPath::new("a/b").unwrap() => FileChange::tracked(
                    ContentId::from_byte_array([1; 32]),
                    FileType::Regular,
                    42,
                    None,
                ),
                MPath::new("c/d").unwrap() => FileChange::tracked(
                    ContentId::from_byte_array([2; 32]),
                    FileType::Executable,
                    84,
                    Some((
                        MPath::new("e/f").unwrap(),
                        ChangesetId::from_byte_array([3; 32]),
                    )),
                ),
                MPath::new("g/h").unwrap() => FileChange::Deletion,
                MPath::new("i/j").unwrap() => FileChange::Deletion,
            ],
            is_snapshot: false,
            git_annotated_tag: None,
        };
        let tc = tc.freeze().expect("fixed bonsai changeset must be valid");

        assert_eq!(
            tc.get_changeset_id(),
            ChangesetId::new(
                Blake2::from_str(
                    "189e67041363f9dc7d10de57aaf0fbd202dec989357e76cada7fa940936c712a"
                )
                .unwrap()
            )
        );
    }

    #[test]
    fn bonsai_snapshots() {
        fn create(untracked: bool, missing: bool, is_snapshot: bool) -> Result<BonsaiChangeset> {
            let mut file_changes = sorted_vector_map! [
                MPath::new("a").unwrap() => FileChange::tracked(
                    ContentId::from_byte_array([1; 32]),
                    FileType::Regular,
                    42,
                    None,
                ),
                MPath::new("b").unwrap() => FileChange::Deletion,
            ];
            if untracked {
                file_changes.insert(
                    MPath::new("c").unwrap(),
                    FileChange::untracked(
                        ContentId::from_byte_array([2; 32]),
                        FileType::Regular,
                        42,
                    ),
                );
            }
            if missing {
                file_changes.insert(MPath::new("d").unwrap(), FileChange::UntrackedDeletion);
            }
            BonsaiChangesetMut {
                parents: vec![],
                author: "foo".into(),
                author_date: DateTime::from_timestamp(1, 2).unwrap(),
                committer: None,
                committer_date: None,
                message: "a".into(),
                hg_extra: SortedVectorMap::new(),
                git_extra_headers: None,
                git_tree_hash: None,
                file_changes,
                is_snapshot,
                git_annotated_tag: None,
            }
            .freeze()
        }
        create(true, true, true).unwrap();
        create(false, false, false).unwrap();
        create(true, false, true).unwrap();
        create(true, false, false).expect_err("Non-snapshot can't have untracked");
        create(false, true, false).expect_err("Non-snapshot can't have missing");
        create(true, true, false).unwrap_err();
    }

    #[test]
    fn invalid_git_tree_bonsai_changeset() {
        let changeset = BonsaiChangesetMut {
            parents: vec![ChangesetId::from_byte_array([3; 32])],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: None,
            file_changes: SortedVectorMap::new(),
            is_snapshot: false,
            git_tree_hash: GitSha1::from_bytes([
                0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60,
                0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09,
            ])
            .ok(),
            git_annotated_tag: None,
        };
        changeset
            .freeze()
            .expect_err("Changeset representing Git trees can't have parents");

        let changeset = BonsaiChangesetMut {
            parents: vec![],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: None,
            file_changes: sorted_vector_map! [
                MPath::new("a").unwrap() => FileChange::tracked(
                    ContentId::from_byte_array([1; 32]),
                    FileType::Regular,
                    42,
                    None,
                ),
                MPath::new("b").unwrap() => FileChange::Deletion,
            ],
            is_snapshot: false,
            git_tree_hash: GitSha1::from_bytes([
                0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60,
                0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09,
            ])
            .ok(),
            git_annotated_tag: None,
        };
        changeset
            .freeze()
            .expect_err("Changeset representing Git trees can't have file changes");
    }

    #[test]
    fn valid_git_tree_bonsai_changeset() {
        let changeset = BonsaiChangesetMut {
            parents: vec![],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: None,
            file_changes: SortedVectorMap::new(),
            is_snapshot: false,
            git_tree_hash: GitSha1::from_bytes([
                0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60,
                0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09,
            ])
            .ok(),
            git_annotated_tag: None,
        };
        changeset.freeze().unwrap();
    }

    #[test]
    fn valid_git_extra_headers_bonsai_changeset() {
        let changeset = BonsaiChangesetMut {
            parents: vec![ChangesetId::from_byte_array([3; 32])],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: Some(sorted_vector_map! [
                SmallVec::from_vec(b"JustAHeader".to_vec()) => Bytes::from_static(b"Some Content")
            ]),
            file_changes: sorted_vector_map! [
                MPath::new("a").unwrap() => FileChange::tracked(
                    ContentId::from_byte_array([1; 32]),
                    FileType::Regular,
                    42,
                    None,
                ),
                MPath::new("b").unwrap() => FileChange::Deletion,
            ],
            is_snapshot: false,
            git_tree_hash: None,
            git_annotated_tag: None,
        };
        changeset.freeze().unwrap();
    }

    #[test]
    fn invalid_git_tag_bonsai_changeset() {
        let changeset = BonsaiChangesetMut {
            parents: vec![ChangesetId::from_byte_array([3; 32])],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: Some(sorted_vector_map! [
                SmallVec::from_vec(b"JustAHeader".to_vec()) => Bytes::from_static(b"Some Content")
            ]),
            file_changes: SortedVectorMap::new(),
            is_snapshot: false,
            git_tree_hash: None,
            git_annotated_tag: Some(BonsaiAnnotatedTag {
                target: BonsaiAnnotatedTagTarget::Changeset(ChangesetId::from_byte_array([4; 32])),
                pgp_signature: None,
            }),
        };
        changeset
            .freeze()
            .expect_err("Bonsai changeset representing a git annotated tag cannot have parents");

        let changeset = BonsaiChangesetMut {
            parents: vec![],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: Some(sorted_vector_map! [
                SmallVec::from_vec(b"JustAHeader".to_vec()) => Bytes::from_static(b"Some Content")
            ]),
            file_changes: sorted_vector_map! [
                MPath::new("a").unwrap() => FileChange::tracked(
                    ContentId::from_byte_array([1; 32]),
                    FileType::Regular,
                    42,
                    None,
                ),
                MPath::new("b").unwrap() => FileChange::Deletion,
            ],
            is_snapshot: false,
            git_tree_hash: None,
            git_annotated_tag: Some(BonsaiAnnotatedTag {
                target: BonsaiAnnotatedTagTarget::Changeset(ChangesetId::from_byte_array([3; 32])),
                pgp_signature: None,
            }),
        };
        changeset.freeze().expect_err(
            "Bonsai changeset representing a git annotated tag cannot have file changes",
        );

        let changeset = BonsaiChangesetMut {
            parents: vec![],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: None,
            file_changes: SortedVectorMap::new(),
            is_snapshot: false,
            git_tree_hash: GitSha1::from_bytes([
                0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60,
                0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09,
            ])
            .ok(),
            git_annotated_tag: Some(BonsaiAnnotatedTag {
                target: BonsaiAnnotatedTagTarget::Changeset(ChangesetId::from_byte_array([3; 32])),
                pgp_signature: None,
            }),
        };
        changeset.freeze().expect_err(
            "Bonsai changeset representing a git annotated tag cannot have a git tree hash (indicating that its a git tree) as part of it",
        );
    }

    #[test]
    fn valid_git_tag_bonsai_changeset() {
        let changeset = BonsaiChangesetMut {
            parents: vec![],
            author: String::new(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: String::new(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: None,
            file_changes: SortedVectorMap::new(),
            is_snapshot: false,
            git_tree_hash: None,
            git_annotated_tag: Some(BonsaiAnnotatedTag {
                target: BonsaiAnnotatedTagTarget::Changeset(ChangesetId::from_byte_array([3; 32])),
                pgp_signature: None,
            }),
        };
        changeset.freeze().unwrap();
    }

    #[test]
    fn valid_git_tag_bonsai_changeset_with_author_and_message() {
        let changeset = BonsaiChangesetMut {
            parents: vec![],
            author: "TagCreator".to_string(),
            author_date: DateTime::now(),
            committer: None,
            committer_date: None,
            message: "This is the tag description".to_string(),
            hg_extra: SortedVectorMap::new(),
            git_extra_headers: None,
            file_changes: SortedVectorMap::new(),
            is_snapshot: false,
            git_tree_hash: None,
            git_annotated_tag: Some(BonsaiAnnotatedTag {
                target: BonsaiAnnotatedTagTarget::Changeset(ChangesetId::from_byte_array([3; 32])),
                pgp_signature: None,
            }),
        };
        changeset.freeze().unwrap();
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fmt;
use std::fmt::Debug;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use bytes::Bytes;
use fbthrift::compact_protocol;
use quickcheck::single_shrinker;
use quickcheck::Arbitrary;
use quickcheck::Gen;

use crate::blob::Blob;
use crate::blob::BlobstoreValue;
use crate::blob::ContentChunkBlob;
use crate::errors::ErrorKind;
use crate::file_contents::ContentChunkPointer;
use crate::thrift;
use crate::typed_hash::ContentChunkId;
use crate::typed_hash::ContentChunkIdContext;

/// Chunk of a file's contents.
#[derive(Clone, Eq, PartialEq)]
pub struct ContentChunk(Bytes);

impl ContentChunk {
    pub fn new_bytes<B: Into<Bytes>>(b: B) -> Self {
        ContentChunk(b.into())
    }

    pub(crate) fn from_thrift(fc: thrift::ContentChunk) -> Result<Self> {
        match fc {
            thrift::ContentChunk::Bytes(bytes) => Ok(ContentChunk(bytes)),
            thrift::ContentChunk::UnknownField(x) => bail!(ErrorKind::InvalidThrift(
                "ContentChunk".into(),
                format!("unknown ContentChunk variant: {}", x)
            )),
        }
    }

    pub(crate) fn into_thrift(self) -> thrift::ContentChunk {
        thrift::ContentChunk::Bytes(self.0)
    }

    pub fn from_encoded_bytes(encoded_bytes: Bytes) -> Result<Self> {
        let thrift_tc = compact_protocol::deserialize(encoded_bytes)
            .with_context(|| ErrorKind::BlobDeserializeError("ContentChunk".into()))?;
        Self::from_thrift(thrift_tc)
    }

    pub fn size(&self) -> u64 {
        // NOTE: This panics if the buffer length doesn't fit a u64: that's fine.
        self.0.len().try_into().unwrap()
    }

    pub fn into_bytes(self) -> Bytes {
        self.0
    }
}

impl BlobstoreValue for ContentChunk {
    type Key = ContentChunkId;

    fn into_blob(self) -> ContentChunkBlob {
        let id = {
            let mut context = ContentChunkIdContext::new();
            context.update(&self.0);
            context.finish()
        };

        let data = compact_protocol::serialize(&self.into_thrift());

        Blob::new(id, data)
    }

    fn from_blob(blob: ContentChunkBlob) -> Result<Self> {
        let thrift_tc = compact_protocol::deserialize(blob.data())
            .with_context(|| ErrorKind::BlobDeserializeError("ContentChunk".into()))?;
        Self::from_thrift(thrift_tc)
    }
}

impl Debug for ContentChunk {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "ContentChunk(length {})", self.0.len())
    }
}

impl Arbitrary for ContentChunk {
    fn arbitrary(g: &mut Gen) -> Self {
        ContentChunk::new_bytes(Vec::arbitrary(g))
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        single_shrinker(ContentChunk::new_bytes(vec![]))
    }
}

pub fn new_blob_and_pointer<B: Into<Bytes>>(bytes: B) -> (ContentChunkBlob, ContentChunkPointer) {
    let chunk = ContentChunk::new_bytes(bytes);
    let size = chunk.size();

    let blob = chunk.into_blob();
    let id = blob.id();

    let pointer = ContentChunkPointer::new(*id, size);

    (blob, pointer)
}

#[cfg(test)]
mod test {
    use quickcheck::quickcheck;

    use super::*;

    quickcheck! {
        fn file_contents_thrift_roundtrip(fc: ContentChunk) -> bool {
            let thrift_fc = fc.clone().into_thrift();
            let fc2 = ContentChunk::from_thrift(thrift_fc)
                .expect("thrift roundtrips should always be valid");
            fc == fc2
        }

        fn file_contents_blob_roundtrip(fc: ContentChunk) -> bool {
            let blob = fc.clone().into_blob();
            let fc2 = ContentChunk::from_blob(blob)
                .expect("blob roundtrips should always be valid");
            fc == fc2
        }

        fn test_blob_and_pointer_consistency(bytes: Vec<u8>) -> bool {
            let (blob, pointer) = new_blob_and_pointer(bytes);
            let blob_id = *blob.id();
            let chunk = ContentChunk::from_blob(blob)
                .expect("blob roundtrips should always be valid");
             blob_id == pointer.chunk_id() && chunk.size() == pointer.size()
        }
    }

    #[test]
    fn bad_thrift() {
        let thrift_fc = thrift::ContentChunk::UnknownField(-1);
        ContentChunk::from_thrift(thrift_fc).expect_err("unexpected OK - unknown field");
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fmt;
use std::fmt::Display;
use std::str::FromStr;

use abomonation_derive::Abomonation;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use chrono::DateTime as ChronoDateTime;
use chrono::Duration as ChronoDuration;
use chrono::FixedOffset;
use chrono::Local;
use chrono::LocalResult;
use chrono::NaiveDateTime;
use chrono::TimeZone;
use chrono_english::parse_date_string;
use chrono_english::Dialect;
use quickcheck::empty_shrinker;
use quickcheck::Arbitrary;
use quickcheck::Gen;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use sql::mysql;

use crate::errors::ErrorKind;
use crate::thrift;

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    Ord,
    Deserialize,
    Serialize,
    PartialEq,
    PartialOrd
)]
pub struct DateTime(ChronoDateTime<FixedOffset>);

impl DateTime {
    #[inline]
    pub fn new(dt: ChronoDateTime<FixedOffset>) -> Self {
        DateTime(dt)
    }

    pub fn now() -> Self {
        let now = Local::now();
        DateTime(now.with_timezone(now.offset()))
    }

    pub fn from_timestamp(secs: i64, tz_offset_secs: i32) -> Result<Self> {
        let tz = FixedOffset::west_opt(tz_offset_secs).ok_or_else(|| {
            ErrorKind::InvalidDateTime(format!("timezone offset out of range: {}", tz_offset_secs))
        })?;
        let dt = match tz.timestamp_opt(secs, 0) {
            LocalResult::Single(dt) => dt,
            _ => bail!(ErrorKind::InvalidDateTime(format!(
                "seconds out of range: {}",
                secs
            ))),
        };
        Ok(Self::new(dt))
    }

    /// Construct a new `DateTime` from an RFC3339 string.
    ///
    /// RFC3339 is a standardized way to represent a specific moment in time. See
    /// <https://tools.ietf.org/html/rfc3339>.
    pub fn from_rfc3339(rfc3339: &str) -> Result<Self> {
        let dt = ChronoDateTime::parse_from_rfc3339(rfc3339)
            .with_context(|| ErrorKind::InvalidDateTime("while parsing rfc3339".into()))?;
        Ok(Self::new(dt))
    }

    pub fn from_thrift(dt: thrift::DateTime) -> Result<Self> {
        Self::from_timestamp(dt.timestamp_secs, dt.tz_offset_secs)
    }

    /// Retrieves the Unix timestamp in UTC.
    #[inline]
    pub fn timestamp_secs(&self) -> i64 {
        self.0.timestamp()
    }

    /// Retrieves the timezone offset, as represented by the number of seconds to
    /// add to convert local time to UTC.
    #[inline]
    pub fn tz_offset_secs(&self) -> i32 {
        // This is the same as the way Mercurial stores timezone offsets.
        self.0.offset().utc_minus_local()
    }

    #[inline]
    pub fn tz_offset_minutes(&self) -> i16 {
        (self.tz_offset_secs() / 60) as i16
    }

    #[inline]
    pub fn as_chrono(&self) -> &ChronoDateTime<FixedOffset> {
        &self.0
    }

    #[inline]
    pub fn into_chrono(self) -> ChronoDateTime<FixedOffset> {
        self.0
    }

    pub fn into_thrift(self) -> thrift::DateTime {
        thrift::DateTime {
            timestamp_secs: self.timestamp_secs(),
            tz_offset_secs: self.tz_offset_secs(),
        }
    }
}

impl From<DateTime> for ChronoDateTime<FixedOffset> {
    fn from(d: DateTime) -> Self {
        d.into_chrono()
    }
}

impl std::ops::Add<ChronoDuration> for DateTime {
    type Output = DateTime;

    #[inline]
    fn add(self, rhs: ChronoDuration) -> DateTime {
        DateTime::new(self.into_chrono() + rhs)
    }
}

impl std::ops::Sub<ChronoDuration> for DateTime {
    type Output = DateTime;

    #[inline]
    fn sub(self, rhs: ChronoDuration) -> DateTime {
        DateTime::new(self.into_chrono() - rhs)
    }
}

impl Display for DateTime {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{}", self.0)
    }
}

impl Arbitrary for DateTime {
    fn arbitrary(g: &mut Gen) -> Self {
        // Ensure a large domain from which to get second values.
        let secs = i32::arbitrary(g) as i64;
        // Timezone offsets in the range [-86399, 86399] (both inclusive) are valid.
        // gen_range generates a value in the range [low, high).
        let tz_offset_secs = (u64::arbitrary(g) % 172_799) as i32 - 86_399;
        DateTime::from_timestamp(secs, tz_offset_secs)
            .expect("Arbitrary instances should always be valid")
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        empty_shrinker()
    }
}

impl FromStr for DateTime {
    type Err = chrono_english::DateError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let now = DateTime::now().into_chrono();
        Ok(DateTime::new(parse_date_string(s, now, Dialect::Us)?))
    }
}

const MS_IN_NS: i64 = 1_000_000;
const SEC_IN_NS: i64 = 1_000_000_000;

/// Number of non-leap-nanoseconds since January 1, 1970 UTC
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[derive(Deserialize, Serialize, mysql::OptTryFromRowField, Abomonation)]
pub struct Timestamp(i64);

impl Timestamp {
    pub fn now() -> Self {
        DateTime::now().into()
    }

    pub fn from_timestamp_secs(ts: i64) -> Self {
        Timestamp(ts * SEC_IN_NS)
    }

    pub fn from_timestamp_nanos(ts: i64) -> Self {
        Timestamp(ts)
    }

    pub fn timestamp_nanos(&self) -> i64 {
        self.0
    }

    pub fn timestamp_seconds(&self) -> i64 {
        self.0 / SEC_IN_NS
    }

    pub fn since_nanos(&self) -> i64 {
        let now = Self::now().timestamp_nanos();
        now - self.0
    }

    pub fn since_millis(&self) -> i64 {
        self.since_nanos() / MS_IN_NS
    }

    pub fn since_seconds(&self) -> i64 {
        self.since_nanos() / SEC_IN_NS
    }
}

impl From<DateTime> for Timestamp {
    fn from(dt: DateTime) -> Self {
        Timestamp(dt.0.timestamp_nanos())
    }
}

impl From<Timestamp> for DateTime {
    fn from(ts: Timestamp) -> Self {
        let ts_secs = ts.timestamp_seconds();
        let ts_nsecs = (ts.0 % SEC_IN_NS) as u32;
        DateTime::new(ChronoDateTime::<FixedOffset>::from_utc(
            NaiveDateTime::from_timestamp_opt(ts_secs, ts_nsecs).unwrap(),
            FixedOffset::west_opt(0).unwrap(),
        ))
    }
}

#[cfg(test)]
mod test {
    use quickcheck::quickcheck;

    use super::*;

    quickcheck! {
        fn thrift_roundtrip(dt: DateTime) -> bool {
            let thrift_dt = dt.into_thrift();
            let dt2 = DateTime::from_thrift(thrift_dt)
                .expect("roundtrip instances should always be valid");
            // Equality on DateTime structs doesn't pay attention to the time zone,
            // in order to be consistent with Ord.
            dt == dt2 && dt.tz_offset_secs() == dt2.tz_offset_secs()
        }
    }

    #[test]
    fn rfc3339() {
        // Valid RFC3339 strings.
        DateTime::from_rfc3339("2018-01-01T00:00:00Z").expect("unexpected err - UTC");
        DateTime::from_rfc3339("2018-01-01T00:00:00+04:00").expect("unexpected err - +04:00");
        DateTime::from_rfc3339("2018-01-01T00:00:00-04:00").expect("unexpected err - -04:00");
        DateTime::from_rfc3339("2018-01-01T01:02:03.04+05:45").expect("unexpected err - subsecond");

        // Missing information.
        DateTime::from_rfc3339("2018-01-01").expect_err("unexpected Ok - no time");
        DateTime::from_rfc3339("12:23:36").expect_err("unexpected Ok - no date");
        DateTime::from_rfc3339("2018-01-01T12:23").expect_err("unexpected Ok - no seconds");
        DateTime::from_rfc3339("2018-01-01T12:23:36").expect_err("unexpected Ok - no timezone");
    }

    #[test]
    fn bad_inputs() {
        DateTime::from_timestamp(0, 86_400)
            .expect_err("unexpected OK - tz_offset_secs out of bounds");
        DateTime::from_timestamp(0, -86_400)
            .expect_err("unexpected OK - tz_offset_secs out of bounds");
        DateTime::from_timestamp(i64::min_value(), 0)
            .expect_err("unexpected OK - timestamp_secs out of bounds");
        DateTime::from_timestamp(i64::max_value(), 0)
            .expect_err("unexpected OK - timestamp_secs out of bounds");
    }

    #[test]
    fn bad_thrift() {
        DateTime::from_thrift(thrift::DateTime {
            timestamp_secs: 0,
            tz_offset_secs: 86_400,
        })
        .expect_err("unexpected OK - tz_offset_secs out of bounds");
        DateTime::from_thrift(thrift::DateTime {
            timestamp_secs: 0,
            tz_offset_secs: -86_400,
        })
        .expect_err("unexpected OK - tz_offset_secs out of bounds");
        DateTime::from_thrift(thrift::DateTime {
            timestamp_secs: i64::min_value(),
            tz_offset_secs: 0,
        })
        .expect_err("unexpected OK - timestamp_secs out of bounds");
        DateTime::from_thrift(thrift::DateTime {
            timestamp_secs: i64::max_value(),
            tz_offset_secs: 0,
        })
        .expect_err("unexpected OK - timestamp_secs out of bounds");
    }

    #[test]
    fn timestamp_round_trip() {
        let ts0 = Timestamp::now();
        let dt0: DateTime = ts0.into();
        let ts1: Timestamp = dt0.into();
        let dt1: DateTime = ts1.into();
        assert_eq!(ts0, ts1);
        assert_eq!(dt0, dt1);
    }

    #[test]
    fn seconds() {
        let ts0 = Timestamp::from_timestamp_nanos(SEC_IN_NS);
        let ts1 = Timestamp::from_timestamp_secs(1);
        assert_eq!(ts0, ts1);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;
use std::fmt::Debug;

use anyhow::Result;
use blobstore::Blobstore;
use blobstore::Loadable;
use context::CoreContext;
use futures::stream::BoxStream;

use crate::blob::BlobstoreValue;
use crate::ChangesetId;
use crate::MPathElement;
use crate::MononokeId;

/// This trait has common behaviour that should be shared among all versions
/// of deleted manifest, and should be used to generalize usage of them.
#[async_trait::async_trait]
pub trait DeletedManifestCommon:
    BlobstoreValue<Key = Self::Id> + Debug + Clone + Send + Sync + 'static
{
    type Id: MononokeId<Value = Self> + Loadable<Value = Self>;

    /// Create a new deleted manifest by copying subentries from `current` and then
    /// adding the subentries from `subentries_to_add` (where `None` means "remove")
    async fn copy_and_update_subentries(
        ctx: &CoreContext,
        blobstore: &impl Blobstore,
        current: Option<Self>,
        linknode: Option<ChangesetId>,
        subentries_to_add: BTreeMap<MPathElement, Option<Self::Id>>,
    ) -> Result<Self>;

    /// Lookup a specific subentry on this manifest.
    async fn lookup(
        &self,
        ctx: &CoreContext,
        blobstore: &impl Blobstore,
        basename: &MPathElement,
    ) -> Result<Option<Self::Id>>;

    /// List all subentries on this manifest. Use with care, some manifests can
    /// have hundreds of thousands of subentries.
    fn into_subentries<'a>(
        self,
        ctx: &'a CoreContext,
        blobstore: &'a impl Blobstore,
    ) -> BoxStream<'a, Result<(MPathElement, Self::Id)>>;

    /// Returns whether this node has no subentries.
    fn is_empty(&self) -> bool;

    /// Whether the file/directory represented by this manifest node currently exists
    /// in the changeset.
    fn is_deleted(&self) -> bool {
        self.linknode().is_some()
    }

    /// Calculate id of manifest object.
    fn id(&self) -> Self::Id;

    /// Last changeset where the this node was deleted
    fn linknode(&self) -> Option<&ChangesetId>;
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;

use anyhow::Result;
use blobstore::Blobstore;
use blobstore::Storable;
use bytes::Bytes;
use context::CoreContext;
use futures::stream::BoxStream;
use futures::StreamExt;
use futures::TryStreamExt;

use crate::blob::Blob;
use crate::blob::BlobstoreValue;
use crate::blob::DeletedManifestV2Blob;
use crate::deleted_manifest_common::DeletedManifestCommon;
use crate::sharded_map::MapValue;
use crate::sharded_map::ShardedMapNode;
use crate::sharded_map::ShardedTraversalOutput;
use crate::thrift;
use crate::typed_hash::BlobstoreKey;
use crate::typed_hash::ChangesetId;
use crate::typed_hash::DeletedManifestV2Context;
use crate::typed_hash::DeletedManifestV2Id;
use crate::typed_hash::IdContext;
pub use crate::typed_hash::ShardedMapNodeDMv2Id;
use crate::MPathElement;
use crate::ThriftConvert;

/// Deleted Manifest is a data structure that tracks deleted files and commits where they
/// were deleted. This manifest was designed in addition to Unodes to make following file history
/// across deletions possible.
///
/// Both directories and files are represented by the same data structure, which consists of:
/// * optional<linknode>: if set, a changeset where this path was deleted
/// * subentries: a map from base name to the deleted manifest for this path
/// Even though the manifest tracks only deleted paths, it will still have entries for the
/// existing directories where files were deleted. Optional field `linknode` indicates whether the
/// path still exists (not set) or it was deleted.
///
/// Q&A
///
/// Why the manifest has same data structure for files and directories?
///
/// Deleted manifest doesn't differ files from directories, because any file path can be
/// reincarnated after the deletion as a directory and vice versa. The manifest doesn't need
/// to know whether the path is a directory or a file, the only important information is "if the
/// path was deleted, which changeset did it?"
///
/// Why we don't keep a path_hash even though it provides uniqueness of entries?
///
/// The deleted manifest entry doesn't have a path_hash, that means the entries are identical
/// for different files deleted in the same commit. This is fine as soon as we don't care about the
/// the uniqueness, but about the fact that the files were deleted. So directory entry will have
/// links to the same entry for different deleted files.
/// However, if one of such files is recreated as a directory, we anyway create a new entry for it:
/// manifest entries are immutable.
///
/// How we derive deleted manifest?
///
/// Assuming we have a computed deleted manifests for all the current commits, for a new
/// changeset:
/// 1. For each deleted file create a new manifest entry with a linknode to the changeset, where
/// file was deleted.
/// 2. For each recreated file remove this file from the manifest.
/// 3. Remove directory manifest if it still exists and donâ€™t have deleted children anymore.
/// 4. Create new manifest nodes recursively.
/// 5. Finalize the conversion by recording the mapping from changeset id to the root deleted
/// files manifest hash.
///
/// Where does point a linknode for a file that was markes as deleted in a merge commit?
///
/// Linknode will point to the merge commit itself, if the file deletion was made in some of the
/// parents and was accepted in merge commit.
///
/// How do we perform tracking file history across deletions?
///
/// Assume we have a commit graph, where:
/// * - file was deleted
/// o - file exists
///
///   o A
///   |
///   * B
///  / \
/// *   * C
/// |   |
/// : D o E
/// |   |
/// o F :
///
/// 1. Check deleted manifest from node B: the file was deleted with linknode to B.
/// 2. Need to consider all ancestors of B:
///    * check unodes for the parents: if the file exists,
///        traverse the history
///    * if not, check the deleted manifest whether the file
///        existed and was deleted
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DeletedManifestV2 {
    linknode: Option<ChangesetId>,
    pub subentries: ShardedMapNode<DeletedManifestV2Id>,
}

impl MapValue for DeletedManifestV2Id {
    type Id = crate::typed_hash::ShardedMapNodeDMv2Id;
    type Context = crate::typed_hash::ShardedMapNodeDMv2Context;
}

#[async_trait::async_trait]
impl DeletedManifestCommon for DeletedManifestV2 {
    type Id = DeletedManifestV2Id;

    async fn copy_and_update_subentries(
        ctx: &CoreContext,
        blobstore: &impl Blobstore,
        current: Option<Self>,
        linknode: Option<ChangesetId>,
        subentries_to_update: BTreeMap<MPathElement, Option<Self::Id>>,
    ) -> Result<Self> {
        let subentries = current.map(|mf| mf.subentries).unwrap_or_default();
        let subentries = subentries
            .update(
                ctx,
                blobstore,
                subentries_to_update
                    .into_iter()
                    .map(|(k, v)| (Bytes::copy_from_slice(k.as_ref()), v))
                    .collect(),
                |_| (),
            )
            .await?;
        Ok(Self::new(linknode, subentries))
    }

    fn linknode(&self) -> Option<&ChangesetId> {
        self.linknode.as_ref()
    }

    fn is_empty(&self) -> bool {
        self.subentries.is_empty()
    }

    fn id(&self) -> Self::Id {
        *self.clone().into_blob().id()
    }

    async fn lookup(
        &self,
        ctx: &CoreContext,
        blobstore: &impl Blobstore,
        basename: &MPathElement,
    ) -> Result<Option<Self::Id>> {
        self.subentries
            .lookup(ctx, blobstore, basename.as_ref())
            .await
    }

    fn into_subentries<'a>(
        self,
        ctx: &'a CoreContext,
        blobstore: &'a impl Blobstore,
    ) -> BoxStream<'a, Result<(MPathElement, Self::Id)>> {
        self.subentries
            .into_entries(ctx, blobstore)
            .and_then(|(k, v)| async move { anyhow::Ok((MPathElement::from_smallvec(k)?, v)) })
            .boxed()
    }
}

impl ThriftConvert for DeletedManifestV2 {
    const NAME: &'static str = "DeletedManifestV2";

    type Thrift = thrift::DeletedManifestV2;
    fn from_thrift(t: thrift::DeletedManifestV2) -> Result<DeletedManifestV2> {
        Ok(Self {
            linknode: t.linknode.map(ChangesetId::from_thrift).transpose()?,
            subentries: ShardedMapNode::from_thrift(t.subentries)?,
        })
    }

    fn into_thrift(self) -> thrift::DeletedManifestV2 {
        thrift::DeletedManifestV2 {
            linknode: self.linknode.map(ChangesetId::into_thrift),
            subentries: self.subentries.into_thrift(),
        }
    }
}

impl DeletedManifestV2 {
    pub fn new(
        linknode: Option<ChangesetId>,
        subentries: ShardedMapNode<DeletedManifestV2Id>,
    ) -> Self {
        Self {
            linknode,
            subentries,
        }
    }
    pub fn into_sharded_subentries<'a>(
        self,
        ctx: &'a CoreContext,
        blobstore: &'a impl Blobstore,
    ) -> BoxStream<'a, Result<ShardedTraversalOutput<'a, DeletedManifestV2Id>>> {
        self.subentries.into_sharded_entries(ctx, blobstore).boxed()
    }
}

impl BlobstoreValue for DeletedManifestV2 {
    type Key = DeletedManifestV2Id;

    fn into_blob(self) -> DeletedManifestV2Blob {
        let data = self.into_bytes();
        let id = DeletedManifestV2Context::id_from_data(&data);
        Blob::new(id, data)
    }

    fn from_blob(blob: Blob<Self::Key>) -> Result<Self> {
        Self::from_bytes(blob.data())
    }
}

#[async_trait::async_trait]
impl Storable for DeletedManifestV2 {
    type Key = DeletedManifestV2Id;

    async fn store<'a, B: Blobstore>(
        self,
        ctx: &'a CoreContext,
        blobstore: &'a B,
    ) -> Result<Self::Key> {
        let blob = self.into_blob();
        let id = blob.id().clone();
        blobstore.put(ctx, id.blobstore_key(), blob.into()).await?;
        Ok(id)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use thiserror::Error;

use crate::path::MPath;

#[derive(Debug, Error)]
pub enum ErrorKind {
    #[error("invalid blake2 input: {0}")]
    InvalidBlake2Input(String),
    #[error("invalid sha1 input: {0}")]
    InvalidSha1Input(String),
    #[error("invalid sha256 input: {0}")]
    InvalidSha256Input(String),
    #[error("invalid git sha1 input: {0}")]
    InvalidGitSha1Input(String),
    #[error("invalid path '{0}': {1}")]
    InvalidPath(String, String),
    #[error("invalid Mononoke path '{0}': {1}")]
    InvalidMPath(MPath, String),
    #[error("error while deserializing blob for '{0}'")]
    BlobDeserializeError(String),
    #[error("error for key '{0}'")]
    BlobKeyError(String),
    #[error("invalid Thrift structure '{0}': {1}")]
    InvalidThrift(String, String),
    #[error("invalid changeset date: {0}")]
    InvalidDateTime(String),
    #[error("not path-conflict-free: changed path '{0}' is a prefix of '{1}'")]
    NotPathConflictFree(MPath, MPath),
    #[error("invalid bonsai changeset: {0}")]
    InvalidBonsaiChangeset(String),
    #[error("Failed to parse RepositoryId from '{0}'")]
    FailedToParseRepositoryId(String),
    #[error("invalid blake3 input: {0}")]
    InvalidBlake3Input(String),
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::VecDeque;

use anyhow::Context;
use anyhow::Result;
use blobstore::Blobstore;
use blobstore::Loadable;
use blobstore::Storable;
use bytes::Bytes;
use context::CoreContext;
use fbthrift::compact_protocol;
use futures::future::try_join_all;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use itertools::Itertools;

use crate::blob::Blob;
use crate::blob::BlobstoreValue;
use crate::blob::FastlogBatchBlob;
use crate::errors::ErrorKind;
use crate::thrift;
use crate::typed_hash::ChangesetId;
use crate::typed_hash::FastlogBatchId;
use crate::typed_hash::FastlogBatchIdContext;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParentOffset(i32);

impl ParentOffset {
    pub fn new(offset: i32) -> Self {
        Self(offset)
    }

    pub fn num(&self) -> i32 {
        self.0
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FastlogBatch {
    latest: VecDeque<(ChangesetId, Vec<ParentOffset>)>,
    previous_batches: VecDeque<FastlogBatchId>,
}

pub const MAX_LATEST_LEN: usize = 10;
pub const MAX_BATCHES: usize = 10;

pub fn max_entries_in_fastlog_batch() -> usize {
    MAX_BATCHES * MAX_LATEST_LEN + MAX_LATEST_LEN
}

impl FastlogBatch {
    pub async fn new_from_raw_list<'a, B: Blobstore>(
        ctx: &'a CoreContext,
        blobstore: &'a B,
        raw_list: impl IntoIterator<Item = (ChangesetId, Vec<ParentOffset>)>,
    ) -> Result<FastlogBatch> {
        let chunks = raw_list
            .into_iter()
            .take(max_entries_in_fastlog_batch())
            .chunks(MAX_LATEST_LEN);
        let chunks: Vec<_> = chunks.into_iter().map(VecDeque::from_iter).collect();
        let mut chunks = chunks.into_iter();
        let latest = chunks.next().unwrap_or_default();

        let previous_batches = VecDeque::from(
            try_join_all(chunks.map(move |chunk| {
                FastlogBatch::new(VecDeque::from_iter(chunk), VecDeque::new())
                    .into_blob()
                    .store(ctx, blobstore)
            }))
            .await?,
        );

        Ok(FastlogBatch {
            latest,
            previous_batches,
        })
    }

    // Prepending a child with a single parent is a special case - we only need to prepend one entry
    // with ParentOffset(1).
    pub async fn prepend_child_with_single_parent<'a, B: Blobstore>(
        &'a self,
        ctx: &'a CoreContext,
        blobstore: &'a B,
        cs_id: ChangesetId,
    ) -> Result<FastlogBatch> {
        let mut new_batch = self.clone();
        if new_batch.latest.len() >= MAX_LATEST_LEN {
            let previous_latest = std::mem::take(&mut new_batch.latest);
            let new_previous_batch = FastlogBatch::new(previous_latest, VecDeque::new());
            let new_batch_id = new_previous_batch.into_blob().store(ctx, blobstore).await?;
            if new_batch.previous_batches.len() >= MAX_BATCHES {
                new_batch.previous_batches.pop_back();
            }
            new_batch.latest.push_front((cs_id, vec![ParentOffset(1)]));
            new_batch.previous_batches.push_front(new_batch_id);
            Ok(new_batch)
        } else {
            new_batch.latest.push_front((cs_id, vec![ParentOffset(1)]));
            Ok(new_batch)
        }
    }

    pub fn fetch_raw_list<'a, B: Blobstore>(
        &'a self,
        ctx: &'a CoreContext,
        blobstore: &'a B,
    ) -> BoxFuture<'a, Result<Vec<(ChangesetId, Vec<ParentOffset>)>>> {
        let mut v = vec![];
        for p in &self.previous_batches {
            v.push(async move {
                let p_load = p.load(ctx, blobstore);
                let full_batch = p_load.await?;
                full_batch.fetch_raw_list(ctx, blobstore).await
            });
        }

        async move {
            let mut res = vec![];
            res.extend(self.latest.clone());
            let previous_batches = try_join_all(v).await;
            for p in previous_batches? {
                res.extend(p);
            }
            Ok(res)
        }
        .boxed()
    }

    pub fn latest(&self) -> &VecDeque<(ChangesetId, Vec<ParentOffset>)> {
        &self.latest
    }

    pub fn previous_batches(&self) -> &VecDeque<FastlogBatchId> {
        &self.previous_batches
    }

    pub fn from_bytes(serialized: &Bytes) -> Result<FastlogBatch> {
        let thrift_entry: Result<thrift::FastlogBatch> = compact_protocol::deserialize(serialized)
            .map_err(|err| ErrorKind::BlobDeserializeError(format!("{}", err)).into());
        thrift_entry.and_then(Self::from_thrift)
    }

    pub fn from_thrift(th: thrift::FastlogBatch) -> Result<FastlogBatch> {
        let latest: Result<VecDeque<_>> = th
            .latest
            .into_iter()
            .map(|hash_and_parents| {
                let cs_id = ChangesetId::from_thrift(hash_and_parents.cs_id);
                let offsets = hash_and_parents
                    .parent_offsets
                    .into_iter()
                    .map(|p| ParentOffset(p.0))
                    .collect();
                cs_id.map(|cs_id| (cs_id, offsets))
            })
            .collect();
        let latest = latest?;

        let previous_batches: Result<VecDeque<_>> = th
            .previous_batches
            .into_iter()
            .map(FastlogBatchId::from_thrift)
            .collect();

        let previous_batches = previous_batches?;
        Ok(FastlogBatch {
            latest,
            previous_batches,
        })
    }

    pub fn into_bytes(self) -> Bytes {
        compact_protocol::serialize(&self.into_thrift())
    }

    pub fn into_thrift(self) -> thrift::FastlogBatch {
        let latest_thrift = self
            .latest
            .into_iter()
            .map(|(cs_id, offsets)| {
                let parent_offsets = offsets
                    .into_iter()
                    .map(|offset| thrift::ParentOffset(offset.0))
                    .collect();

                thrift::CompressedHashAndParents {
                    cs_id: cs_id.into_thrift(),
                    parent_offsets,
                }
            })
            .collect();

        let previous_batches = self
            .previous_batches
            .into_iter()
            .map(|previous_batch| previous_batch.into_thrift())
            .collect();
        thrift::FastlogBatch {
            latest: latest_thrift,
            previous_batches,
        }
    }

    fn new(
        latest: VecDeque<(ChangesetId, Vec<ParentOffset>)>,
        previous_batches: VecDeque<FastlogBatchId>,
    ) -> Self {
        Self {
            latest,
            previous_batches,
        }
    }
}

impl BlobstoreValue for FastlogBatch {
    type Key = FastlogBatchId;

    fn into_blob(self) -> FastlogBatchBlob {
        let thrift = self.into_thrift();
        let data = compact_protocol::serialize(&thrift);
        let mut context = FastlogBatchIdContext::new();
        context.update(&data);
        let id = context.finish();
        Blob::new(id, data)
    }

    fn from_blob(blob: FastlogBatchBlob) -> Result<Self> {
        let thrift_tc = compact_protocol::deserialize(blob.data().as_ref())
            .with_context(|| ErrorKind::BlobDeserializeError("FastlogBatch".into()))?;
        Self::from_thrift(thrift_tc)
    }
}

#[cfg(test)]
mod test {
    use std::sync::Arc;

    use borrowed::borrowed;
    use context::CoreContext;
    use fbinit::FacebookInit;
    use memblob::Memblob;
    use pretty_assertions::assert_eq;
    use quickcheck::TestResult;

    use super::*;
    use crate::hash::Blake2;

    #[fbinit::test]
    async fn test_fastlog_batch_empty(fb: FacebookInit) -> Result<()> {
        let blobstore = Arc::new(Memblob::default());
        let ctx = CoreContext::test_mock(fb);
        borrowed!(ctx, blobstore: &Arc<_>);

        let list = VecDeque::new();
        let batch = FastlogBatch::new_from_raw_list(ctx, blobstore, list).await?;
        assert!(batch.latest.is_empty());
        assert!(batch.previous_batches.is_empty());

        Ok(())
    }

    #[fbinit::test]
    async fn test_fastlog_batch_single(fb: FacebookInit) -> Result<()> {
        let blobstore = Arc::new(Memblob::default());
        let ctx = CoreContext::test_mock(fb);
        borrowed!(ctx, blobstore: &Arc<_>);

        let mut list = VecDeque::new();
        let csid = ChangesetId::new(Blake2::from_byte_array([1; 32]));
        list.push_back((csid, vec![]));
        let batch = FastlogBatch::new_from_raw_list(ctx, blobstore, list.clone()).await?;
        assert_eq!(batch.latest, list);
        assert!(batch.previous_batches.is_empty());

        Ok(())
    }

    #[fbinit::test]
    async fn test_fastlog_batch_large(fb: FacebookInit) -> Result<()> {
        let blobstore = Arc::new(Memblob::default());
        let ctx = CoreContext::test_mock(fb);
        borrowed!(ctx, blobstore: &Arc<_>);

        let mut list = VecDeque::new();
        for i in 0..max_entries_in_fastlog_batch() {
            let csid = ChangesetId::new(Blake2::from_byte_array([i as u8; 32]));
            list.push_back((csid, vec![ParentOffset(1)]));
        }

        let batch = FastlogBatch::new_from_raw_list(ctx, blobstore, list.clone()).await?;
        assert_eq!(batch.latest.len(), MAX_LATEST_LEN);
        assert_eq!(batch.previous_batches.len(), MAX_BATCHES);

        let fetched_list = batch.fetch_raw_list(ctx, blobstore).await?;

        assert_eq!(fetched_list, Vec::from(list));
        Ok(())
    }

    #[fbinit::test]
    async fn test_fastlog_batch_overflow(fb: FacebookInit) -> Result<()> {
        let blobstore = Arc::new(Memblob::default());
        let ctx = CoreContext::test_mock(fb);
        borrowed!(ctx, blobstore: &Arc<_>);

        let mut list = VecDeque::new();
        for i in 0..max_entries_in_fastlog_batch() + 1 {
            let csid = ChangesetId::new(Blake2::from_byte_array([i as u8; 32]));
            list.push_back((csid, vec![ParentOffset(1)]));
        }

        let batch = FastlogBatch::new_from_raw_list(ctx, blobstore, list.clone()).await?;
        assert_eq!(batch.latest.len(), MAX_LATEST_LEN);
        assert_eq!(batch.previous_batches.len(), MAX_BATCHES);

        let fetched_list = batch.fetch_raw_list(ctx, blobstore).await?;

        list.pop_back();
        assert_eq!(fetched_list, Vec::from(list));
        Ok(())
    }

    #[quickcheck_async::tokio]
    async fn fastlog_roundtrip(fb: FacebookInit, hashes: Vec<(ChangesetId, i32)>) -> TestResult {
        let blobstore = Arc::new(Memblob::default());
        let ctx = CoreContext::test_mock(fb);
        borrowed!(ctx, blobstore: &Arc<_>);

        let mut raw_list = VecDeque::new();
        for (cs_id, offset) in hashes {
            raw_list.push_back((cs_id, vec![ParentOffset(offset)]));
        }

        let batch = FastlogBatch::new_from_raw_list(ctx, blobstore, raw_list.clone())
            .await
            .unwrap();

        if batch.latest.len() > MAX_LATEST_LEN {
            return TestResult::from_bool(false);
        }
        if batch.previous_batches.len() > MAX_BATCHES {
            return TestResult::from_bool(false);
        }

        raw_list.truncate(max_entries_in_fastlog_batch());
        let actual = batch.fetch_raw_list(ctx, blobstore).await.unwrap();

        TestResult::from_bool(raw_list == actual)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fmt;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use edenapi_types::FileType as EdenapiFileType;
use quickcheck::empty_shrinker;
use quickcheck::single_shrinker;
use quickcheck::Arbitrary;
use quickcheck::Gen;
use serde_derive::Deserialize;
use serde_derive::Serialize;

use crate::errors::ErrorKind;
use crate::path::MPath;
use crate::thrift;
use crate::typed_hash::ChangesetId;
use crate::typed_hash::ContentId;

#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct TrackedFileChange {
    inner: BasicFileChange,
    copy_from: Option<(MPath, ChangesetId)>,
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct BasicFileChange {
    content_id: ContentId,
    file_type: FileType,
    size: u64,
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum FileChange {
    Change(TrackedFileChange),
    Deletion,
    // TODO(T98053352): Possibly put copy information on untracked changes
    UntrackedChange(BasicFileChange),
    UntrackedDeletion,
}

impl TrackedFileChange {
    pub fn new(
        content_id: ContentId,
        file_type: FileType,
        size: u64,
        copy_from: Option<(MPath, ChangesetId)>,
    ) -> Self {
        Self {
            inner: BasicFileChange {
                content_id,
                file_type,
                size,
            },
            copy_from,
        }
    }

    pub fn with_new_copy_from(&self, copy_from: Option<(MPath, ChangesetId)>) -> Self {
        Self::new(
            self.inner.content_id,
            self.inner.file_type,
            self.inner.size,
            copy_from,
        )
    }

    pub(crate) fn into_thrift(self) -> thrift::FileChange {
        thrift::FileChange {
            content_id: self.inner.content_id.into_thrift(),
            file_type: self.inner.file_type.into_thrift(),
            size: self.inner.size as i64,
            copy_from: self.copy_from.map(|(file, cs_id)| thrift::CopyInfo {
                file: file.into_thrift(),
                cs_id: cs_id.into_thrift(),
            }),
        }
    }

    pub fn content_id(&self) -> ContentId {
        self.inner.content_id
    }

    pub fn file_type(&self) -> FileType {
        self.inner.file_type
    }

    pub fn size(&self) -> u64 {
        self.inner.size
    }

    pub fn copy_from(&self) -> Option<&(MPath, ChangesetId)> {
        self.copy_from.as_ref()
    }

    pub fn copy_from_mut(&mut self) -> Option<&mut (MPath, ChangesetId)> {
        self.copy_from.as_mut()
    }

    pub(crate) fn from_thrift(fc: thrift::FileChange, mpath: &MPath) -> Result<Self> {
        let catch_block = || -> Result<_> {
            Ok(Self {
                inner: BasicFileChange {
                    content_id: ContentId::from_thrift(fc.content_id)?,
                    file_type: FileType::from_thrift(fc.file_type)?,
                    size: fc.size as u64,
                },
                copy_from: match fc.copy_from {
                    Some(copy_info) => Some((
                        MPath::from_thrift(copy_info.file)?,
                        ChangesetId::from_thrift(copy_info.cs_id)?,
                    )),
                    None => None,
                },
            })
        };

        catch_block().with_context(|| {
            ErrorKind::InvalidThrift(
                "FileChange".into(),
                format!("Invalid changed entry for path {}", mpath),
            )
        })
    }
}

impl BasicFileChange {
    pub fn new(content_id: ContentId, file_type: FileType, size: u64) -> Self {
        Self {
            content_id,
            file_type,
            size,
        }
    }

    pub fn content_id(&self) -> ContentId {
        self.content_id
    }

    pub fn file_type(&self) -> FileType {
        self.file_type
    }

    pub fn size(&self) -> u64 {
        self.size
    }

    pub(crate) fn into_thrift_untracked(self) -> thrift::UntrackedFileChange {
        thrift::UntrackedFileChange {
            content_id: self.content_id.into_thrift(),
            file_type: self.file_type.into_thrift(),
            size: self.size as i64,
        }
    }

    pub(crate) fn from_thrift_untracked(uc: thrift::UntrackedFileChange) -> Result<Self> {
        Ok(Self {
            content_id: ContentId::from_thrift(uc.content_id)?,
            file_type: FileType::from_thrift(uc.file_type)?,
            size: uc.size as u64,
        })
    }
}

impl FileChange {
    pub fn tracked(
        content_id: ContentId,
        file_type: FileType,
        size: u64,
        copy_from: Option<(MPath, ChangesetId)>,
    ) -> Self {
        Self::Change(TrackedFileChange::new(
            content_id, file_type, size, copy_from,
        ))
    }

    pub fn untracked(content_id: ContentId, file_type: FileType, size: u64) -> Self {
        Self::UntrackedChange(BasicFileChange {
            content_id,
            file_type,
            size,
        })
    }

    /// Convert this to a simple file change, where tracked and untracked
    /// changes are treated the same way, as well as missing and deleted
    pub fn simplify(&self) -> Option<&BasicFileChange> {
        match self {
            Self::Change(tc) => Some(&tc.inner),
            Self::UntrackedChange(uc) => Some(uc),
            Self::Deletion | Self::UntrackedDeletion => None,
        }
    }

    pub fn copy_from(&self) -> Option<&(MPath, ChangesetId)> {
        match self {
            Self::Change(tc) => tc.copy_from(),
            Self::Deletion | Self::UntrackedDeletion | Self::UntrackedChange(_) => None,
        }
    }

    pub fn size(&self) -> Option<u64> {
        match &self {
            Self::Change(tc) => Some(tc.size()),
            Self::UntrackedChange(uc) => Some(uc.size),
            Self::Deletion | Self::UntrackedDeletion => None,
        }
    }

    pub fn is_changed(&self) -> bool {
        match &self {
            Self::Change(_) | Self::UntrackedChange(_) => true,
            Self::Deletion | Self::UntrackedDeletion => false,
        }
    }

    pub fn is_removed(&self) -> bool {
        match &self {
            Self::Change(_) | Self::UntrackedChange(_) => false,
            Self::Deletion | Self::UntrackedDeletion => true,
        }
    }

    pub(crate) fn from_thrift(fc_opt: thrift::FileChangeOpt, mpath: &MPath) -> Result<Self> {
        match (
            fc_opt.change,
            fc_opt.untracked_change,
            fc_opt.untracked_deletion,
        ) {
            (Some(tc), None, None) => Ok(Self::Change(TrackedFileChange::from_thrift(tc, mpath)?)),
            (None, Some(uc), None) => Ok(Self::UntrackedChange(
                BasicFileChange::from_thrift_untracked(uc)?,
            )),
            (None, None, Some(_)) => Ok(Self::UntrackedDeletion),
            (None, None, None) => Ok(Self::Deletion),
            _ => bail!("FileChangeOpt has more than one present field"),
        }
    }

    #[inline]
    pub(crate) fn into_thrift(self) -> thrift::FileChangeOpt {
        let mut fco = thrift::FileChangeOpt {
            change: None,
            untracked_change: None,
            untracked_deletion: None,
        };
        match self {
            Self::Change(tc) => {
                fco.change = Some(tc.into_thrift());
            }
            Self::UntrackedChange(uc) => {
                fco.untracked_change = Some(uc.into_thrift_untracked());
            }
            Self::UntrackedDeletion => {
                fco.untracked_deletion = Some(thrift::UntrackedDeletion {});
            }
            Self::Deletion => {}
        }
        fco
    }

    /// Generate a random FileChange which picks copy-from parents from the list of parents
    /// provided.
    pub(crate) fn arbitrary_from_parents(g: &mut Gen, parents: &[ChangesetId]) -> Self {
        let copy_from = if *g.choose(&[0, 1, 2, 3, 4]).unwrap() < 1 {
            g.choose(parents)
                .map(|parent| (MPath::arbitrary(g), *parent))
        } else {
            None
        };
        Self::Change(TrackedFileChange::new(
            ContentId::arbitrary(g),
            FileType::arbitrary(g),
            u64::arbitrary(g),
            copy_from,
        ))
    }
}

impl Arbitrary for FileChange {
    fn arbitrary(g: &mut Gen) -> Self {
        let copy_from = if *g.choose(&[0, 1, 2, 3, 4]).unwrap() < 1 {
            Some((MPath::arbitrary(g), ChangesetId::arbitrary(g)))
        } else {
            None
        };
        Self::Change(TrackedFileChange::new(
            ContentId::arbitrary(g),
            FileType::arbitrary(g),
            u64::arbitrary(g),
            copy_from,
        ))
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        // The only thing that can be reduced here is copy_from.
        if let Self::Change(tc) = self {
            if tc.copy_from.is_some() {
                single_shrinker(Self::Change(TrackedFileChange {
                    copy_from: None,
                    ..tc.clone()
                }))
            } else {
                empty_shrinker()
            }
        } else {
            empty_shrinker()
        }
    }
}

/// Type of a file.
///
/// Regular and Executable are identical - they both represent files containing arbitrary content.
/// The only difference is that the Executables are created with executable permission when
/// checked out.
///
/// Symlink is also the same as Regular, but the content of the file is interpolated into a path
/// being traversed during lookup.
#[derive(
    Debug,
    Clone,
    Copy,
    Eq,
    PartialEq,
    Hash,
    Ord,
    PartialOrd,
    Serialize,
    Deserialize
)]
pub enum FileType {
    Regular,
    Executable,
    Symlink,
}

impl FileType {
    /// All possible file types.
    pub fn all() -> [FileType; 3] {
        [FileType::Regular, FileType::Executable, FileType::Symlink]
    }

    /// All the file types that `self` is not.
    pub fn complement(&self) -> [FileType; 2] {
        match self {
            FileType::Regular => [FileType::Executable, FileType::Symlink],
            FileType::Executable => [FileType::Regular, FileType::Symlink],
            FileType::Symlink => [FileType::Regular, FileType::Executable],
        }
    }

    pub fn from_thrift(ft: thrift::FileType) -> Result<Self> {
        let file_type = match ft {
            thrift::FileType::Regular => FileType::Regular,
            thrift::FileType::Executable => FileType::Executable,
            thrift::FileType::Symlink => FileType::Symlink,
            thrift::FileType(x) => bail!(ErrorKind::InvalidThrift(
                "FileType".into(),
                format!("unknown file type '{}'", x)
            )),
        };
        Ok(file_type)
    }

    pub fn into_thrift(self) -> thrift::FileType {
        match self {
            FileType::Regular => thrift::FileType::Regular,
            FileType::Executable => thrift::FileType::Executable,
            FileType::Symlink => thrift::FileType::Symlink,
        }
    }
}

impl From<FileType> for EdenapiFileType {
    fn from(v: FileType) -> Self {
        use EdenapiFileType::*;
        match v {
            FileType::Regular => Regular,
            FileType::Executable => Executable,
            FileType::Symlink => Symlink,
        }
    }
}

impl From<EdenapiFileType> for FileType {
    fn from(v: EdenapiFileType) -> Self {
        use EdenapiFileType::*;
        match v {
            Regular => FileType::Regular,
            Executable => FileType::Executable,
            Symlink => FileType::Symlink,
        }
    }
}

impl fmt::Display for FileType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let s = match self {
            FileType::Symlink => "symlink",
            FileType::Executable => "executable",
            FileType::Regular => "regular",
        };
        write!(f, "{}", s)
    }
}

impl Arbitrary for FileType {
    fn arbitrary(g: &mut Gen) -> Self {
        match u64::arbitrary(g) % 10 {
            0 => FileType::Executable,
            1 => FileType::Symlink,
            _ => FileType::Regular,
        }
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        empty_shrinker()
    }
}

#[cfg(test)]
mod test {
    use quickcheck::quickcheck;

    use super::*;

    quickcheck! {
        fn filetype_thrift_roundtrip(ft: FileType) -> bool {
            let thrift_ft = ft.into_thrift();
            let ft2 = FileType::from_thrift(thrift_ft)
                .expect("thrift roundtrip should always be valid");
            ft == ft2
        }

        fn filechange_thrift_roundtrip(fc: FileChange) -> bool {
            let thrift_fc = fc.clone().into_thrift();
            let fc2 = FileChange::from_thrift(thrift_fc, &MPath::new("foo").unwrap())
                .expect("thrift roundtrip should always be valid");
            fc == fc2
        }
    }

    #[test]
    fn bad_filetype_thrift() {
        let thrift_ft = thrift::FileType(42);
        FileType::from_thrift(thrift_ft).expect_err("unexpected OK - unknown file type");
    }

    #[test]
    fn bad_filechange_thrift() {
        let thrift_fc = thrift::FileChange {
            content_id: thrift::ContentId(thrift::IdType::Blake2(thrift::Blake2(
                vec![0; 16].into(),
            ))),
            file_type: thrift::FileType::Regular,
            size: 0,
            copy_from: None,
        };
        TrackedFileChange::from_thrift(thrift_fc, &MPath::new("foo").unwrap())
            .expect_err("unexpected OK - bad content ID");
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fmt;
use std::fmt::Debug;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use bytes::Bytes;
use fbthrift::compact_protocol;
use quickcheck::empty_shrinker;
use quickcheck::single_shrinker;
use quickcheck::Arbitrary;
use quickcheck::Gen;

use crate::blob::Blob;
use crate::blob::BlobstoreValue;
use crate::blob::ContentBlob;
use crate::errors::ErrorKind;
use crate::thrift;
use crate::typed_hash::ContentChunkId;
use crate::typed_hash::ContentId;
use crate::typed_hash::ContentIdContext;

/// An enum representing contents for a file.
#[derive(Clone, Eq, PartialEq)]
pub enum FileContents {
    /// Raw contents of the file
    Bytes(Bytes),
    /// Reference to separate ContentChunks that need to be joined together to produce this file
    Chunked(ChunkedFileContents),
}

impl FileContents {
    pub fn new_bytes<B: Into<Bytes>>(b: B) -> Self {
        FileContents::Bytes(b.into())
    }

    pub(crate) fn from_thrift(fc: thrift::FileContents) -> Result<Self> {
        match fc {
            thrift::FileContents::Bytes(bytes) => Ok(FileContents::Bytes(bytes)),
            thrift::FileContents::Chunked(chunked) => {
                let contents = ChunkedFileContents::from_thrift(chunked)?;
                Ok(FileContents::Chunked(contents))
            }
            thrift::FileContents::UnknownField(x) => bail!(ErrorKind::InvalidThrift(
                "FileContents".into(),
                format!("unknown file contents field: {}", x)
            )),
        }
    }

    pub(crate) fn into_thrift(self) -> thrift::FileContents {
        match self {
            FileContents::Bytes(bytes) => thrift::FileContents::Bytes(bytes),
            FileContents::Chunked(chunked) => thrift::FileContents::Chunked(chunked.into_thrift()),
        }
    }

    pub fn from_encoded_bytes(encoded_bytes: Bytes) -> Result<Self> {
        let thrift_tc = compact_protocol::deserialize(encoded_bytes)
            .with_context(|| ErrorKind::BlobDeserializeError("FileContents".into()))?;
        Self::from_thrift(thrift_tc)
    }

    pub fn content_id(&self) -> ContentId {
        match self {
            this @ FileContents::Bytes(..) => *this.clone().into_blob().id(),
            FileContents::Chunked(chunked) => chunked.content_id(),
        }
    }

    pub fn size(&self) -> u64 {
        match self {
            // NOTE: This unwrap() will panic iif we have a Bytes in memory that's larger than a
            // u64. That's not going to happen.
            FileContents::Bytes(bytes) => bytes.len().try_into().unwrap(),
            FileContents::Chunked(chunked) => chunked.size(),
        }
    }

    pub fn content_id_for_bytes(bytes: &Bytes) -> ContentId {
        let mut context = ContentIdContext::new();
        context.update(bytes);
        context.finish()
    }

    pub fn content_chunk_ids(&self) -> Vec<ContentChunkId> {
        match self {
            Self::Bytes(_) => vec![],
            Self::Chunked(chunked) => chunked.chunk_ids(),
        }
    }
}

impl BlobstoreValue for FileContents {
    type Key = ContentId;

    fn into_blob(self) -> ContentBlob {
        let id = match &self {
            Self::Bytes(bytes) => Self::content_id_for_bytes(bytes),
            Self::Chunked(chunked) => chunked.content_id(),
        };

        let thrift = self.into_thrift();
        let data = compact_protocol::serialize(&thrift);

        Blob::new(id, data)
    }

    fn from_blob(blob: ContentBlob) -> Result<Self> {
        let thrift_tc = compact_protocol::deserialize(blob.data())
            .with_context(|| ErrorKind::BlobDeserializeError("FileContents".into()))?;
        Self::from_thrift(thrift_tc)
    }
}

impl Debug for FileContents {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            FileContents::Bytes(ref bytes) => {
                write!(f, "FileContents::Bytes(length {})", bytes.len())
            }
            FileContents::Chunked(ref chunked) => write!(
                f,
                "FileContents::Chunked({}, length {})",
                chunked.content_id(),
                chunked.size()
            ),
        }
    }
}

impl Arbitrary for FileContents {
    fn arbitrary(g: &mut Gen) -> Self {
        if bool::arbitrary(g) {
            FileContents::new_bytes(Vec::arbitrary(g))
        } else {
            FileContents::Chunked(ChunkedFileContents::arbitrary(g))
        }
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        match self {
            FileContents::Bytes(..) => single_shrinker(FileContents::new_bytes(vec![])),
            FileContents::Chunked(..) => empty_shrinker(),
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ChunkedFileContents {
    content_id: ContentId,
    chunks: Vec<ContentChunkPointer>,
    // NOTE: We compute the size upon construction to make size() a O(1) call, not O(len(chunks)).
    size: u64,
}

impl ChunkedFileContents {
    pub fn new(content_id: ContentId, chunks: Vec<ContentChunkPointer>) -> Self {
        let size = chunks.iter().map(|c| c.size).sum();

        Self {
            content_id,
            chunks,
            size,
        }
    }

    pub fn from_bytes(blob: Bytes) -> Result<Self> {
        let thrift_chunked = compact_protocol::deserialize(blob)
            .with_context(|| ErrorKind::BlobDeserializeError("ChunkedFileContents".into()))?;
        Self::from_thrift(thrift_chunked)
    }

    pub fn from_thrift(thrift_chunked: thrift::ChunkedFileContents) -> Result<Self> {
        let content_id = ContentId::from_thrift(thrift_chunked.content_id)?;
        let chunks = thrift_chunked
            .chunks
            .into_iter()
            .map(ContentChunkPointer::from_thrift)
            .collect::<Result<Vec<_>>>()?;

        Ok(Self::new(content_id, chunks))
    }

    pub fn into_thrift(self) -> thrift::ChunkedFileContents {
        let content_id = self.content_id.into_thrift();
        let chunks = self
            .chunks
            .into_iter()
            .map(ContentChunkPointer::into_thrift)
            .collect();
        thrift::ChunkedFileContents { content_id, chunks }
    }

    pub fn into_chunks(self) -> Vec<ContentChunkPointer> {
        self.chunks
    }

    pub fn num_chunks(&self) -> usize {
        self.chunks.len()
    }

    pub fn iter_chunks(&self) -> impl Iterator<Item = &ContentChunkPointer> {
        self.chunks.iter()
    }

    pub fn content_id(&self) -> ContentId {
        self.content_id
    }

    pub fn size(&self) -> u64 {
        self.size
    }
    pub(crate) fn chunk_ids(&self) -> Vec<ContentChunkId> {
        self.iter_chunks()
            .map(|content_chunk_pointer| content_chunk_pointer.chunk_id())
            .collect()
    }
}

impl Arbitrary for ChunkedFileContents {
    fn arbitrary(g: &mut Gen) -> Self {
        Self::new(ContentId::arbitrary(g), Vec::arbitrary(g))
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub struct ContentChunkPointer {
    chunk_id: ContentChunkId,
    size: u64,
}

impl ContentChunkPointer {
    pub fn new(chunk_id: ContentChunkId, size: u64) -> Self {
        Self { chunk_id, size }
    }

    pub fn from_bytes(blob: Bytes) -> Result<Self> {
        let thrift_chunk = compact_protocol::deserialize(blob)
            .with_context(|| ErrorKind::BlobDeserializeError("ContentChunkPointer".into()))?;
        Self::from_thrift(thrift_chunk)
    }

    pub fn from_thrift(thrift_chunk: thrift::ContentChunkPointer) -> Result<Self> {
        let chunk_id = ContentChunkId::from_thrift(thrift_chunk.chunk_id)?;
        let size: u64 = thrift_chunk.size.try_into()?;
        Ok(Self::new(chunk_id, size))
    }

    pub fn into_thrift(self) -> thrift::ContentChunkPointer {
        // NOTE: unwrap() will fail here if we are dealing with a chunk whose size doesn't fit an
        // i64 but does fit a u64. This isn't something we meaningfully seek to support at the
        // moment.
        let chunk_id = self.chunk_id.into_thrift();
        let size: i64 = self.size.try_into().unwrap();
        thrift::ContentChunkPointer { chunk_id, size }
    }

    pub fn chunk_id(&self) -> ContentChunkId {
        self.chunk_id
    }

    pub fn size(&self) -> u64 {
        self.size
    }
}

impl Arbitrary for ContentChunkPointer {
    fn arbitrary(g: &mut Gen) -> Self {
        // We don't want a big size because the sum of multiple ContentChunkPointer
        // should also fit into a i64
        let size = u32::arbitrary(g) as u64;
        Self::new(ContentChunkId::arbitrary(g), size)
    }
}

#[cfg(test)]
mod test {
    use quickcheck::quickcheck;

    use super::*;

    quickcheck! {
        fn file_contents_thrift_roundtrip(fc: FileContents) -> bool {
            let thrift_fc = fc.clone().into_thrift();
            let fc2 = FileContents::from_thrift(thrift_fc)
                .expect("thrift roundtrips should always be valid");
            fc == fc2
        }

        fn file_contents_blob_roundtrip(fc: FileContents) -> bool {
            let blob = fc.clone().into_blob();
            let fc2 = FileContents::from_blob(blob)
                .expect("blob roundtrips should always be valid");
            fc == fc2
        }
    }

    #[test]
    fn bad_thrift() {
        let thrift_fc = thrift::FileContents::UnknownField(-1);
        FileContents::from_thrift(thrift_fc).expect_err("unexpected OK - unknown field");
    }
}