/*
 * 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::HashSet;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;

use anyhow::anyhow;
use anyhow::Result;
use configmodel::Config;
use configmodel::ConfigExt;
use io::IO;
use manifest_tree::ReadTreeManifest;
use parking_lot::Mutex;
use pathmatcher::AlwaysMatcher;
use pathmatcher::Matcher;
use pathmatcher::NeverMatcher;
use progress_model::ProgressBar;
use repolock::RepoLocker;
use serde::Deserialize;
use serde::Serialize;
use treestate::filestate::StateFlags;
use treestate::treestate::TreeState;
use types::path::ParseError;
use types::RepoPathBuf;
use vfs::VFS;
use watchman_client::prelude::*;

use super::treestate::clear_needs_check;
use super::treestate::mark_needs_check;
use super::treestate::set_clock;
use crate::filechangedetector::ArcReadFileContents;
use crate::filechangedetector::FileChangeDetector;
use crate::filechangedetector::FileChangeDetectorTrait;
use crate::filechangedetector::ResolvedFileChangeResult;
use crate::filesystem::ChangeType;
use crate::filesystem::PendingChangeResult;
use crate::filesystem::PendingChanges;
use crate::metadata;
use crate::metadata::Metadata;
use crate::util::walk_treestate;
use crate::watchmanfs::treestate::get_clock;
use crate::watchmanfs::treestate::list_needs_check;
use crate::watchmanfs::treestate::maybe_flush_treestate;
use crate::workingcopy::WorkingCopy;

type ArcReadTreeManifest = Arc<dyn ReadTreeManifest + Send + Sync>;

pub struct WatchmanFileSystem {
    vfs: VFS,
    treestate: Arc<Mutex<TreeState>>,
    tree_resolver: ArcReadTreeManifest,
    store: ArcReadFileContents,
    locker: Arc<RepoLocker>,
}

struct WatchmanConfig {
    clock: Option<Clock>,
    sync_timeout: std::time::Duration,
}

query_result_type! {
    pub struct StatusQuery {
        name: BytesNameField,
        mode: ModeAndPermissionsField,
        size: SizeField,
        mtime: MTimeField,
        exists: ExistsField,
    }
}

#[derive(Deserialize, Debug)]
struct DebugRootStatusResponse {
    pub root_status: Option<RootStatus>,
}

#[derive(Deserialize, Debug)]
struct RootStatus {
    pub recrawl_info: Option<RecrawlInfo>,
}

#[derive(Deserialize, Debug)]
pub struct RecrawlInfo {
    pub stats: Option<u64>,
}

#[derive(Serialize, Clone, Debug)]
pub struct DebugRootStatusRequest(pub &'static str, pub PathBuf);

impl WatchmanFileSystem {
    pub fn new(
        vfs: VFS,
        treestate: Arc<Mutex<TreeState>>,
        tree_resolver: ArcReadTreeManifest,
        store: ArcReadFileContents,
        locker: Arc<RepoLocker>,
    ) -> Result<Self> {
        Ok(WatchmanFileSystem {
            vfs,
            treestate,
            tree_resolver,
            store,
            locker,
        })
    }

    async fn query_files(
        &self,
        config: WatchmanConfig,
        ignore_dirs: Vec<PathBuf>,
    ) -> Result<QueryResult<StatusQuery>> {
        let start = std::time::Instant::now();

        // This starts watchman if it isn't already started.
        let client = Connector::new().connect().await?;

        // This blocks until the recrawl (if required) is done. Progress is
        // shown by the crawl_progress task.
        let resolved = client
            .resolve_root(CanonicalPath::canonicalize(self.vfs.root())?)
            .await?;

        let mut expr = None;
        if !ignore_dirs.is_empty() {
            expr = Some(Expr::Not(Box::new(Expr::Any(
                ignore_dirs
                    .into_iter()
                    .map(|p| {
                        Expr::DirName(DirNameTerm {
                            path: p,
                            depth: None,
                        })
                    })
                    .collect(),
            ))));
        }

        // The crawl is done - display a generic "we're querying" spinner.
        let _bar = ProgressBar::register_new("querying watchman", 0, "");

        let result = client
            .query::<StatusQuery>(
                &resolved,
                QueryRequestCommon {
                    since: config.clock,
                    expression: expr,
                    sync_timeout: config.sync_timeout.into(),
                    ..Default::default()
                },
            )
            .await?;

        tracing::trace!(target: "measuredtimes", watchmanquery_time=start.elapsed().as_millis());

        Ok(result)
    }
}

async fn crawl_progress(root: PathBuf, approx_file_count: u64) -> Result<()> {
    let client = {
        let _bar = ProgressBar::register_new("connecting watchman", 0, "");

        // If watchman just started (and we issued "watch-project" from
        // query_files), this connect gets stuck indefinitely. Work around by
        // timing out and retrying until we get through.
        loop {
            match tokio::time::timeout(Duration::from_secs(1), Connector::new().connect()).await {
                Ok(client) => break client?,
                Err(_) => {}
            };

            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    };

    let mut bar = None;

    let req = DebugRootStatusRequest(
        "debug-root-status",
        CanonicalPath::canonicalize(root)?.into_path_buf(),
    );

    loop {
        let response: DebugRootStatusResponse = client.generic_request(req.clone()).await?;

        if let Some(RootStatus {
            recrawl_info: Some(RecrawlInfo { stats: Some(stats) }),
        }) = response.root_status
        {
            bar.get_or_insert_with(|| {
                ProgressBar::register_new("crawling", approx_file_count, "files (approx)")
            })
            .set_position(stats);
        } else if bar.is_some() {
            return Ok(());
        }

        tokio::time::sleep(Duration::from_millis(100)).await;
    }
}

impl PendingChanges for WatchmanFileSystem {
    #[tracing::instrument(skip_all)]
    fn pending_changes(
        &self,
        matcher: Arc<dyn Matcher + Send + Sync + 'static>,
        mut ignore_matcher: Arc<dyn Matcher + Send + Sync + 'static>,
        ignore_dirs: Vec<PathBuf>,
        last_write: SystemTime,
        config: &dyn Config,
        io: &IO,
    ) -> Result<Box<dyn Iterator<Item = Result<PendingChangeResult>>>> {
        let ts = &mut *self.treestate.lock();

        let ts_metadata = ts.metadata()?;
        let mut prev_clock = get_clock(&ts_metadata)?;

        let track_ignored = config.get_or_default::<bool>("fsmonitor", "track-ignore-files")?;
        let ts_track_ignored = ts_metadata.get("track-ignored").map(|v| v.as_ref()) == Some("1");
        if track_ignored != ts_track_ignored {
            // If track-ignore-files has changed, trigger a migration by
            // unsetting the clock. Watchman will do a full crawl and report
            // fresh instance.
            prev_clock = None;

            // Store new value of track ignored so we don't migrate again.
            let md_value = if track_ignored {
                "1".to_string()
            } else {
                "0".to_string()
            };
            tracing::info!(track_ignored = md_value, "migrating track-ignored");
            ts.update_metadata(&[("track-ignored".to_string(), Some(md_value))])?;
        }

        if config.get_or_default("devel", "watchman-reset-clock")? {
            prev_clock = None;
        }

        let progress_handle = async_runtime::spawn(crawl_progress(
            self.vfs.root().to_path_buf(),
            ts.len() as u64,
        ));

        let result = {
            // Instrument query_files() from outside to avoid async weirdness.
            let _span = tracing::info_span!("query_files").entered();

            async_runtime::block_on(self.query_files(
                WatchmanConfig {
                    clock: prev_clock.clone(),
                    sync_timeout:
                        config.get_or::<Duration>("fsmonitor", "timeout", || {
                            Duration::from_secs(10)
                        })?,
                },
                ignore_dirs,
            ))?
        };

        progress_handle.abort();

        tracing::debug!(
            target: "watchman_info",
            watchmanfreshinstances= if result.is_fresh_instance { 1 } else { 0 },
            watchmanfilecount=result.files.as_ref().map_or(0, |f| f.len()),
        );

        let should_warn = config.get_or_default("fsmonitor", "warn-fresh-instance")?;
        if result.is_fresh_instance && should_warn {
            let _ = warn_about_fresh_instance(
                io,
                parse_watchman_pid(prev_clock.as_ref()),
                parse_watchman_pid(Some(&result.clock)),
            );
        }

        let file_change_threshold =
            config.get_or("fsmonitor", "watchman-changed-file-threshold", || 200)?;
        let should_update_clock = result.is_fresh_instance
            || result
                .files
                .as_ref()
                .map_or(false, |f| f.len() > file_change_threshold);

        let manifests = WorkingCopy::current_manifests(ts, &self.tree_resolver)?;

        let mut wm_errors: Vec<ParseError> = Vec::new();
        let use_watchman_metadata =
            config.get_or::<bool>("workingcopy", "use-watchman-metadata", || true)?;
        let wm_needs_check: Vec<metadata::File> = result
            .files
            .unwrap_or_default()
            .into_iter()
            .filter_map(
                |file| match RepoPathBuf::from_utf8(file.name.into_inner().into_bytes()) {
                    Ok(path) => {
                        tracing::trace!(
                            ?path,
                            mode = *file.mode,
                            size = *file.size,
                            mtime = *file.mtime,
                            exists = *file.exists,
                            "watchman file"
                        );

                        let meta = Metadata::from_stat(
                            file.mode.into_inner() as u32,
                            file.size.into_inner(),
                            file.mtime.into_inner(),
                        );

                        let fs_meta = if *file.exists {
                            if use_watchman_metadata {
                                Some(Some(meta))
                            } else {
                                None
                            }
                        } else {
                            // If watchman says the file doesn't exist, indicate
                            // that via the metadata being None. This is
                            // important when a file moves behind a symlink;
                            // Watchman will report it as deleted, but a naive
                            // lstat() call would show the file to still exist.
                            Some(None)
                        };

                        Some(metadata::File {
                            path,
                            fs_meta,
                            ts_state: None,
                        })
                    }
                    Err(err) => {
                        wm_errors.push(err);
                        None
                    }
                },
            )
            .collect();

        if track_ignored {
            // If we want to track ignored files, say that nothing is ignored.
            // Note that the "full" matcher will still skip ignored files.
            ignore_matcher = Arc::new(NeverMatcher::new());
        }

        let detector = FileChangeDetector::new(
            self.vfs.clone(),
            last_write.try_into()?,
            manifests[0].clone(),
            self.store.clone(),
            config.get_opt("workingcopy", "worker-count")?,
        );
        let mut pending_changes = detect_changes(
            matcher,
            ignore_matcher,
            detector,
            ts,
            wm_needs_check,
            result.is_fresh_instance,
            self.vfs.case_sensitive(),
        )?;

        // Add back path errors into the pending changes. The caller
        // of pending_changes must choose how to handle these.
        pending_changes
            .pending_changes
            .extend(wm_errors.into_iter().map(|e| Err(anyhow!(e))));

        let did_something = pending_changes.update_treestate(ts)?;
        if did_something || should_update_clock {
            // If we had something to update in the treestate, make sure clock is updated as well.
            set_clock(ts, result.clock)?;
        }

        maybe_flush_treestate(self.vfs.root(), ts, &self.locker)?;

        Ok(Box::new(pending_changes.into_iter()))
    }
}

fn warn_about_fresh_instance(io: &IO, old_pid: Option<u32>, new_pid: Option<u32>) -> Result<()> {
    let mut output = io.error();
    match (old_pid, new_pid) {
        (Some(old_pid), Some(new_pid)) if old_pid != new_pid => {
            writeln!(
                &mut output,
                "warning: watchman has recently restarted (old pid {}, new pid {}) - operation will be slower than usual",
                old_pid, new_pid
            )?;
        }
        (None, Some(new_pid)) => {
            writeln!(
                &mut output,
                "warning: watchman has recently started (pid {}) - operation will be slower than usual",
                new_pid
            )?;
        }
        _ => {
            writeln!(
                &mut output,
                "warning: watchman failed to catch up with file change events and requires a full scan - operation will be slower than usual"
            )?;
        }
    }

    Ok(())
}

// Given the existing treestate and files watchman says to check,
// figure out all the files that may have changed and check them for
// changes. Also track paths we need to mark or unmark as NEED_CHECK
// in the treestate.
#[tracing::instrument(skip_all)]
pub(crate) fn detect_changes(
    matcher: Arc<dyn Matcher + Send + Sync + 'static>,
    ignore_matcher: Arc<dyn Matcher + Send + Sync + 'static>,
    mut file_change_detector: impl FileChangeDetectorTrait + 'static,
    ts: &mut TreeState,
    wm_need_check: Vec<metadata::File>,
    wm_fresh_instance: bool,
    fs_case_sensitive: bool,
) -> Result<WatchmanPendingChanges> {
    let _span = tracing::info_span!("prepare stuff").entered();

    let (ts_need_check, ts_errors) = list_needs_check(ts, matcher)?;

    // NB: ts_need_check is filtered by the matcher, so it does not
    // necessarily contain all NEED_CHECK entries in the treestate.
    let ts_need_check: HashSet<_> = ts_need_check.into_iter().collect();

    let mut pending_changes: Vec<Result<PendingChangeResult>> =
        ts_errors.into_iter().map(|e| Err(anyhow!(e))).collect();
    let mut needs_clear = Vec::new();
    let mut needs_mark = Vec::new();

    tracing::debug!(
        watchman_needs_check = wm_need_check.len(),
        treestate_needs_check = ts_need_check.len(),
    );

    let total_needs_check = ts_need_check.len()
        + wm_need_check
            .iter()
            .filter(|p| !ts_need_check.contains(&p.path))
            .count();

    // This is to set "total" for progress bar.
    file_change_detector.total_work_hint(total_needs_check as u64);

    let wm_seen: HashSet<RepoPathBuf> = wm_need_check.iter().map(|f| f.path.clone()).collect();

    drop(_span);

    let _span = tracing::info_span!("submit ts_need_check").entered();

    for ts_needs_check in ts_need_check.iter() {
        // Prefer to kick off file check using watchman data since that already
        // includes disk metadata.
        if wm_seen.contains(ts_needs_check) {
            continue;
        }

        // We don't need the ignore check since ts_need_check was filtered by
        // the full matcher, which incorporates the ignore matcher.
        file_change_detector.submit(metadata::File {
            path: ts_needs_check.clone(),
            ts_state: ts.normalized_get(ts_needs_check)?,
            fs_meta: None,
        });
    }

    drop(_span);

    let _span = tracing::info_span!("submit wm_need_check").entered();

    for mut wm_needs_check in wm_need_check {
        let state = ts.normalized_get(&wm_needs_check.path)?;

        let is_tracked = match &state {
            Some(state) => state
                .state
                .intersects(StateFlags::EXIST_P1 | StateFlags::EXIST_P2 | StateFlags::EXIST_NEXT),
            None => false,
        };
        // Skip ignored files to reduce work. We short circuit with an
        // "untracked" check to minimize use of the GitignoreMatcher.
        if !is_tracked && ignore_matcher.matches_file(&wm_needs_check.path)? {
            continue;
        }

        wm_needs_check.ts_state = state;

        file_change_detector.submit(wm_needs_check);
    }

    drop(_span);

    let _span = tracing::info_span!("handle results").entered();

    let mut deletes = Vec::new();

    for result in file_change_detector {
        match result {
            Ok(ResolvedFileChangeResult::Yes(change)) => {
                let path = change.get_path();
                if let ChangeType::Deleted(path) = change {
                    deletes.push(path);
                } else {
                    if !ts_need_check.contains(path) {
                        needs_mark.push(path.clone());
                    }
                    pending_changes.push(Ok(PendingChangeResult::File(change)));
                }
            }
            Ok(ResolvedFileChangeResult::No(path)) => {
                if ts_need_check.contains(&path) {
                    needs_clear.push(path);
                }
            }
            Err(e) => pending_changes.push(Err(e)),
        }
    }

    drop(_span);

    if wm_fresh_instance {
        let _span = tracing::info_span!("fresh_instance work", wm_len = wm_seen.len()).entered();

        // On fresh instance, watchman returns all files present on
        // disk. We need to catch the case where a tracked file has been
        // deleted while watchman wasn't running. To do that, report a
        // pending "delete" change for all EXIST_NEXT files that were
        // _not_ in the list we got from watchman.
        walk_treestate(
            ts,
            Arc::new(AlwaysMatcher::new()),
            StateFlags::EXIST_NEXT,
            StateFlags::NEED_CHECK,
            |path, _state| {
                if !wm_seen.contains(&path) {
                    deletes.push(path);
                }
                Ok(())
            },
        )?;

        // Clear out ignored/untracked files that have been deleted.
        walk_treestate(
            ts,
            Arc::new(AlwaysMatcher::new()),
            StateFlags::NEED_CHECK,
            StateFlags::EXIST_NEXT | StateFlags::EXIST_P1 | StateFlags::EXIST_P2,
            |path, _state| {
                if !wm_seen.contains(&path) {
                    needs_clear.push(path);
                }
                Ok(())
            },
        )?;
    }

    if !deletes.is_empty() {
        let mut deletes: HashSet<_> = deletes.into_iter().collect();

        if !fs_case_sensitive {
            // On case insensitive (but case preservign) filesystems, watchman
            // reports a case sensitive rename as a delete of the old name and
            // update of the new name. We need to ignore the delete. We do that
            // here by skipping deletes that insensitively match other paths
            // watchman told us about.
            let other_changes: HashSet<RepoPathBuf> = wm_seen
                .iter()
                .filter_map(|p| {
                    if !deletes.contains(p) {
                        Some(p.to_lower_case())
                    } else {
                        None
                    }
                })
                .collect();

            deletes.retain(|d| {
                if !other_changes.contains(&d.to_lower_case()) {
                    true
                } else {
                    tracing::trace!(deleted=?d, "ignoring case insensitive delete");
                    false
                }
            });
        }

        for d in deletes {
            if !ts_need_check.contains(&d) {
                needs_mark.push(d.clone());
            }
            pending_changes.push(Ok(PendingChangeResult::File(ChangeType::Deleted(d))));
        }
    }

    Ok(WatchmanPendingChanges {
        pending_changes,
        needs_clear,
        needs_mark,
    })
}

pub struct WatchmanPendingChanges {
    pending_changes: Vec<Result<PendingChangeResult>>,
    needs_clear: Vec<RepoPathBuf>,
    needs_mark: Vec<RepoPathBuf>,
}

impl WatchmanPendingChanges {
    #[tracing::instrument(skip_all)]
    pub fn update_treestate(&mut self, ts: &mut TreeState) -> Result<bool> {
        let bar = ProgressBar::register_new(
            "recording files",
            (self.needs_clear.len() + self.needs_mark.len()) as u64,
            "entries",
        );

        let mut wrote = false;
        for path in self.needs_clear.iter() {
            match clear_needs_check(ts, path) {
                Ok(v) => wrote |= v,
                Err(e) =>
                // We can still build a valid result if we fail to clear the
                // needs check flag. Propagate the error to the caller but allow
                // the persist to continue.
                {
                    self.pending_changes.push(Err(e))
                }
            }

            bar.increase_position(1);
        }

        for path in self.needs_mark.iter() {
            wrote |= mark_needs_check(ts, path)?;
            bar.increase_position(1);
        }

        Ok(wrote)
    }
}

impl IntoIterator for WatchmanPendingChanges {
    type Item = Result<PendingChangeResult>;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.pending_changes.into_iter()
    }
}

fn parse_watchman_pid(clock: Option<&Clock>) -> Option<u32> {
    match clock {
        Some(Clock::Spec(ClockSpec::StringClock(clock_str))) => match clock_str.split(':').nth(2) {
            None => None,
            Some(pid) => pid.parse().ok(),
        },
        _ => None,
    }
}


/*
 * 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.
 */

//! Functions to find the difference between two texts.
//! Under the hood it's using the xdiff library that's also used by git and hg.

use std::cmp::min;
use std::ops::Range;
use std::os::raw::c_char;
use std::os::raw::c_int;
use std::os::raw::c_void;

use xdiff_sys as ffi;

/// An individual difference between two texts. Consists of two
/// line ranges that specify which parts of the texts differ.
///
/// If any of the ranges is empty it's still significant because it
/// specifies the location in the text where the other range applies.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Hunk {
    /// Line range from new text to insert
    pub add: Range<usize>,
    /// Line from the old text to delete
    pub remove: Range<usize>,
}

/// Computes the hunks of differerences between two texts.
///
/// Takes two byte-slices as arguments.
/// Returns a list of hunks that indicate differences between the old and new text.
///
/// # Example
/// ```
/// use xdiff::{diff_hunks, Hunk};
/// let a = "a\n b\n c\n d\n";
/// let b = "a\n c\n d\n e\n";
/// let c = "a\n b\n c\n d\n e\n";
/// assert_eq!(
///     diff_hunks(a, b),
///     [
///         Hunk {
///             add: 1..1,
///             remove: 1..2,
///         },
///         Hunk {
///             add: 3..4,
///             remove: 4..4,
///         }
///     ]
/// );
/// assert_eq!(
///     diff_hunks(a, c),
///     [Hunk {
///         add: 4..5,
///         remove: 4..4
///     }]
/// );
/// let x = diff_hunks(b, c);
/// assert_eq!(x[0].add.end, 2);
/// ```
pub fn diff_hunks<T>(old_text: T, new_text: T) -> Vec<Hunk>
where
    T: AsRef<[u8]>,
{
    extern "C" fn hunk_consumer(a1: i64, a2: i64, b1: i64, b2: i64, _priv: *mut c_void) -> c_int {
        let mut _priv = unsafe { (_priv as *mut Vec<Hunk>).as_mut() };
        let a1 = a1 as usize;
        let a2 = a2 as usize;
        let b1 = b1 as usize;
        let b2 = b2 as usize;
        if let Some(result) = _priv {
            result.push(Hunk {
                add: (b1..(b1 + b2)),
                remove: (a1..(a1 + a2)),
            });
        }
        0
    }

    let old_text = old_text.as_ref();
    let mut old_mmfile = ffi::mmfile_t {
        ptr: old_text.as_ptr() as *mut c_char,
        size: old_text.len() as i64,
    };
    let new_text = new_text.as_ref();
    let mut new_mmfile = ffi::mmfile_t {
        ptr: new_text.as_ptr() as *mut c_char,
        size: new_text.len() as i64,
    };
    let xpp = ffi::xpparam_t {
        flags: ffi::XDF_INDENT_HEURISTIC as u64,
        ..Default::default()
    };
    let xecfg = ffi::xdemitconf_t {
        flags: 0,
        hunk_func: Some(hunk_consumer),
    };
    let mut result: Vec<Hunk> = Vec::new();
    let mut ecb = ffi::xdemitcb_t {
        priv_: &mut result as *mut Vec<Hunk> as *mut c_void,
    };

    unsafe {
        ffi::xdl_diff_vendored(&mut old_mmfile, &mut new_mmfile, &xpp, &xecfg, &mut ecb);
    }

    result
}

/// Produce matching blocks, in (a1, a2, b1, b2) format.
/// `a_lines[a1:a2]` matches `b_lines[b1:b2]`.
pub fn blocks(a: &[u8], b: &[u8]) -> Vec<(u64, u64, u64, u64)> {
    extern "C" fn hunk_consumer(a1: i64, a2: i64, b1: i64, b2: i64, blocks: *mut c_void) -> c_int {
        let blocks = unsafe { (blocks as *mut Vec<(u64, u64, u64, u64)>).as_mut() };
        if let Some(blocks) = blocks {
            blocks.push((a1 as _, a2 as _, b1 as _, b2 as _));
        }
        0
    }

    let mut a_mmfile = ffi::mmfile_t {
        ptr: a.as_ptr() as *mut c_char,
        size: a.len() as i64,
    };
    let mut b_mmfile = ffi::mmfile_t {
        ptr: b.as_ptr() as *mut c_char,
        size: b.len() as i64,
    };
    let xpp = ffi::xpparam_t {
        flags: ffi::XDF_INDENT_HEURISTIC as u64,
        ..Default::default()
    };
    let xecfg = ffi::xdemitconf_t {
        flags: ffi::XDL_EMIT_BDIFFHUNK as _,
        hunk_func: Some(hunk_consumer),
    };
    let mut result: Vec<(u64, u64, u64, u64)> = Vec::new();
    let mut ecb = ffi::xdemitcb_t {
        priv_: &mut result as *mut Vec<(u64, u64, u64, u64)> as *mut c_void,
    };

    let ret =
        unsafe { ffi::xdl_diff_vendored(&mut a_mmfile, &mut b_mmfile, &xpp, &xecfg, &mut ecb) };

    assert_eq!(ret, 0, "xdl_diff failed");

    result
}

/// Calculate the edit cost (added and deleted line count), with a maximum threshold.
///
/// The maximum threshold `max_edit_cost` decides the maximum D in O(N+D^2)
/// complexity. Do not set it to a large value. The underlying diff algorithm
/// does not use any heuristics!
///
/// This is useful for similarity check.
pub fn edit_cost(a: &[u8], b: &[u8], max_edit_cost: u64) -> u64 {
    let mut a_mmfile = ffi::mmfile_t {
        ptr: a.as_ptr() as *mut c_char,
        size: a.len() as i64,
    };
    let mut b_mmfile = ffi::mmfile_t {
        ptr: b.as_ptr() as *mut c_char,
        size: b.len() as i64,
    };
    let xpp = ffi::xpparam_t {
        flags: ffi::XDF_CAPPED_EDIT_COST_ONLY as _,
        max_edit_cost: max_edit_cost as _,
    };
    let xecfg = ffi::xdemitconf_t {
        flags: 0,
        hunk_func: None,
    };

    let ret = unsafe {
        ffi::xdl_diff_vendored(
            &mut a_mmfile,
            &mut b_mmfile,
            &xpp,
            &xecfg,
            std::ptr::null_mut(),
        )
    };

    assert!(ret >= 0, "xdl_diff failed");

    ret as _
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct HeaderlessDiffOpts {
    /// Number of context lines
    pub context: usize,
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub enum CopyInfo {
    /// File was modified, added or removed.
    None,
    /// File was moved.
    Move,
    /// File was copied.
    Copy,
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DiffOpts {
    /// Number of context lines
    pub context: usize,
    pub copy_info: CopyInfo,
}

const MISSING_NEWLINE_MARKER: &[u8] = b"\\ No newline at end of file\n";

struct DiffPayload<'a, 'b> {
    old_has_trailing_newline: bool,
    new_has_trailing_newline: bool,
    old_lines: Vec<&'a [u8]>,
    new_lines: Vec<&'b [u8]>,
}

struct DiffState<S, F>
where
    F: Fn(S, &[u8]) -> S,
{
    seed: Option<S>,
    reduce: F,
}

impl<S, F> DiffState<S, F>
where
    F: Fn(S, &[u8]) -> S,
{
    fn new(seed: S, reduce: F) -> Self {
        Self {
            seed: Some(seed),
            reduce,
        }
    }

    fn emit_line(&mut self, prefix: &[u8], line: &[u8]) {
        self.emit(prefix);
        self.emit(line);
        self.emit(b"\n");
    }

    fn emit(&mut self, text: &[u8]) {
        // This option can be unwrapped since it'll never be None: we always put something back in
        // its place.
        let next = (self.reduce)(self.seed.take().unwrap(), text);
        self.seed = Some(next)
    }

    pub fn emit_hunk_cluster(
        &mut self,
        payload: &DiffPayload,
        cluster_bounds: Hunk,
        included_hunks: &[Hunk],
    ) {
        // Emit the header.
        self.emit(
            format!(
                "@@ -{},{} +{},{} @@\n",
                // Of course line ranges in the diff format start from 1.
                &cluster_bounds.remove.start + 1,
                &cluster_bounds.remove.len(),
                &cluster_bounds.add.start + 1,
                &cluster_bounds.add.len()
            )
            .as_bytes(),
        );

        let mut previous_hunk: Option<&Hunk> = None;

        for hunk in included_hunks {
            let context_start = previous_hunk.map_or(cluster_bounds.remove.start, |h| h.remove.end);
            payload.old_lines[context_start..hunk.remove.start]
                .iter()
                .for_each(|line| self.emit_line(b" ", line));

            // Emit the lines from the old file preceded by '-' char.
            payload.old_lines[hunk.remove.clone()]
                .iter()
                .for_each(|line| {
                    self.emit_line(b"-", line);
                });
            // In case file ends without newline we need to print a warning about it.
            if hunk.remove.end == payload.old_lines.len() {
                if !payload.old_has_trailing_newline {
                    self.emit(MISSING_NEWLINE_MARKER);
                }
            }
            // Emit the lines from the new file preceded by '+' char.
            payload.new_lines[hunk.add.clone()].iter().for_each(|line| {
                self.emit_line(b"+", line);
            });
            // In case file ends without newline we need to print a warning about it.
            if hunk.add.end == payload.new_lines.len() {
                if !payload.new_has_trailing_newline {
                    self.emit(MISSING_NEWLINE_MARKER);
                }
            }
            previous_hunk = Some(hunk);
        }
        // After the last chunk emit the remaining context.
        if let Some(previous_hunk) = previous_hunk {
            payload.old_lines[previous_hunk.remove.end..cluster_bounds.remove.end]
                .iter()
                .for_each(|line| {
                    self.emit_line(b" ", line);
                });
            // If the last line is the same in both files and it's missing newline we print the marker
            if cluster_bounds.remove.end == payload.old_lines.len()
                && previous_hunk.remove.end != cluster_bounds.remove.end
            {
                if !payload.old_has_trailing_newline {
                    self.emit(MISSING_NEWLINE_MARKER);
                }
            }
        }
    }

    fn emit_binary_has_changed(&mut self, path: &[u8]) {
        self.emit(b"Binary file ");
        self.emit(path);
        self.emit(b" has changed\n");
    }

    fn emit_binary_files_differ(&mut self, old_path: &[u8], new_path: &[u8]) {
        self.emit(b"Binary files a/");
        self.emit(old_path);
        self.emit(b" and b/");
        self.emit(new_path);
        self.emit(b" differ\n");
    }

    pub fn collect(self) -> S {
        self.seed.unwrap()
    }

    pub fn unwrap(self) -> (F, S) {
        (self.reduce, self.seed.unwrap())
    }
}

/// Computes a headerless diff between two byte-slices: `old_text` and `new_text`,
/// the number of `context` lines can be set in `opts` struct.
///
/// `emit` callback is called many times as the parts of diff are generated
///
/// Hopefuly we'll be able to refactor this API to use (currently unstable) Rust generators.
/// The only available public API now is `diff_unified_headerless` which just returns a `vec<u8>`.
fn gen_diff_unified_headerless<T, F, S>(
    old_text: &T,
    new_text: &T,
    opts: HeaderlessDiffOpts,
    seed: S,
    reduce: F,
) -> S
where
    T: AsRef<[u8]>,
    F: Fn(S, &[u8]) -> S,
{
    let old_text = old_text.as_ref();
    let new_text = new_text.as_ref();
    let mut old_lines: Vec<_> = old_text.split(|c| c == &b'\n').collect();
    let mut new_lines: Vec<_> = new_text.split(|c| c == &b'\n').collect();

    // No trailing newline in empty files is expected, for any other file a warning is displayed.
    let old_has_trailing_newline = old_text.is_empty() || old_text.last() == Some(&b'\n');
    let new_has_trailing_newline = new_text.is_empty() || new_text.last() == Some(&b'\n');

    // The last empty line traditionally doesn't count as line. Not having it is a case for warnings.
    if old_has_trailing_newline {
        old_lines.pop();
    }
    if new_has_trailing_newline {
        new_lines.pop();
    }
    let hunks = diff_hunks(old_text, new_text);

    if hunks.is_empty() {
        return seed;
    }

    let payload = DiffPayload {
        old_lines,
        new_lines,
        old_has_trailing_newline,
        new_has_trailing_newline,
    };

    let mut state = DiffState::new(seed, reduce);

    // Helpers for adding and removing context to the line numbers while keeping them
    // within array bounds.
    let sub_context = |n: usize| n.saturating_sub(opts.context);
    let add_context = |n: usize, max_l: usize| min(n + opts.context, max_l);

    // Helper for emitting a single hunk cluster.
    // (a group of chunks with overlapping contexts and shared header)

    let mut cluster: Option<(Hunk, Range<usize>)> = None;
    for (hunk_no, hunk) in hunks.iter().enumerate() {
        // `clusters` is vector of tuples (cluster_bounds, hunk_range) where:
        //  - cluster_bounds contains the line ranges the new and old file covered
        //    by cluster (including context)
        //  - hunk range contains the range of individual hunks from the `hunks` vector that
        //    are included
        // Check for overlap with the previous hunk:
        cluster = match cluster {
            Some((ref cluster_bounds, ref included_hunks_range))
                if hunks[hunk_no - 1].remove.end + opts.context
                    >= hunk.remove.start.saturating_sub(opts.context) =>
            {
                // Overlap with previous hunk, merging the bounds and extending the range.
                Some((
                    Hunk {
                        remove: cluster_bounds.remove.start
                            ..add_context(hunk.remove.end, payload.old_lines.len()),
                        add: cluster_bounds.add.start
                            ..add_context(hunk.add.end, payload.new_lines.len()),
                    },
                    included_hunks_range.start..hunk_no + 1,
                ))
            }
            _ => {
                // No overlap with previous hunk. Emit current cluster and start a new one.
                if let Some((cluster_bounds, included_hunks_range)) = cluster {
                    state.emit_hunk_cluster(&payload, cluster_bounds, &hunks[included_hunks_range]);
                }
                Some((
                    Hunk {
                        remove: sub_context(hunk.remove.start)
                            ..add_context(hunk.remove.end, payload.old_lines.len()),
                        add: sub_context(hunk.add.start)
                            ..add_context(hunk.add.end, payload.new_lines.len()),
                    },
                    hunk_no..hunk_no + 1,
                ))
            }
        };
    }
    // Emit the last cluster.
    if let Some((cluster_bounds, included_hunks_range)) = cluster {
        state.emit_hunk_cluster(&payload, cluster_bounds, &hunks[included_hunks_range]);
    }

    state.collect()
}

/// A helper function that allow us to avoid calling xdiff for trivial all-plus
/// and all-minus diffs.
///
/// `emit` callback is called many times as the parts of diff are generated
///
/// Hopefuly we'll be able to refactor this API to use (currently unstable) Rust generators.
/// The only available public API now is `diff_unified` which just returns a `vec<u8>`.
fn gen_diff_unified_headerless_entire_file<T, F, S>(prefix: u8, text: &T, seed: S, reduce: F) -> S
where
    T: AsRef<[u8]>,
    F: Fn(S, &[u8]) -> S,
{
    let mut state = DiffState::new(seed, reduce);
    let text = text.as_ref();
    let missing_newline = !text.is_empty() && !text.ends_with(b"\n");
    let text_to_split = if missing_newline || text.is_empty() {
        text
    } else {
        &text[0..text.len() - 1]
    };
    let start = if text.is_empty() { 0 } else { 1 };
    let count = text_to_split.split(|c| c == &b'\n').count();
    state.emit(
        format!(
            "@@ -{},{} +{},{} @@\n",
            if prefix == b'-' { start } else { 0 },
            if prefix == b'-' { count } else { 0 },
            if prefix == b'+' { start } else { 0 },
            if prefix == b'+' { count } else { 0 },
        )
        .as_bytes(),
    );
    text_to_split.split(|c| c == &b'\n').for_each(|line| {
        state.emit(&[prefix]);
        state.emit(line);
        state.emit(b"\n");
    });
    if missing_newline {
        state.emit(MISSING_NEWLINE_MARKER);
    }
    state.collect()
}

/// Computes a headerless diff between two byte-slices: `old_text` and `new_text`,
/// the number of `context` lines can be set in `opts` struct.
///
/// Returns a vector of bytes containing the entire diff.
pub fn diff_unified_headerless<T>(old_text: &T, new_text: &T, opts: HeaderlessDiffOpts) -> Vec<u8>
where
    T: AsRef<[u8]>,
{
    gen_diff_unified_headerless(old_text, new_text, opts, Vec::new(), |mut v, part| {
        v.extend(part);
        v
    })
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FileType {
    Regular,
    Executable,
    Symlink,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FileContent<C>
where
    C: AsRef<[u8]>,
{
    // The file content was not fetched (for example, if it's too large), though we still want to produce a placeholder diff for it.
    // `content_id` is hash of a file file content.
    Omitted { content_hash: String },
    Inline(C),
}

impl<C: AsRef<[u8]>> FileContent<C> {
    fn as_bytes(&self) -> Option<&[u8]> {
        match self {
            FileContent::Omitted { .. } => None,
            FileContent::Inline(c) => Some(c.as_ref()),
        }
    }

    fn is_omitted(&self) -> bool {
        match self {
            FileContent::Omitted { .. } => true,
            FileContent::Inline(_) => false,
        }
    }
}

/// Struct representing the diffed file. Contains all the information
/// needed for header-generation.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DiffFile<P, C>
where
    P: AsRef<[u8]>,
    C: AsRef<[u8]>,
{
    /// file path (as [u8])
    pub path: P,
    /// file contents (as [u8])
    pub contents: FileContent<C>,
    pub file_type: FileType,
}

impl<P, C> DiffFile<P, C>
where
    P: AsRef<[u8]>,
    C: AsRef<[u8]>,
{
    pub fn new(path: P, contents: C, file_type: FileType) -> Self {
        Self {
            path,
            contents: FileContent::Inline(contents),
            file_type,
        }
    }
}

pub fn file_is_binary<P, C>(file: &Option<DiffFile<P, C>>) -> bool
where
    P: AsRef<[u8]>,
    C: AsRef<[u8]>,
{
    match file {
        Some(file) => file
            .contents
            .as_bytes()
            .map_or(false, |bytes| bytes.contains(&0)),
        None => false,
    }
}

fn file_is_omitted<P, C>(file: &Option<DiffFile<P, C>>) -> bool
where
    P: AsRef<[u8]>,
    C: AsRef<[u8]>,
{
    match file {
        Some(file) => file.contents.is_omitted(),
        None => false,
    }
}

/// Computes a diff between two files `old_file` and `new_file`,
/// the number of `context` lines can be set in `opts` struct.
///
/// In case of binary files just emits a placeholder.
///
/// `emit` callback is called many times as the parts of diff are generated.
fn gen_diff_unified<P, C, F, S>(
    old_file: Option<DiffFile<P, C>>,
    new_file: Option<DiffFile<P, C>>,
    diff_opts: DiffOpts,
    seed: S,
    reduce: F,
) -> S
where
    P: AsRef<[u8]>,
    P: Clone,
    C: AsRef<[u8]> + PartialEq + Eq,
    F: Fn(S, &[u8]) -> S,
{
    let mut state = DiffState::new(seed, reduce);
    fn file_type_to_mode(file_type: FileType) -> &'static [u8] {
        match file_type {
            FileType::Executable => b"100755",
            FileType::Symlink => b"120000",
            FileType::Regular => b"100644",
        }
    }
    if let (None, None) = (&old_file, &new_file) {
        return state.collect();
    }

    // When the files have no content differences and no metadata differences the output should be empty.
    if let (Some(old_file), Some(new_file)) = (&old_file, &new_file) {
        if old_file.file_type == new_file.file_type && diff_opts.copy_info == CopyInfo::None {
            if old_file.contents == new_file.contents {
                return state.collect();
            }
        }
    }

    let old_name = &(old_file.as_ref()).or(new_file.as_ref()).unwrap().path;
    let new_name = &(new_file.as_ref()).or(old_file.as_ref()).unwrap().path;
    state.emit(b"diff --git a/");
    state.emit(old_name.as_ref());
    state.emit(b" b/");
    state.emit(new_name.as_ref());
    state.emit(b"\n");
    match (&old_file, &new_file) {
        (None, Some(new_file)) => {
            state.emit(b"new file mode ");
            state.emit(file_type_to_mode(new_file.file_type));
            state.emit(b"\n");
        }
        (Some(old_file), None) => {
            state.emit(b"deleted file mode ");
            state.emit(file_type_to_mode(old_file.file_type));
            state.emit(b"\n");
        }
        (Some(old_file), Some(new_file)) => {
            if file_type_to_mode(old_file.file_type) != file_type_to_mode(new_file.file_type) {
                state.emit(b"old mode ");
                state.emit(file_type_to_mode(old_file.file_type));
                state.emit(b"\n");
                state.emit(b"new mode ");
                state.emit(file_type_to_mode(new_file.file_type));
                state.emit(b"\n");
            }
            match diff_opts.copy_info {
                CopyInfo::Move => {
                    state.emit(b"rename from ");
                    state.emit(old_file.path.as_ref());
                    state.emit(b"\n");
                    state.emit(b"rename to ");
                    state.emit(new_file.path.as_ref());
                    state.emit(b"\n");
                }
                CopyInfo::Copy => {
                    state.emit(b"copy from ");
                    state.emit(old_file.path.as_ref());
                    state.emit(b"\n");
                    state.emit(b"copy to ");
                    state.emit(new_file.path.as_ref());
                    state.emit(b"\n");
                }
                CopyInfo::None => {}
            }
        }
        // Impossible here.
        (None, None) => {}
    }

    // When the files have no differences we shouldn't print any further
    // headers - those are reserved for changed files.
    if let (Some(old_file), Some(new_file)) = (&old_file, &new_file) {
        if old_file.contents == new_file.contents {
            return state.collect();
        }
    }

    // Header for binary files
    if file_is_binary(&old_file)
        || file_is_binary(&new_file)
        || file_is_omitted(&old_file)
        || file_is_omitted(&new_file)
    {
        match (old_file, new_file) {
            (Some(old_file), None) => state.emit_binary_has_changed(old_file.path.as_ref()),
            (None, Some(new_file)) => state.emit_binary_has_changed(new_file.path.as_ref()),
            (Some(old_file), Some(new_file)) => {
                if old_file.path.as_ref() == new_file.path.as_ref() {
                    state.emit_binary_has_changed(new_file.path.as_ref())
                } else {
                    state.emit_binary_files_differ(old_file.path.as_ref(), new_file.path.as_ref())
                }
            }
            _ => {}
        }
        return state.collect();
    }

    // Headers for old file.
    if let Some(old_file) = &old_file {
        state.emit(b"--- a/");
        state.emit(old_file.path.as_ref());
        state.emit(b"\n");
    } else {
        state.emit(b"--- /dev/null\n");
    }
    // Headers for new file.
    if let Some(new_file) = &new_file {
        state.emit(b"+++ b/");
        state.emit(new_file.path.as_ref());
        state.emit(b"\n");
    } else {
        state.emit(b"+++ /dev/null\n");
    }
    // All headers emitted, now emit the actual diff.
    let (reduce, seed) = state.unwrap();
    match (
        &old_file.map(|file| file.contents),
        &new_file.map(|file| file.contents),
    ) {
        (Some(FileContent::Inline(old_file)), Some(FileContent::Inline(new_file))) => {
            // Typical case, we need to call actual diff function to get the diff.
            let opts = HeaderlessDiffOpts {
                context: diff_opts.context,
            };
            gen_diff_unified_headerless(old_file, new_file, opts, seed, reduce)
        }
        (Some(FileContent::Inline(old_file)), None) => {
            // Degenerated case of all-minus diff.
            gen_diff_unified_headerless_entire_file(b'-', old_file, seed, reduce)
        }
        (None, Some(FileContent::Inline(new_file))) => {
            // Degenerated case of all-plus diff.
            gen_diff_unified_headerless_entire_file(b'+', new_file, seed, reduce)
        }
        _ => {
            // There's nothing to diff.
            seed
        }
    }
}

/// Computes a diff between two files `old_file` and `new_file`,
/// the number of `context` lines can be set in `opts` struct.
///
/// Returns a vector of bytes containing the entire diff.
pub fn diff_unified<N, C>(
    old_file: Option<DiffFile<N, C>>,
    new_file: Option<DiffFile<N, C>>,
    opts: DiffOpts,
) -> Vec<u8>
where
    N: AsRef<[u8]> + Clone,
    C: AsRef<[u8]> + PartialEq + Eq,
{
    gen_diff_unified(old_file, new_file, opts, Vec::new(), |mut v, part| {
        v.extend(part);
        v
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_diff_hunks() {
        // We're repeating the example from doctest because "buck test" doesn't run it.
        let a = "a\n b\n c\n d\n";
        let b = "a\n c\n d\n e\n";
        let c = "a\n b\n c\n d\n e\n";
        assert_eq!(
            diff_hunks(a, b),
            [
                Hunk {
                    add: 1..1,
                    remove: 1..2,
                },
                Hunk {
                    add: 3..4,
                    remove: 4..4,
                }
            ]
        );
        assert_eq!(
            diff_hunks(a, c),
            [Hunk {
                add: 4..5,
                remove: 4..4
            }]
        );
        let x = diff_hunks(b, c);
        assert_eq!(x[0].add.end, 2);
    }

    #[test]
    fn test_diff_unified_headerless() {
        let a = r#"a
b
c
d
"#;
        let b = r#"a
c
d
e
z"#;
        assert_eq!(
            diff_unified_headerless(&a, &b, HeaderlessDiffOpts { context: 10 }),
            r"@@ -1,4 +1,5 @@
 a
-b
 c
 d
+e
+z
\ No newline at end of file
"
            .as_bytes()
        );
    }

    #[test]
    fn test_diff_unified() {
        let a = r#"a
b
c
d1
d2
z"#;
        let b = r#"a
b2
b3
c
d
e
z"#;
        assert_eq!(
            String::from_utf8_lossy(&diff_unified(
                Some(DiffFile {
                    contents: FileContent::Inline(&a),
                    path: "x",
                    file_type: FileType::Regular,
                }),
                Some(DiffFile {
                    contents: FileContent::Inline(&b),
                    path: "y",
                    file_type: FileType::Regular,
                }),
                DiffOpts {
                    context: 10,
                    copy_info: CopyInfo::None,
                }
            )),
            r"diff --git a/x b/y
--- a/x
+++ b/y
@@ -1,6 +1,7 @@
 a
-b
+b2
+b3
 c
-d1
-d2
+d
+e
 z
\ No newline at end of file
"
        );
    }

    #[test]
    fn test_diff_unified_file_removal() {
        let a = r#"a
b
c
d
"#;
        assert_eq!(
            String::from_utf8_lossy(&diff_unified(
                Some(DiffFile {
                    contents: FileContent::Inline(&a),
                    path: "x",
                    file_type: FileType::Regular,
                }),
                None,
                DiffOpts {
                    context: 10,
                    copy_info: CopyInfo::None,
                }
            )),
            r"diff --git a/x b/x
deleted file mode 100644
--- a/x
+++ /dev/null
@@ -1,4 +0,0 @@
-a
-b
-c
-d
"
        );
    }

    #[test]
    fn test_diff_unified_with_empty() {
        let a = r#"a
b
c
d
"#;
        assert_eq!(
            String::from_utf8_lossy(&diff_unified(
                Some(DiffFile {
                    contents: FileContent::Inline(&""),
                    path: "x",
                    file_type: FileType::Regular,
                }),
                Some(DiffFile {
                    contents: FileContent::Inline(&a),
                    path: "x",
                    file_type: FileType::Regular,
                }),
                DiffOpts {
                    context: 10,
                    copy_info: CopyInfo::None,
                }
            )),
            r"diff --git a/x b/x
--- a/x
+++ b/x
@@ -1,0 +1,4 @@
+a
+b
+c
+d
"
        );
    }

    #[test]
    fn test_diff_unified_with_large() {
        let hash1 = "hash1".to_string();
        let content_type_first: FileContent<Vec<u8>> = FileContent::Omitted {
            content_hash: hash1,
        };
        let hash2 = "hash2".to_string();
        let content_type_second: FileContent<Vec<u8>> = FileContent::Omitted {
            content_hash: hash2,
        };
        assert_eq!(
            String::from_utf8_lossy(&diff_unified(
                Some(DiffFile {
                    contents: content_type_first,
                    path: "x",
                    file_type: FileType::Regular,
                }),
                Some(DiffFile {
                    contents: content_type_second,
                    path: "x",
                    file_type: FileType::Regular,
                }),
                DiffOpts {
                    context: 10,
                    copy_info: CopyInfo::None,
                }
            )),
            r"diff --git a/x b/x
Binary file x has changed
"
        );
    }

    #[test]
    fn test_blocks() {
        assert_eq!(
            blocks(b"a\nb\nc\nd\nx\ny\nz\n", b"b\nc\nd\ne\nf\nu\nv\nw\nx\n"),
            [(1, 4, 0, 3), (4, 5, 8, 9), (7, 7, 9, 9)],
        );
    }

    #[test]
    fn test_edit_cost_small_diffs() {
        let c = |a: &str, b: &str, max_edit_cost: u64| {
            // Insert "\n" per character.
            let a = a
                .chars()
                .map(|c| format!("{c}\n"))
                .collect::<Vec<_>>()
                .concat();
            let b = b
                .chars()
                .map(|c| format!("{c}\n"))
                .collect::<Vec<_>>()
                .concat();
            edit_cost(a.as_bytes(), b.as_bytes(), max_edit_cost)
        };

        // Same content.
        assert_eq!(c("", "", 0), 0);
        assert_eq!(c("", "", 10), 0);
        assert_eq!(c("xyz", "xyz", 0), 0);
        assert_eq!(c("xyz", "xyz", 10), 0);

        // Unique lines.
        assert_eq!(c("", "xyz", 10), 3);
        assert_eq!(c("abcd", "", 10), 4);
        assert_eq!(c("abcd", "xyz", 10), 7);

        // Capped unique lines.
        assert_eq!(c("abcd", "xyz", 6), 6);
        assert_eq!(c("abcd", "xyz", 5), 5);

        // "One side empty" fast path.
        assert_eq!(c("ab", "abb", 10), 1);
        assert_eq!(c("ab", "aabb", 10), 2);
        assert_eq!(c("aabb", "ab", 10), 2);
        assert_eq!(c("aab", "ab", 10), 1);

        // No fast path - run xdl_split.
        assert_eq!(c("abc", "cba", 10), 4);
        assert_eq!(c("abcd", "dcba", 10), 6);
        assert_eq!(c("abcdefg", "gfedcba", 3), 3);

        // Mixed. xdl_split and unique lines.
        assert_eq!(c("abcxyz", "cba", 10), 7);
        assert_eq!(c("abcxyz", "cba", 4), 4);
    }

    #[test]
    fn test_edit_cost_large_diff() {
        // Large diff. Return early set by max_edit_cost.
        // Avoid allocation when generating the test case.
        // This test case is generally challenging for diff algorithms.
        let n = 10_000_000;
        let mut a: Vec<u8> = Vec::with_capacity(n);
        let mut b: Vec<u8> = Vec::with_capacity(n);
        a.resize(n, b'\n');
        b.resize(n, b'\n');
        // Range of printable ASCII characters.
        let start_byte = b' ';
        let end_byte = b'~';
        let mut a_byte = start_byte;
        let mut b_byte = end_byte;
        for i in (0..n).step_by(2) {
            a[i] = a_byte;
            b[i] = b_byte;
            a_byte += 1;
            b_byte -= 1;
            if a_byte > end_byte {
                a_byte = start_byte;
                b_byte = end_byte;
            }
        }

        // Uncomment to write the test case to disk.
        // std::fs::write("a", &a).unwrap();
        // std::fs::write("b", &b).unwrap();

        // This can complete relatively fast (run with --release to reduce test generation time).
        // When n = 10M, edit_cost takes ~0.6s, full xdiff (blocks(), or git diff --stat) takes ~68s.
        assert_eq!(edit_cost(&a, &b, 100), 100);
    }

    #[test]
    fn test_edit_cost_against_diff_hunks() {
        // Edit cost should match diff_hunks line count.
        const CHARS: [u8; 4] = [b'x', b'y', b'z', b'\n'];
        let to_slice = |bits: u8| -> [u8; 8] {
            [
                CHARS[(bits & 3) as usize],
                b'\n',
                CHARS[((bits >> 2) & 3) as usize],
                b'\n',
                CHARS[((bits >> 4) & 3) as usize],
                b'\n',
                CHARS[((bits >> 6) & 3) as usize],
                b'\n',
            ]
        };
        (0..=0xffu8).for_each(|i| {
            let a = to_slice(i);
            (0..=0xffu8).for_each(|i| {
                let b = to_slice(i);
                let cost1 = edit_cost(&a, &b, 1000);
                let cost2 = diff_hunks(&a, &b)
                    .into_iter()
                    .fold(0, |a, h| a + h.add.len() + h.remove.len())
                    as u64;
                use std::str::from_utf8;
                assert_eq!(
                    cost1,
                    cost2,
                    "edit cost does not match: {:?} {:?}",
                    from_utf8(&a).unwrap(),
                    from_utf8(&b).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.
 */

//! A simple binary that computes the diff between two files.

use std::fs;
#[cfg(target_family = "unix")]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;

use structopt::StructOpt;
use xdiff::diff_unified;
use xdiff::CopyInfo;
use xdiff::DiffFile;
use xdiff::DiffOpts;
use xdiff::FileType;

const EXEC_BIT: u32 = 0o0000100;

#[derive(Debug, StructOpt)]
#[structopt(name = "diff", about = "A showcase binary for xdiff diff library.")]

struct Opt {
    /// Input file
    #[structopt(parse(from_os_str))]
    file_a: PathBuf,

    /// Output file, stdout if not present
    #[structopt(parse(from_os_str))]
    file_b: PathBuf,

    /// Treat the <file-b> as a copy of the <file-a>
    #[structopt(short, long)]
    copy: bool,

    /// Treat the <file-b> as a move of the <file-a>
    #[structopt(short, long)]
    move_: bool,

    /// Do not follow symlinks - compare them instead (POSIX-only)
    #[structopt(short, long)]
    symlink: bool,

    /// Number of lines of unified context (default: 3)
    #[structopt(short = "U", long, default_value = "3")]
    unified: usize,
}

fn main() -> Result<(), std::io::Error> {
    let opt = Opt::from_args();

    #[cfg(target_family = "unix")]
    fn file_mode_and_contents(
        opt: &Opt,
        path: &Path,
    ) -> Result<(FileType, Vec<u8>), std::io::Error> {
        use std::ffi::OsStr;
        use std::os::unix::ffi::OsStrExt;

        if opt.symlink && path.symlink_metadata()?.file_type().is_symlink() {
            let dest = path.read_link()?;
            let dest: &OsStr = dest.as_ref();
            Ok((FileType::Symlink, dest.as_bytes().to_owned()))
        } else if (path.metadata()?.permissions().mode() & EXEC_BIT) > 0 {
            Ok((FileType::Executable, fs::read(path)?))
        } else {
            Ok((FileType::Regular, fs::read(path)?))
        }
    }

    #[cfg(target_family = "windows")]
    fn file_mode_and_contents(
        _opt: &Opt,
        path: &Path,
    ) -> Result<(FileType, Vec<u8>), std::io::Error> {
        Ok((FileType::Regular, fs::read(path)?))
    }

    let copy_info = match (opt.copy, opt.move_) {
        (true, false) => CopyInfo::Copy,
        (false, true) => CopyInfo::Move,
        (false, false) => CopyInfo::None,
        (true, true) => panic!("file can't be marked as both copy and move"),
    };

    let a_path_str = opt.file_a.to_string_lossy();
    let a = if opt.file_a.is_file() {
        let (mode, contents) = file_mode_and_contents(&opt, &opt.file_a)?;
        Some(DiffFile::new(a_path_str.as_bytes(), contents, mode))
    } else {
        None
    };
    let b_path_str = opt.file_b.to_string_lossy();
    let b = if opt.file_b.is_file() {
        let (mode, contents) = file_mode_and_contents(&opt, &opt.file_b)?;
        Some(DiffFile::new(b_path_str.as_bytes(), contents, mode))
    } else {
        None
    };

    let diff = diff_unified(
        a,
        b,
        DiffOpts {
            context: opt.unified,
            copy_info,
        },
    );

    print!("{}", String::from_utf8_lossy(&diff));
    Ok(())
}


/*
 * 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.
 */

fn main() {
    cc::Build::new()
        .files(&[
            "../third-party/xdiff/xdiffi.c",
            "../third-party/xdiff/xprepare.c",
            "../third-party/xdiff/xutils.c",
        ])
        .compile("xdiff");
}


/*
 * 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.
 */

/* automatically generated by rust-bindgen 0.65.1 */

pub const XDF_NEED_MINIMAL: u32 = 1;
pub const XDF_INDENT_HEURISTIC: u32 = 8388608;
pub const XDF_CAPPED_EDIT_COST_ONLY: u32 = 4194304;
pub const XDL_EMIT_BDIFFHUNK: u32 = 16;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct s_mmfile {
    pub ptr: *mut ::std::os::raw::c_char,
    pub size: i64,
}
#[test]
fn bindgen_test_layout_s_mmfile() {
    const UNINIT: ::std::mem::MaybeUninit<s_mmfile> = ::std::mem::MaybeUninit::uninit();
    let ptr = UNINIT.as_ptr();
    assert_eq!(
        ::std::mem::size_of::<s_mmfile>(),
        16usize,
        concat!("Size of: ", stringify!(s_mmfile))
    );
    assert_eq!(
        ::std::mem::align_of::<s_mmfile>(),
        8usize,
        concat!("Alignment of ", stringify!(s_mmfile))
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).ptr) as usize - ptr as usize },
        0usize,
        concat!(
            "Offset of field: ",
            stringify!(s_mmfile),
            "::",
            stringify!(ptr)
        )
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).size) as usize - ptr as usize },
        8usize,
        concat!(
            "Offset of field: ",
            stringify!(s_mmfile),
            "::",
            stringify!(size)
        )
    );
}
impl Default for s_mmfile {
    fn default() -> Self {
        let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
        unsafe {
            ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
            s.assume_init()
        }
    }
}
pub type mmfile_t = s_mmfile;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct s_mmbuffer {
    pub ptr: *mut ::std::os::raw::c_char,
    pub size: i64,
}
#[test]
fn bindgen_test_layout_s_mmbuffer() {
    const UNINIT: ::std::mem::MaybeUninit<s_mmbuffer> = ::std::mem::MaybeUninit::uninit();
    let ptr = UNINIT.as_ptr();
    assert_eq!(
        ::std::mem::size_of::<s_mmbuffer>(),
        16usize,
        concat!("Size of: ", stringify!(s_mmbuffer))
    );
    assert_eq!(
        ::std::mem::align_of::<s_mmbuffer>(),
        8usize,
        concat!("Alignment of ", stringify!(s_mmbuffer))
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).ptr) as usize - ptr as usize },
        0usize,
        concat!(
            "Offset of field: ",
            stringify!(s_mmbuffer),
            "::",
            stringify!(ptr)
        )
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).size) as usize - ptr as usize },
        8usize,
        concat!(
            "Offset of field: ",
            stringify!(s_mmbuffer),
            "::",
            stringify!(size)
        )
    );
}
impl Default for s_mmbuffer {
    fn default() -> Self {
        let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
        unsafe {
            ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
            s.assume_init()
        }
    }
}
pub type mmbuffer_t = s_mmbuffer;
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct s_xpparam {
    pub flags: u64,
    pub max_edit_cost: i64,
}
#[test]
fn bindgen_test_layout_s_xpparam() {
    const UNINIT: ::std::mem::MaybeUninit<s_xpparam> = ::std::mem::MaybeUninit::uninit();
    let ptr = UNINIT.as_ptr();
    assert_eq!(
        ::std::mem::size_of::<s_xpparam>(),
        16usize,
        concat!("Size of: ", stringify!(s_xpparam))
    );
    assert_eq!(
        ::std::mem::align_of::<s_xpparam>(),
        8usize,
        concat!("Alignment of ", stringify!(s_xpparam))
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).flags) as usize - ptr as usize },
        0usize,
        concat!(
            "Offset of field: ",
            stringify!(s_xpparam),
            "::",
            stringify!(flags)
        )
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).max_edit_cost) as usize - ptr as usize },
        8usize,
        concat!(
            "Offset of field: ",
            stringify!(s_xpparam),
            "::",
            stringify!(max_edit_cost)
        )
    );
}
pub type xpparam_t = s_xpparam;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct s_xdemitcb {
    pub priv_: *mut ::std::os::raw::c_void,
}
#[test]
fn bindgen_test_layout_s_xdemitcb() {
    const UNINIT: ::std::mem::MaybeUninit<s_xdemitcb> = ::std::mem::MaybeUninit::uninit();
    let ptr = UNINIT.as_ptr();
    assert_eq!(
        ::std::mem::size_of::<s_xdemitcb>(),
        8usize,
        concat!("Size of: ", stringify!(s_xdemitcb))
    );
    assert_eq!(
        ::std::mem::align_of::<s_xdemitcb>(),
        8usize,
        concat!("Alignment of ", stringify!(s_xdemitcb))
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).priv_) as usize - ptr as usize },
        0usize,
        concat!(
            "Offset of field: ",
            stringify!(s_xdemitcb),
            "::",
            stringify!(priv_)
        )
    );
}
impl Default for s_xdemitcb {
    fn default() -> Self {
        let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
        unsafe {
            ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
            s.assume_init()
        }
    }
}
pub type xdemitcb_t = s_xdemitcb;
pub type xdl_emit_hunk_consume_func_t = ::std::option::Option<
    unsafe extern "C" fn(
        start_a: i64,
        count_a: i64,
        start_b: i64,
        count_b: i64,
        cb_data: *mut ::std::os::raw::c_void,
    ) -> ::std::os::raw::c_int,
>;
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct s_xdemitconf {
    pub flags: u64,
    pub hunk_func: xdl_emit_hunk_consume_func_t,
}
#[test]
fn bindgen_test_layout_s_xdemitconf() {
    const UNINIT: ::std::mem::MaybeUninit<s_xdemitconf> = ::std::mem::MaybeUninit::uninit();
    let ptr = UNINIT.as_ptr();
    assert_eq!(
        ::std::mem::size_of::<s_xdemitconf>(),
        16usize,
        concat!("Size of: ", stringify!(s_xdemitconf))
    );
    assert_eq!(
        ::std::mem::align_of::<s_xdemitconf>(),
        8usize,
        concat!("Alignment of ", stringify!(s_xdemitconf))
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).flags) as usize - ptr as usize },
        0usize,
        concat!(
            "Offset of field: ",
            stringify!(s_xdemitconf),
            "::",
            stringify!(flags)
        )
    );
    assert_eq!(
        unsafe { ::std::ptr::addr_of!((*ptr).hunk_func) as usize - ptr as usize },
        8usize,
        concat!(
            "Offset of field: ",
            stringify!(s_xdemitconf),
            "::",
            stringify!(hunk_func)
        )
    );
}
pub type xdemitconf_t = s_xdemitconf;
extern "C" {
    pub fn xdl_mmfile_first_vendored(
        mmf: *mut mmfile_t,
        size: *mut i64,
    ) -> *mut ::std::os::raw::c_void;
}
extern "C" {
    pub fn xdl_mmfile_size_vendored(mmf: *mut mmfile_t) -> i64;
}
extern "C" {
    pub fn xdl_diff_vendored(
        mf1: *mut mmfile_t,
        mf2: *mut mmfile_t,
        xpp: *const xpparam_t,
        xecfg: *const xdemitconf_t,
        ecb: *mut xdemitcb_t,
    ) -> i64;
}


/*
 * 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.
 */

// A small subset of xdiff bindings that is just enough to run xdl_diff_vendored
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

mod bindgen;

pub use bindgen::*;

#[cfg(test)]
mod tests {
    use std::os::raw::c_char;
    use std::os::raw::c_int;
    use std::os::raw::c_void;

    use super::*;

    #[test]
    fn test_xdl_diff_vendored() {
        extern "C" fn hunk_func(a1: i64, a2: i64, b1: i64, b2: i64, _priv: *mut c_void) -> c_int {
            let mut _priv = unsafe { (_priv as *mut Vec<(i64, i64, i64, i64)>).as_mut() };
            if let Some(result) = _priv {
                result.push((a1, a2, b1, b2));
            }
            return 0;
        }

        let a = "a\nb\nc\nd\n".to_owned();
        let b = "a\nc\nd\ne\n".to_owned();
        let mut a_mmfile = mmfile_t {
            ptr: a.as_ptr() as *mut c_char,
            size: a.len() as i64,
        };
        let mut b_mmfile = mmfile_t {
            ptr: b.as_ptr() as *mut c_char,
            size: b.len() as i64,
        };
        let xpp = xpparam_t {
            flags: 0,
            max_edit_cost: 0,
        };
        let xecfg = xdemitconf_t {
            flags: 0,
            hunk_func: Some(hunk_func),
        };
        let mut result: Vec<(i64, i64, i64, i64)> = Vec::new();
        let mut ecb = xdemitcb_t {
            priv_: &mut result as *mut Vec<(i64, i64, i64, i64)> as *mut c_void,
        };

        unsafe {
            xdl_diff_vendored(&mut a_mmfile, &mut b_mmfile, &xpp, &xecfg, &mut ecb);
        }
        assert_eq!(result, [(1, 1, 1, 0), (4, 0, 3, 1)]);
    }
}


/*
 * 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.
 */

//! A simple binary that runs xdiff in a minimal way. This is mainly for
//! exposing xdiff logic so it can be used in command line for testing purpose.
//! It also serves as an example of how to use xdiff.
extern crate xdiff_sys;

use std::env;
use std::fs;
use std::os::raw::c_char;
use std::os::raw::c_int;
use std::os::raw::c_void;

use xdiff_sys::mmfile_t;
use xdiff_sys::xdemitcb_t;
use xdiff_sys::xdemitconf_t;
use xdiff_sys::xdl_diff_vendored;
use xdiff_sys::xpparam_t;

extern "C" fn hunk_func(a1: i64, a2: i64, b1: i64, b2: i64, _priv: *mut c_void) -> c_int {
    print!("@@ -{},{} +{},{} @@\n", a1, a2, b1, b2);
    return 0;
}

fn main() -> Result<(), std::io::Error> {
    let args: Vec<String> = env::args().collect();

    if args.len() < 3 {
        eprintln!("usage: {} FILE1 FILE2\n", &args[0]);
        std::process::exit(1);
    }

    let mut a = fs::read(&args[1])?;
    let mut a_mmfile = mmfile_t {
        ptr: a.as_mut_ptr() as *mut c_char,
        size: a.len() as i64,
    };
    let mut b = fs::read(&args[2])?;
    let mut b_mmfile = mmfile_t {
        ptr: b.as_mut_ptr() as *mut c_char,
        size: b.len() as i64,
    };
    let xpp = xpparam_t {
        flags: 0,
        max_edit_cost: 0,
    };
    let xecfg = xdemitconf_t {
        flags: 0,
        hunk_func: Some(hunk_func),
    };
    let mut ecb = xdemitcb_t {
        priv_: std::ptr::null_mut(),
    };

    unsafe {
        xdl_diff_vendored(&mut a_mmfile, &mut b_mmfile, &xpp, &xecfg, &mut ecb);
    }
    Ok(())
}


/*
 * 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.
 */

mod zstdelta;

pub use crate::zstdelta::apply;
pub use crate::zstdelta::diff;


/*
 * 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.
 */

mod zstdelta;

use std::env::args;
use std::fs::File;
use std::io;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;

use crate::zstdelta::apply;
use crate::zstdelta::diff;

fn read(path: &Path) -> Vec<u8> {
    let mut buf = Vec::new();
    File::open(path)
        .expect("open")
        .read_to_end(&mut buf)
        .expect("read");
    buf
}

fn main() {
    let args: Vec<_> = args().skip(1).collect();
    if args.len() < 3 {
        println!("Usage: zstdelta -c base data > delta\n       zstdelta -d base delta > data\n");
        exit(1);
    }
    let base = read(&PathBuf::from(&args[1]));
    let data = read(&PathBuf::from(&args[2]));
    let out = if args[0] == "-c" {
        diff(&base, &data).expect("diff")
    } else {
        apply(&base, &data).expect("apply")
    };

    io::stdout().write_all(&out).expect("write");
}


/*
 * 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::cmp;
use std::ffi::CStr;
use std::io;

use libc::c_void;
use zstd_sys::ZSTD_DCtx_setMaxWindowSize;
use zstd_sys::ZSTD_compressBound;
use zstd_sys::ZSTD_compress_advanced;
use zstd_sys::ZSTD_compressionParameters;
use zstd_sys::ZSTD_createCCtx;
use zstd_sys::ZSTD_createDCtx;
use zstd_sys::ZSTD_decompress_usingDict;
use zstd_sys::ZSTD_findDecompressedSize;
use zstd_sys::ZSTD_frameParameters;
use zstd_sys::ZSTD_freeCCtx;
use zstd_sys::ZSTD_freeDCtx;
use zstd_sys::ZSTD_getErrorName;
use zstd_sys::ZSTD_isError;
use zstd_sys::ZSTD_parameters;
use zstd_sys::ZSTD_strategy;
use zstd_sys::ZSTD_CHAINLOG_MIN;
use zstd_sys::ZSTD_CONTENTSIZE_ERROR;
use zstd_sys::ZSTD_CONTENTSIZE_UNKNOWN;
use zstd_sys::ZSTD_HASHLOG_MIN;
use zstd_sys::ZSTD_SEARCHLOG_MIN;
use zstd_sys::ZSTD_WINDOWLOG_MIN;

// They are complex "#define"s that are not exposed by bindgen automatically
const ZSTD_WINDOWLOG_MAX: u32 = 30;
const ZSTD_HASHLOG_MAX: u32 = 30;

/// Return `y` so `1 << y` is greater than `x`.
/// Note: `1 << y` might be greater than `u64::MAX`.
fn log_base2(x: u64) -> u32 {
    64 - x.leading_zeros()
}

/// Adjust a value so it is in the given range.
fn clamp(v: u32, min: u32, max: u32) -> u32 {
    cmp::max(min, cmp::min(v, max))
}

/// Convert zstd error code to a static string.
fn explain_error(code: usize) -> &'static str {
    unsafe {
        // ZSTD_getErrorName returns a static string.
        let name = ZSTD_getErrorName(code);
        let cstr = CStr::from_ptr(name);
        cstr.to_str().expect("zstd error is utf-8")
    }
}

/// Create a "zstd delta". Compress `data` using dictionary `base`.
pub fn diff(base: &[u8], data: &[u8]) -> io::Result<Vec<u8>> {
    // Customized wlog, hlog to let zstd do better at delta-ing. Use "fast" strategy, which is
    // good enough assuming the primary space saving is caused by "delta-ing".
    let log = log_base2((data.len() + base.len() + 1) as u64);
    let wlog = clamp(log, ZSTD_WINDOWLOG_MIN, ZSTD_WINDOWLOG_MAX);
    let hlog = clamp(log, ZSTD_HASHLOG_MIN, ZSTD_HASHLOG_MAX);
    let cparams = ZSTD_compressionParameters {
        windowLog: wlog,
        chainLog: ZSTD_CHAINLOG_MIN, // useless using "fast" strategy
        hashLog: hlog,
        searchLog: ZSTD_SEARCHLOG_MIN, // useless using "fast" strategy
        minMatch: 7,                   // level 1 default (see ZSTD_defaultCParameters)
        targetLength: 0, // enable huffman compression of literals (for "fast" strategy)
        strategy: ZSTD_strategy::ZSTD_fast,
    };
    let fparams = ZSTD_frameParameters {
        contentSizeFlag: 1, // needed by `apply`
        checksumFlag: 0,    // checksum is done at another level
        noDictIDFlag: 1,    // dictionary is fixed, not reused
    };
    let params = ZSTD_parameters {
        cParams: cparams,
        fParams: fparams,
    };

    unsafe {
        let cctx = ZSTD_createCCtx();
        if cctx.is_null() {
            return Err(io::Error::new(io::ErrorKind::Other, "cannot create CCtx"));
        }

        let max_outsize = ZSTD_compressBound(data.len());
        let mut buf: Vec<u8> = Vec::with_capacity(max_outsize);

        buf.set_len(max_outsize);
        let outsize = ZSTD_compress_advanced(
            cctx,
            buf.as_mut_ptr() as *mut c_void,
            buf.len(),
            data.as_ptr() as *const c_void,
            data.len(),
            base.as_ptr() as *const c_void,
            base.len(),
            params,
        );

        ZSTD_freeCCtx(cctx);

        if ZSTD_isError(outsize) != 0 {
            let msg = format!("cannot compress ({})", explain_error(outsize));
            Err(io::Error::new(io::ErrorKind::Other, msg))
        } else {
            buf.set_len(outsize);
            Ok(buf)
        }
    }
}

/// Apply a zstd `delta` generated by `diff` to `base`. Return reconstructed `data`.
pub fn apply(base: &[u8], delta: &[u8]) -> io::Result<Vec<u8>> {
    unsafe {
        let dctx = ZSTD_createDCtx();
        if dctx.is_null() {
            return Err(io::Error::new(io::ErrorKind::Other, "cannot create DCtx"));
        }
        ZSTD_DCtx_setMaxWindowSize(dctx, 1 << ZSTD_WINDOWLOG_MAX);

        let size = ZSTD_findDecompressedSize(delta.as_ptr() as *const c_void, delta.len()) as usize;
        if size == ZSTD_CONTENTSIZE_ERROR as usize || size == ZSTD_CONTENTSIZE_UNKNOWN as usize {
            ZSTD_freeDCtx(dctx);
            let msg = "cannot get decompress size";
            return Err(io::Error::new(io::ErrorKind::Other, msg));
        }

        let mut buf: Vec<u8> = Vec::with_capacity(size);
        buf.set_len(size);

        let outsize = ZSTD_decompress_usingDict(
            dctx,
            buf.as_mut_ptr() as *mut c_void,
            size,
            delta.as_ptr() as *const c_void,
            delta.len(),
            base.as_ptr() as *const c_void,
            base.len(),
        );
        ZSTD_freeDCtx(dctx);

        if ZSTD_isError(outsize) != 0 {
            let msg = format!("cannot decompress ({})", explain_error(outsize));
            Err(io::Error::new(io::ErrorKind::Other, msg))
        } else if outsize != size {
            let msg = format!(
                "decompress size mismatch (expected {}, got {})",
                size, outsize
            );
            Err(io::Error::new(io::ErrorKind::Other, msg))
        } else {
            Ok(buf)
        }
    }
}

#[cfg(test)]
mod tests {
    use quickcheck::quickcheck;
    use rand::RngCore;
    use rand::SeedableRng;
    use rand_chacha::ChaChaRng;

    use super::*;

    fn check_round_trip(base: &[u8], data: &[u8]) -> bool {
        let delta = diff(base, data).expect("delta");
        let reconstructed = apply(base, &delta).expect("apply");
        reconstructed[..] == data[..]
    }

    #[test]
    fn test_round_trip_manual() {
        assert!(check_round_trip(b"", b""));
        assert!(check_round_trip(b"123", b""));
        assert!(check_round_trip(b"", b"123"));
        assert!(check_round_trip(b"1234567890", b"3"));
        assert!(check_round_trip(b"3", b"1234567890"));
    }

    #[test]
    fn test_delta_efficiency() {
        // 1 MB incompressible random data
        let mut base = vec![0u8; 1000000];
        ChaChaRng::from_seed([0; 32]).fill_bytes(base.as_mut());
        // Change a few bytes
        let mut data = base.clone();
        data[0] ^= 1;
        data[10000] ^= 3;
        data[900000] ^= 7;
        let delta = diff(&base, &data).expect("diff");
        // Should generate a small delta.
        // Note: this will fail if wlog/hlog are not tweaked.
        assert!(delta.len() < 200);
    }

    quickcheck! {
        fn test_round_trip_quickcheck(a: Vec<u8>, b: Vec<u8>) -> bool {
            check_round_trip(&a, &b)
        }
    }
}


/*
 * 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.
 */

// Minimalistic error handling that just converts all errors to strings.
//
// The error type implements `std::error::Error` so it does not enforce
// callsites to use a particular library like failure or anyhow.

use std::fmt;

#[derive(Debug)]
pub struct Error(pub(crate) String);

pub type Result<T> = std::result::Result<T, Error>;

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl std::error::Error for Error {}

pub trait ForeignError: ToString {}

impl<T: ForeignError> From<T> for Error {
    fn from(err: T) -> Error {
        Error(err.to_string())
    }
}

impl ForeignError for indexedlog::Error {}
impl ForeignError for mincode::Error {}
impl ForeignError for std::io::Error {}


/*
 * 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.
 */

//! Blob store backed by zstd delta compression.

mod errors;
mod zstore;

pub use errors::Error;
pub use errors::Result;
pub use indexedlog::Repair;

pub use crate::zstore::sha1;
pub use crate::zstore::Id20;
pub use crate::zstore::OpenOptions;
pub use crate::zstore::Zstore;