<commit_before>// xfail-test
iface hax { }
impl <A> of hax for A { }

fn perform_hax<T>(x: @T) -> hax {
    x as hax
}

fn deadcode() {
    perform_hax(@"deadcode");
}

fn main() {
    let _ = perform_hax(@42);
}
<commit_msg>Un-xfail issue 2734 test<commit_after>iface hax { }
impl <A> of hax for A { }

fn perform_hax<T>(x: @T) -> hax {
    x as hax
}

fn deadcode() {
    perform_hax(@"deadcode");
}

fn main() {
    let _ = perform_hax(@42);
}
<|endoftext|>


<commit_before>// aux-build:issue-26606-macro.rs
// ignore-cross-compile
// build-aux-docs

// @has issue_26606_macro/macro.make_item.html
#[macro_use]
extern crate issue_26606_macro;

// @has issue_26606/constant.FOO.html
// @!has - '//a/@href' '../src/'
make_item!(FOO);
<commit_msg>pacify rustdoc by using better url<commit_after>// aux-build:issue-26606-macro.rs
// ignore-cross-compile
// build-aux-docs

// @has issue_26606_macro/macro.make_item.html
#[macro_use]
extern crate issue_26606_macro;

// @has issue_26606/constant.FOO.html
// @has - '//a/@href' '../src/issue_26606/auxiliary/issue-26606-macro.rs.html#3'
make_item!(FOO);
<|endoftext|>


<commit_before>#[cfg(test)]
mod tests {
    use rusqlite::{Connection, Result};
    use tagged_rusqlite::tagged_sql;

    #[test]
    fn smoke_test() -> Result<()> {
        let conn = Connection::open_in_memory()?;

        tagged_sql!(
        CreateTable,
        "CREATE TABLE person (
                  id              INTEGER PRIMARY KEY,
                  name            TEXT NOT NULL,
                  data            BLOB
                  );"
    );

        CreateTable::prepare(&conn).execute()?;

        tagged_sql!(
        InsertPerson,
        r#"INSERT INTO
                person (name, data)
                VALUES (
                    ?1 /*:name:String*/,
                    ?2 /*:data:Option<Vec<u8>>*/
                )
                RETURNINGjj id /*:i64*/;
                "#
    );

        let mut insert = InsertPerson::prepare(&conn);
        let id = insert.query_row_bind(&InsertPersonParams {
            name: "Steven".to_string(),
            data: Some(vec![1u8, 2u8]),
        })?;
        assert_eq!(id.id,1);
        let id = insert.query_row_bind(&InsertPersonParams {
            name: "John".to_string(),
            data: None,
        })?;
        assert_eq!(id.id,2);
        let id = insert.query_row_bind(&InsertPersonParams {
            name: "Bill".to_string(),
            data: None,
        })?;
        assert_eq!(id.id,3);

        tagged_sql!(
        SelectPerson,
        r#"SELECT
                            id   /*:i64*/,
                            name /*:String*/,
                            data /*:Option<Vec<u8>>*/
                            FROM person;
                            "#
    );

        let mut stmt = SelectPerson::prepare(&conn);
        let people: Result<Vec<_>, _> = stmt.query()?.collect();
        assert_eq!(people?, vec![SelectPersonRow { id: 1, name: "Steven".into(), data: Some(vec![1u8, 2u8]) },
                                 SelectPersonRow { id: 2, name: "John".into(), data: None },
                                 SelectPersonRow { id: 3, name: "Bill".into(), data: None }, ]);

        tagged_sql!(
        SearchByName,
        r#"SELECT
                            id   /*:i64*/,
                            name /*:String*/,
                            data /*:Option<Vec<u8>>*/
                            FROM person
                            WHERE name = ? /*:name:String*/;
                            "#
    );
        let search_params = SearchByNameParams {
            name: "John".into(),
        };
        let people: Result<Vec<_>, _> = SearchByName::prepare(&conn).query_bind(&search_params)?.collect();
        assert_eq!(people?, vec![
            SearchByNameRow { id: 2, name: "John".into(), data: None },
        ]);

        let mut stmt = SelectPerson::prepare(&conn);
        assert_eq!(stmt.query_row()?, SelectPersonRow { id: 1, name: "Steven".into(), data: Some(vec![1u8, 2u8]) });

        let mut stmt = SearchByName::prepare(&conn);
        let search_params = SearchByNameParams {
            name: "Bill".into(),
        };
        assert_eq!(stmt.query_row_bind(&search_params)?, SearchByNameRow { id: 3, name: "Bill".into(), data: None });
        Ok(())
    }
}<commit_msg>Upgrade Rusqlite to 0.25.<commit_after>#[cfg(test)]
mod tests {
    use rusqlite::{Connection, Result};
    use tagged_rusqlite::tagged_sql;

    #[test]
    fn smoke_test() -> Result<()> {
        let conn = Connection::open_in_memory()?;

        tagged_sql!(
        CreateTable,
        "CREATE TABLE person (
                  id              INTEGER PRIMARY KEY,
                  name            TEXT NOT NULL,
                  data            BLOB
                  );"
    );

        CreateTable::prepare(&conn).execute()?;

        tagged_sql!(
        InsertPerson,
        r#"INSERT INTO
                person (name, data)
                VALUES (
                    ?1 /*:name:String*/,
                    ?2 /*:data:Option<Vec<u8>>*/
                )
                RETURNING id /*:i64*/;
                "#
    );

        let mut insert = InsertPerson::prepare(&conn);
        let id = insert.query_row_bind(&InsertPersonParams {
            name: "Steven".to_string(),
            data: Some(vec![1u8, 2u8]),
        })?;
        assert_eq!(id.id,1);
        let id = insert.query_row_bind(&InsertPersonParams {
            name: "John".to_string(),
            data: None,
        })?;
        assert_eq!(id.id,2);
        let id = insert.query_row_bind(&InsertPersonParams {
            name: "Bill".to_string(),
            data: None,
        })?;
        assert_eq!(id.id,3);

        tagged_sql!(
        SelectPerson,
        r#"SELECT
                            id   /*:i64*/,
                            name /*:String*/,
                            data /*:Option<Vec<u8>>*/
                            FROM person;
                            "#
    );

        let mut stmt = SelectPerson::prepare(&conn);
        let people: Result<Vec<_>, _> = stmt.query()?.collect();
        assert_eq!(people?, vec![SelectPersonRow { id: 1, name: "Steven".into(), data: Some(vec![1u8, 2u8]) },
                                 SelectPersonRow { id: 2, name: "John".into(), data: None },
                                 SelectPersonRow { id: 3, name: "Bill".into(), data: None }, ]);

        tagged_sql!(
        SearchByName,
        r#"SELECT
                            id   /*:i64*/,
                            name /*:String*/,
                            data /*:Option<Vec<u8>>*/
                            FROM person
                            WHERE name = ? /*:name:String*/;
                            "#
    );
        let search_params = SearchByNameParams {
            name: "John".into(),
        };
        let people: Result<Vec<_>, _> = SearchByName::prepare(&conn).query_bind(&search_params)?.collect();
        assert_eq!(people?, vec![
            SearchByNameRow { id: 2, name: "John".into(), data: None },
        ]);

        let mut stmt = SelectPerson::prepare(&conn);
        assert_eq!(stmt.query_row()?, SelectPersonRow { id: 1, name: "Steven".into(), data: Some(vec![1u8, 2u8]) });

        let mut stmt = SearchByName::prepare(&conn);
        let search_params = SearchByNameParams {
            name: "Bill".into(),
        };
        assert_eq!(stmt.query_row_bind(&search_params)?, SearchByNameRow { id: 3, name: "Bill".into(), data: None });
        Ok(())
    }
}<|endoftext|>


<commit_before>use crate::*;

use context::Context;
use diesel::{QueryDsl, QueryResult, RunQueryDsl, SqliteConnection};
use juniper::{FieldError, FieldResult};
use schema::problems;
use std::path::PathBuf;
use turingarena::problem::driver::{ProblemDriver, ProblemPack};
use turingarena::problem::material::Material;
use turingarena::problem::ProblemName;
use user::UserId;

#[derive(Insertable)]
#[table_name = "problems"]
struct ProblemDataInput<'a> {
    name: &'a str,
}

#[derive(Queryable)]
pub struct ProblemData {
    name: String,
}

/// A problem in the contest
pub struct Problem {
    /// Raw database data of the contest
    pub data: ProblemData,

    /// Id of the user (if specified)
    pub user_id: Option<UserId>,
}

/// A problem in a contest
#[juniper::object(Context = Context)]
impl Problem {
    /// Name of this problem. Unique in the current contest.
    fn name(&self) -> ProblemName {
        ProblemName(self.data.name.clone())
    }

    /// Material of this problem
    fn material(&self, ctx: &Context) -> FieldResult<Material> {
        turingarena_task_maker::driver::IoiProblemDriver::gen_material(self.pack(ctx))
            .map_err(FieldError::from)
    }

    /// Score awards of the current user (if to be shown)
    fn scores(&self, ctx: &Context) -> FieldResult<Option<Vec<evaluation::MaxScoreAward>>> {
        let result = if let Some(UserId(user_id)) = &self.user_id {
            Some(evaluation::query_score_awards_of_user_and_problem(
                &ctx.connect_db()?,
                &user_id,
                &self.data.name,
            )?)
        } else {
            None
        };
        Ok(result)
    }

    /// Badge awards of the current user (if to be shown)
    fn badges(&self, ctx: &Context) -> FieldResult<Option<Vec<evaluation::BestBadgeAward>>> {
        let result = if let Some(UserId(user_id)) = &self.user_id {
            Some(evaluation::query_badge_awards_of_user_and_problem(
                &ctx.connect_db()?,
                &user_id,
                &self.data.name,
            )?)
        } else {
            None
        };
        Ok(result)
    }

    /// Submissions of the current user (if to be shown)
    fn submissions(&self, ctx: &Context) -> FieldResult<Option<Vec<submission::Submission>>> {
        let result = if let Some(UserId(user_id)) = &self.user_id {
            Some(submission::of_user_and_problem(
                &ctx.connect_db()?,
                &user_id,
                &self.data.name,
            )?)
        } else {
            None
        };
        Ok(result)
    }

    /// Submit a solution to the problem
    fn submit(
        &self,
        ctx: &Context,
        files: Vec<submission::FileInput>,
    ) -> FieldResult<submission::Submission> {
        if let Some(UserId(user_id)) = &self.user_id {
            let submission =
                submission::insert(&ctx.connect_db()?, &user_id, &self.data.name, files)?;
            evaluation::evaluate(self.pack(ctx), &submission, ctx.connect_db()?)?;
            Ok(submission)
        } else {
            Err(FieldError::from("Must specify a user id"))
        }
    }

    /// Indicates if the user can submit to this problem
    fn can_submit(&self) -> bool {
        self.user_id.is_some()
    }
}

impl Problem {
    /// Path of the problem
    fn path(&self, ctx: &Context) -> PathBuf {
        ctx.problems_dir.join(&self.data.name)
    }

    /// return the problem pack object
    fn pack(&self, ctx: &Context) -> ProblemPack {
        ProblemPack(std::path::PathBuf::from(&self.path(ctx)))
    }
}

/// Get a problem data by its name
pub fn by_name(conn: &SqliteConnection, name: ProblemName) -> QueryResult<ProblemData> {
    problems::table.find(name.0).first(conn)
}

/// Get all the problems data in the database
pub fn all(conn: &SqliteConnection) -> QueryResult<Vec<ProblemData>> {
    problems::table.load(conn)
}

/// Insert a problem in the database
pub fn insert(conn: &SqliteConnection, name: ProblemName) -> QueryResult<()> {
    let problem = ProblemDataInput { name: &name.0 };
    diesel::insert_into(schema::problems::table)
        .values(problem)
        .execute(conn)?;
    Ok(())
}

/// Delete a problem from the database
pub fn delete(conn: &SqliteConnection, name: ProblemName) -> QueryResult<()> {
    diesel::delete(problems::table.find(name.0)).execute(conn)?;
    Ok(())
}
<commit_msg>rust: extract `ProblemTackling` field of `Problem`<commit_after>use crate::*;

use context::Context;
use diesel::{QueryDsl, QueryResult, RunQueryDsl, SqliteConnection};
use juniper::{FieldError, FieldResult};
use schema::problems;
use std::path::PathBuf;
use turingarena::problem::driver::{ProblemDriver, ProblemPack};
use turingarena::problem::material::Material;
use turingarena::problem::ProblemName;
use user::UserId;

#[derive(Insertable)]
#[table_name = "problems"]
struct ProblemDataInput<'a> {
    name: &'a str,
}

#[derive(Queryable, Clone)]
pub struct ProblemData {
    name: String,
}

/// A problem in the contest
#[derive(Clone)]
pub struct Problem {
    /// Raw database data of the contest
    pub data: ProblemData,

    /// Id of the user (if specified)
    pub user_id: Option<UserId>,
}

/// A problem in a contest
#[juniper::object(Context = Context)]
impl Problem {
    /// Name of this problem. Unique in the current contest.
    fn name(&self) -> ProblemName {
        ProblemName(self.data.name.clone())
    }

    /// Material of this problem
    fn material(&self, ctx: &Context) -> FieldResult<Material> {
        turingarena_task_maker::driver::IoiProblemDriver::gen_material(self.pack(ctx))
            .map_err(FieldError::from)
    }

    /// Material of this problem
    fn tackling(&self, ctx: &Context) -> Option<ProblemTackling> {
        self.user_id.as_ref().map(|_| ProblemTackling { problem: Box::new((*self).clone()) })
    }
}

impl Problem {
    /// Path of the problem
    fn path(&self, ctx: &Context) -> PathBuf {
        ctx.problems_dir.join(&self.data.name)
    }

    /// return the problem pack object
    fn pack(&self, ctx: &Context) -> ProblemPack {
        ProblemPack(std::path::PathBuf::from(&self.path(ctx)))
    }
}

/// Get a problem data by its name
pub fn by_name(conn: &SqliteConnection, name: ProblemName) -> QueryResult<ProblemData> {
    problems::table.find(name.0).first(conn)
}

/// Get all the problems data in the database
pub fn all(conn: &SqliteConnection) -> QueryResult<Vec<ProblemData>> {
    problems::table.load(conn)
}

/// Insert a problem in the database
pub fn insert(conn: &SqliteConnection, name: ProblemName) -> QueryResult<()> {
    let problem = ProblemDataInput { name: &name.0 };
    diesel::insert_into(schema::problems::table)
        .values(problem)
        .execute(conn)?;
    Ok(())
}

/// Delete a problem from the database
pub fn delete(conn: &SqliteConnection, name: ProblemName) -> QueryResult<()> {
    diesel::delete(problems::table.find(name.0)).execute(conn)?;
    Ok(())
}

/// Attempts at solving a problem by a user in the contest
pub struct ProblemTackling {
    /// The problem
    pub problem: Box<Problem>,
}

impl ProblemTackling {
    fn user_id(&self) -> UserId {
        self.problem.user_id.clone().unwrap()
    }

    fn name(&self) -> &str {
        &self.problem.data.name
    }
}

/// Attempts at solving a problem by a user in the contest
#[juniper::object(Context = Context)]
impl ProblemTackling {
    /// Score awards of the current user (if to be shown)
    fn scores(&self, ctx: &Context) -> FieldResult<Vec<evaluation::MaxScoreAward>> {
        Ok(evaluation::query_score_awards_of_user_and_problem(
            &ctx.connect_db()?,
            &self.user_id().0,
            self.name(),
        )?)
    }

    /// Badge awards of the current user (if to be shown)
    fn badges(&self, ctx: &Context) -> FieldResult<Vec<evaluation::BestBadgeAward>> {
        Ok(evaluation::query_badge_awards_of_user_and_problem(
            &ctx.connect_db()?,
            &self.user_id().0,
            self.name(),
        )?)
    }

    /// Submissions of the current user (if to be shown)
    fn submissions(&self, ctx: &Context) -> FieldResult<Vec<submission::Submission>> {
        Ok(submission::of_user_and_problem(
            &ctx.connect_db()?,
            &self.user_id().0,
            self.name(),
        )?)
    }

    /// Submit a solution to the problem
    fn submit(
        &self,
        ctx: &Context,
        files: Vec<submission::FileInput>,
    ) -> FieldResult<submission::Submission> {
        let submission = submission::insert(
            &ctx.connect_db()?,
            &self.user_id().0,
            self.name(),
            files,
        )?;
        evaluation::evaluate(self.problem.pack(ctx), &submission, ctx.connect_db()?)?;
        Ok(submission)
    }

    /// Indicates if the user can submit to this problem
    fn can_submit(&self) -> bool {
        true
    }
}<|endoftext|>


<commit_before>//  * This file is part of the uutils coreutils package.
//  *
//  * For the full copyright and license information, please view the LICENSE
//  * file that was distributed with this source code.
// spell-checker:ignore ficlone reflink ftruncate pwrite
use std::fs::File;
use std::io::Read;
use std::os::unix::io::AsRawFd;
use std::path::Path;

use quick_error::ResultExt;

use crate::{CopyResult, ReflinkMode, SparseMode};

// From /usr/include/linux/fs.h:
// #define FICLONE		_IOW(0x94, 9, int)
// Use a macro as libc::ioctl expects u32 or u64 depending on the arch
macro_rules! FICLONE {
    () => {
        0x40049409
    };
}

/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone.
///
/// If `fallback` is true and there is a failure performing the clone,
/// then this function performs a standard [`std::fs::copy`]. Otherwise,
/// this function returns an error.
#[cfg(any(target_os = "linux", target_os = "android"))]
fn clone<P>(source: P, dest: P, fallback: bool) -> std::io::Result<()>
where
    P: AsRef<Path>,
{
    let src_file = File::open(&source)?;
    let dst_file = File::create(&dest)?;
    let src_fd = src_file.as_raw_fd();
    let dst_fd = dst_file.as_raw_fd();
    let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) };
    if result != 0 {
        if fallback {
            std::fs::copy(source, dest).map(|_| ())
        } else {
            Err(std::io::Error::last_os_error())
        }
    } else {
        Ok(())
    }
}

/// Perform a sparse copy from one file to another.
#[cfg(any(target_os = "linux", target_os = "android"))]
fn sparse_copy<P>(source: P, dest: P) -> std::io::Result<()>
where
    P: AsRef<Path>,
{
    use std::os::unix::prelude::MetadataExt;

    let mut src_file = File::open(source)?;
    let dst_file = File::create(dest)?;
    let dst_fd = dst_file.as_raw_fd();

    let size: usize = src_file.metadata()?.size().try_into().unwrap();
    if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
        return Err(std::io::Error::last_os_error());
    }

    let blksize = dst_file.metadata()?.blksize();
    let mut buf: Vec<u8> = vec![0; blksize.try_into().unwrap()];
    let mut current_offset: usize = 0;

    // TODO Perhaps we can employ the "fiemap ioctl" API to get the
    // file extent mappings:
    // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html
    while current_offset < size {
        let this_read = src_file.read(&mut buf)?;
        if buf.iter().any(|&x| x != 0) {
            unsafe {
                libc::pwrite(
                    dst_fd,
                    buf.as_ptr() as *const libc::c_void,
                    this_read,
                    current_offset.try_into().unwrap(),
                )
            };
        }
        current_offset += this_read;
    }
    Ok(())
}

/// Copies `source` to `dest` using copy-on-write if possible.
pub(crate) fn copy_on_write(
    source: &Path,
    dest: &Path,
    reflink_mode: ReflinkMode,
    sparse_mode: SparseMode,
    context: &str,
) -> CopyResult<()> {
    let result = match (reflink_mode, sparse_mode) {
        (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()),
        (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest),
        (ReflinkMode::Auto, _) => clone(source, dest, true),
        (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false),
        (ReflinkMode::Always, _) => {
            return Err("`--reflink=always` can be used only with --sparse=auto".into())
        }
    };
    result.context(context)?;
    Ok(())
}
<commit_msg>Add fiemap to the spelling<commit_after>//  * This file is part of the uutils coreutils package.
//  *
//  * For the full copyright and license information, please view the LICENSE
//  * file that was distributed with this source code.
// spell-checker:ignore ficlone reflink ftruncate pwrite fiemap
use std::fs::File;
use std::io::Read;
use std::os::unix::io::AsRawFd;
use std::path::Path;

use quick_error::ResultExt;

use crate::{CopyResult, ReflinkMode, SparseMode};

// From /usr/include/linux/fs.h:
// #define FICLONE		_IOW(0x94, 9, int)
// Use a macro as libc::ioctl expects u32 or u64 depending on the arch
macro_rules! FICLONE {
    () => {
        0x40049409
    };
}

/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone.
///
/// If `fallback` is true and there is a failure performing the clone,
/// then this function performs a standard [`std::fs::copy`]. Otherwise,
/// this function returns an error.
#[cfg(any(target_os = "linux", target_os = "android"))]
fn clone<P>(source: P, dest: P, fallback: bool) -> std::io::Result<()>
where
    P: AsRef<Path>,
{
    let src_file = File::open(&source)?;
    let dst_file = File::create(&dest)?;
    let src_fd = src_file.as_raw_fd();
    let dst_fd = dst_file.as_raw_fd();
    let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) };
    if result != 0 {
        if fallback {
            std::fs::copy(source, dest).map(|_| ())
        } else {
            Err(std::io::Error::last_os_error())
        }
    } else {
        Ok(())
    }
}

/// Perform a sparse copy from one file to another.
#[cfg(any(target_os = "linux", target_os = "android"))]
fn sparse_copy<P>(source: P, dest: P) -> std::io::Result<()>
where
    P: AsRef<Path>,
{
    use std::os::unix::prelude::MetadataExt;

    let mut src_file = File::open(source)?;
    let dst_file = File::create(dest)?;
    let dst_fd = dst_file.as_raw_fd();

    let size: usize = src_file.metadata()?.size().try_into().unwrap();
    if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
        return Err(std::io::Error::last_os_error());
    }

    let blksize = dst_file.metadata()?.blksize();
    let mut buf: Vec<u8> = vec![0; blksize.try_into().unwrap()];
    let mut current_offset: usize = 0;

    // TODO Perhaps we can employ the "fiemap ioctl" API to get the
    // file extent mappings:
    // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html
    while current_offset < size {
        let this_read = src_file.read(&mut buf)?;
        if buf.iter().any(|&x| x != 0) {
            unsafe {
                libc::pwrite(
                    dst_fd,
                    buf.as_ptr() as *const libc::c_void,
                    this_read,
                    current_offset.try_into().unwrap(),
                )
            };
        }
        current_offset += this_read;
    }
    Ok(())
}

/// Copies `source` to `dest` using copy-on-write if possible.
pub(crate) fn copy_on_write(
    source: &Path,
    dest: &Path,
    reflink_mode: ReflinkMode,
    sparse_mode: SparseMode,
    context: &str,
) -> CopyResult<()> {
    let result = match (reflink_mode, sparse_mode) {
        (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()),
        (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest),
        (ReflinkMode::Auto, _) => clone(source, dest, true),
        (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false),
        (ReflinkMode::Always, _) => {
            return Err("`--reflink=always` can be used only with --sparse=auto".into())
        }
    };
    result.context(context)?;
    Ok(())
}
<|endoftext|>


<commit_before>use std::collections::HashMap;
use std::path::Path;
use std::str;

use semver::Version;
use serde_json;

use core::dependency::Dependency;
use core::{PackageId, SourceId, Summary};
use sources::registry::RegistryData;
use sources::registry::{RegistryPackage, INDEX_LOCK};
use util::{internal, CargoResult, Config, Filesystem};

pub struct RegistryIndex<'cfg> {
    source_id: SourceId,
    path: Filesystem,
    cache: HashMap<&'static str, Vec<(Summary, bool)>>,
    hashes: HashMap<&'static str, HashMap<Version, String>>, // (name, vers) => cksum
    config: &'cfg Config,
    locked: bool,
}

impl<'cfg> RegistryIndex<'cfg> {
    pub fn new(
        id: &SourceId,
        path: &Filesystem,
        config: &'cfg Config,
        locked: bool,
    ) -> RegistryIndex<'cfg> {
        RegistryIndex {
            source_id: id.clone(),
            path: path.clone(),
            cache: HashMap::new(),
            hashes: HashMap::new(),
            config,
            locked,
        }
    }

    /// Return the hash listed for a specified PackageId.
    pub fn hash(&mut self, pkg: &PackageId, load: &mut RegistryData) -> CargoResult<String> {
        let name = pkg.name().as_str();
        let version = pkg.version();
        if let Some(s) = self.hashes.get(name).and_then(|v| v.get(version)) {
            return Ok(s.clone());
        }
        // Ok, we're missing the key, so parse the index file to load it.
        self.summaries(name, load)?;
        self.hashes
            .get(name)
            .and_then(|v| v.get(version))
            .ok_or_else(|| internal(format!("no hash listed for {}", pkg)))
            .map(|s| s.clone())
    }

    /// Parse the on-disk metadata for the package provided
    ///
    /// Returns a list of pairs of (summary, yanked) for the package name
    /// specified.
    pub fn summaries(
        &mut self,
        name: &'static str,
        load: &mut RegistryData,
    ) -> CargoResult<&Vec<(Summary, bool)>> {
        if self.cache.contains_key(name) {
            return Ok(&self.cache[name]);
        }
        let summaries = self.load_summaries(name, load)?;
        self.cache.insert(name, summaries);
        Ok(&self.cache[name])
    }

    fn load_summaries(
        &mut self,
        name: &str,
        load: &mut RegistryData,
    ) -> CargoResult<Vec<(Summary, bool)>> {
        // Prepare the `RegistryData` which will lazily initialize internal data
        // structures. Note that this is also importantly needed to initialize
        // to avoid deadlocks where we acquire a lock below but the `load`
        // function inside *also* wants to acquire a lock. See an instance of
        // this on #5551.
        load.prepare()?;
        let (root, _lock) = if self.locked {
            let lock = self
                .path
                .open_ro(Path::new(INDEX_LOCK), self.config, "the registry index");
            match lock {
                Ok(lock) => (lock.path().parent().unwrap().to_path_buf(), Some(lock)),
                Err(_) => return Ok(Vec::new()),
            }
        } else {
            (self.path.clone().into_path_unlocked(), None)
        };

        let fs_name = name
            .chars()
            .flat_map(|c| c.to_lowercase())
            .collect::<String>();

        // see module comment for why this is structured the way it is
        let raw_path = match fs_name.len() {
            1 => format!("1/{}", fs_name),
            2 => format!("2/{}", fs_name),
            3 => format!("3/{}/{}", &fs_name[..1], fs_name),
            _ => format!("{}/{}/{}", &fs_name[0..2], &fs_name[2..4], fs_name),
        };
        let num_hyphen_underscore = ::std::cmp::min(
            raw_path.chars().filter(|&c| c == '_' || c == '-').count(),
            10,
        ) as u16;
        let mut ret = Vec::new();
        // Crates.io treats hyphen and underscores as interchangeable
        // but, the index and old cargo do not. So the index must store uncanonicalized version
        // of the name so old cargos can find it.
        // This loop tries all possible combinations of switching
        // hyphen and underscores to find the uncanonicalized one.
        for hyphen_combination_num in 0u16..(1 << num_hyphen_underscore) {
            let path = raw_path
                .chars()
                .scan(0u16, |s, c| {
                    if (c == '_' || c == '-') && *s <= num_hyphen_underscore {
                        let out = Some(if (c == '_') ^ ((hyphen_combination_num & (1u16 << *s)) > 0) {
                            '_'
                        } else {
                            '-'
                        });
                        *s += 1;
                        out
                    } else {
                        Some(c)
                    }
                })
                .collect::<String>();
            let mut hit_closure = false;
            let err = load.load(&root, Path::new(&path), &mut |contents| {
                hit_closure = true;
                let contents = str::from_utf8(contents)
                    .map_err(|_| format_err!("registry index file was not valid utf-8"))?;
                ret.reserve(contents.lines().count());
                let lines = contents.lines().map(|s| s.trim()).filter(|l| !l.is_empty());

                let online = !self.config.cli_unstable().offline;
                // Attempt forwards-compatibility on the index by ignoring
                // everything that we ourselves don't understand, that should
                // allow future cargo implementations to break the
                // interpretation of each line here and older cargo will simply
                // ignore the new lines.
                ret.extend(lines.filter_map(|line| {
                    let (summary, locked) = match self.parse_registry_package(line) {
                        Ok(p) => p,
                        Err(e) => {
                            info!("failed to parse `{}` registry package: {}", name, e);
                            trace!("line: {}", line);
                            return None;
                        }
                    };
                    if online || load.is_crate_downloaded(summary.package_id()) {
                        Some((summary, locked))
                    } else {
                        None
                    }
                }));

                Ok(())
            });

            // We ignore lookup failures as those are just crates which don't exist
            // or we haven't updated the registry yet. If we actually ran the
            // closure though then we care about those errors.
            if hit_closure {
                err?;
                // Crates.io ensures that there is only one hyphen and underscore equivalent
                // result in the index so return when we find it.
                return Ok(ret);
            }
        }

        Ok(ret)
    }

    /// Parse a line from the registry's index file into a Summary for a
    /// package.
    ///
    /// The returned boolean is whether or not the summary has been yanked.
    fn parse_registry_package(&mut self, line: &str) -> CargoResult<(Summary, bool)> {
        let RegistryPackage {
            name,
            vers,
            cksum,
            deps,
            features,
            yanked,
            links,
        } = serde_json::from_str(line)?;
        let pkgid = PackageId::new(&name, &vers, &self.source_id)?;
        let name = pkgid.name();
        let deps = deps
            .into_iter()
            .map(|dep| dep.into_dep(&self.source_id))
            .collect::<CargoResult<Vec<_>>>()?;
        let summary = Summary::new(pkgid, deps, features, links, false)?;
        let summary = summary.set_checksum(cksum.clone());
        self.hashes
            .entry(name.as_str())
            .or_insert_with(HashMap::new)
            .insert(vers, cksum);
        Ok((summary, yanked.unwrap_or(false)))
    }

    pub fn query_inner(
        &mut self,
        dep: &Dependency,
        load: &mut RegistryData,
        f: &mut FnMut(Summary),
    ) -> CargoResult<()> {
        let source_id = self.source_id.clone();
        let summaries = self.summaries(dep.name().as_str(), load)?;
        let summaries = summaries
            .iter()
            .filter(|&&(_, yanked)| dep.source_id().precise().is_some() || !yanked)
            .map(|s| s.0.clone());

        // Handle `cargo update --precise` here. If specified, our own source
        // will have a precise version listed of the form
        // `<pkg>=<p_req>o-><f_req>` where `<pkg>` is the name of a crate on
        // this source, `<p_req>` is the version installed and `<f_req> is the
        // version requested (argument to `--precise`).
        let summaries = summaries.filter(|s| match source_id.precise() {
            Some(p) if p.starts_with(&*dep.name()) && p[dep.name().len()..].starts_with('=') => {
                let mut vers = p[dep.name().len() + 1..].splitn(2, "->");
                if dep
                    .version_req()
                    .matches(&Version::parse(vers.next().unwrap()).unwrap())
                {
                    vers.next().unwrap() == s.version().to_string()
                } else {
                    true
                }
            }
            _ => true,
        });

        for summary in summaries {
            f(summary);
        }
        Ok(())
    }
}
<commit_msg>move the Uncanonicalizing loop to a Iterator<commit_after>use std::collections::HashMap;
use std::path::Path;
use std::str;

use semver::Version;
use serde_json;

use core::dependency::Dependency;
use core::{PackageId, SourceId, Summary};
use sources::registry::RegistryData;
use sources::registry::{RegistryPackage, INDEX_LOCK};
use util::{internal, CargoResult, Config, Filesystem};

/// Crates.io treats hyphen and underscores as interchangeable
/// but, the index and old cargo do not. So the index must store uncanonicalized version
/// of the name so old cargos can find it.
/// This loop tries all possible combinations of switching
/// hyphen and underscores to find the uncanonicalized one.
/// As all stored inputs have the correct spelling, we start with the spelling as provided.
struct UncanonicalizedIter<'s> {
    input: &'s str,
    num_hyphen_underscore: u32,
    hyphen_combination_num: u16,
}

impl<'s> UncanonicalizedIter<'s> {
    fn new(input: &'s str) -> Self {
        let num_hyphen_underscore = input.chars().filter(|&c| c == '_' || c == '-').count() as u32;
        UncanonicalizedIter {
            input,
            num_hyphen_underscore,
            hyphen_combination_num: 0,
        }
    }
}

impl<'s> Iterator for UncanonicalizedIter<'s> {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.hyphen_combination_num > 0 && self.hyphen_combination_num.trailing_zeros() >= self.num_hyphen_underscore {
            return None;
        }

        let ret = Some(self.input
            .chars()
            .scan(0u16, |s, c| {
                // the check against 15 here's to prevent
                // shift overflow on inputs with more then 15 hyphens
                if (c == '_' || c == '-') && *s <= 15 {
                    let switch = (self.hyphen_combination_num & (1u16 << *s)) > 0;
                    let out = if (c == '_') ^ switch {
                        '_'
                    } else {
                        '-'
                    };
                    *s += 1;
                    Some(out)
                } else {
                    Some(c)
                }
            })
            .collect());
        self.hyphen_combination_num += 1;
        ret
    }
}

pub struct RegistryIndex<'cfg> {
    source_id: SourceId,
    path: Filesystem,
    cache: HashMap<&'static str, Vec<(Summary, bool)>>,
    hashes: HashMap<&'static str, HashMap<Version, String>>, // (name, vers) => cksum
    config: &'cfg Config,
    locked: bool,
}

impl<'cfg> RegistryIndex<'cfg> {
    pub fn new(
        id: &SourceId,
        path: &Filesystem,
        config: &'cfg Config,
        locked: bool,
    ) -> RegistryIndex<'cfg> {
        RegistryIndex {
            source_id: id.clone(),
            path: path.clone(),
            cache: HashMap::new(),
            hashes: HashMap::new(),
            config,
            locked,
        }
    }

    /// Return the hash listed for a specified PackageId.
    pub fn hash(&mut self, pkg: &PackageId, load: &mut RegistryData) -> CargoResult<String> {
        let name = pkg.name().as_str();
        let version = pkg.version();
        if let Some(s) = self.hashes.get(name).and_then(|v| v.get(version)) {
            return Ok(s.clone());
        }
        // Ok, we're missing the key, so parse the index file to load it.
        self.summaries(name, load)?;
        self.hashes
            .get(name)
            .and_then(|v| v.get(version))
            .ok_or_else(|| internal(format!("no hash listed for {}", pkg)))
            .map(|s| s.clone())
    }

    /// Parse the on-disk metadata for the package provided
    ///
    /// Returns a list of pairs of (summary, yanked) for the package name
    /// specified.
    pub fn summaries(
        &mut self,
        name: &'static str,
        load: &mut RegistryData,
    ) -> CargoResult<&Vec<(Summary, bool)>> {
        if self.cache.contains_key(name) {
            return Ok(&self.cache[name]);
        }
        let summaries = self.load_summaries(name, load)?;
        self.cache.insert(name, summaries);
        Ok(&self.cache[name])
    }

    fn load_summaries(
        &mut self,
        name: &str,
        load: &mut RegistryData,
    ) -> CargoResult<Vec<(Summary, bool)>> {
        // Prepare the `RegistryData` which will lazily initialize internal data
        // structures. Note that this is also importantly needed to initialize
        // to avoid deadlocks where we acquire a lock below but the `load`
        // function inside *also* wants to acquire a lock. See an instance of
        // this on #5551.
        load.prepare()?;
        let (root, _lock) = if self.locked {
            let lock = self
                .path
                .open_ro(Path::new(INDEX_LOCK), self.config, "the registry index");
            match lock {
                Ok(lock) => (lock.path().parent().unwrap().to_path_buf(), Some(lock)),
                Err(_) => return Ok(Vec::new()),
            }
        } else {
            (self.path.clone().into_path_unlocked(), None)
        };

        let fs_name = name
            .chars()
            .flat_map(|c| c.to_lowercase())
            .collect::<String>();

        // see module comment for why this is structured the way it is
        let raw_path = match fs_name.len() {
            1 => format!("1/{}", fs_name),
            2 => format!("2/{}", fs_name),
            3 => format!("3/{}/{}", &fs_name[..1], fs_name),
            _ => format!("{}/{}/{}", &fs_name[0..2], &fs_name[2..4], fs_name),
        };
        let mut ret = Vec::new();
         for path in UncanonicalizedIter::new(&raw_path).take(1024) {
            let mut hit_closure = false;
            let err = load.load(&root, Path::new(&path), &mut |contents| {
                hit_closure = true;
                let contents = str::from_utf8(contents)
                    .map_err(|_| format_err!("registry index file was not valid utf-8"))?;
                ret.reserve(contents.lines().count());
                let lines = contents.lines().map(|s| s.trim()).filter(|l| !l.is_empty());

                let online = !self.config.cli_unstable().offline;
                // Attempt forwards-compatibility on the index by ignoring
                // everything that we ourselves don't understand, that should
                // allow future cargo implementations to break the
                // interpretation of each line here and older cargo will simply
                // ignore the new lines.
                ret.extend(lines.filter_map(|line| {
                    let (summary, locked) = match self.parse_registry_package(line) {
                        Ok(p) => p,
                        Err(e) => {
                            info!("failed to parse `{}` registry package: {}", name, e);
                            trace!("line: {}", line);
                            return None;
                        }
                    };
                    if online || load.is_crate_downloaded(summary.package_id()) {
                        Some((summary, locked))
                    } else {
                        None
                    }
                }));

                Ok(())
            });

            // We ignore lookup failures as those are just crates which don't exist
            // or we haven't updated the registry yet. If we actually ran the
            // closure though then we care about those errors.
            if hit_closure {
                err?;
                // Crates.io ensures that there is only one hyphen and underscore equivalent
                // result in the index so return when we find it.
                return Ok(ret);
            }
        }

        Ok(ret)
    }

    /// Parse a line from the registry's index file into a Summary for a
    /// package.
    ///
    /// The returned boolean is whether or not the summary has been yanked.
    fn parse_registry_package(&mut self, line: &str) -> CargoResult<(Summary, bool)> {
        let RegistryPackage {
            name,
            vers,
            cksum,
            deps,
            features,
            yanked,
            links,
        } = serde_json::from_str(line)?;
        let pkgid = PackageId::new(&name, &vers, &self.source_id)?;
        let name = pkgid.name();
        let deps = deps
            .into_iter()
            .map(|dep| dep.into_dep(&self.source_id))
            .collect::<CargoResult<Vec<_>>>()?;
        let summary = Summary::new(pkgid, deps, features, links, false)?;
        let summary = summary.set_checksum(cksum.clone());
        self.hashes
            .entry(name.as_str())
            .or_insert_with(HashMap::new)
            .insert(vers, cksum);
        Ok((summary, yanked.unwrap_or(false)))
    }

    pub fn query_inner(
        &mut self,
        dep: &Dependency,
        load: &mut RegistryData,
        f: &mut FnMut(Summary),
    ) -> CargoResult<()> {
        let source_id = self.source_id.clone();
        let summaries = self.summaries(dep.name().as_str(), load)?;
        let summaries = summaries
            .iter()
            .filter(|&&(_, yanked)| dep.source_id().precise().is_some() || !yanked)
            .map(|s| s.0.clone());

        // Handle `cargo update --precise` here. If specified, our own source
        // will have a precise version listed of the form
        // `<pkg>=<p_req>o-><f_req>` where `<pkg>` is the name of a crate on
        // this source, `<p_req>` is the version installed and `<f_req> is the
        // version requested (argument to `--precise`).
        let summaries = summaries.filter(|s| match source_id.precise() {
            Some(p) if p.starts_with(&*dep.name()) && p[dep.name().len()..].starts_with('=') => {
                let mut vers = p[dep.name().len() + 1..].splitn(2, "->");
                if dep
                    .version_req()
                    .matches(&Version::parse(vers.next().unwrap()).unwrap())
                {
                    vers.next().unwrap() == s.version().to_string()
                } else {
                    true
                }
            }
            _ => true,
        });

        for summary in summaries {
            f(summary);
        }
        Ok(())
    }
}
<|endoftext|>


<commit_before>use crate::validators::num::*;
use clap::{App, AppSettings, Arg, Shell, SubCommand};

pub fn build_cli() -> App<'static, 'static> {
    let database_arg = Arg::with_name("DATABASE_URL")
        .long("database-url")
        .help(
            "Specifies the database URL to connect to. Falls back to \
             the DATABASE_URL environment variable if unspecified.",
        )
        .global(true)
        .takes_value(true);

    let migration_subcommand = SubCommand::with_name("migration")
        .about(
            "A group of commands for generating, running, and reverting \
             migrations.",
        )
        .setting(AppSettings::VersionlessSubcommands)
        .arg(migration_dir_arg())
        .subcommand(SubCommand::with_name("run").about("Runs all pending migrations"))
        .subcommand(
            SubCommand::with_name("revert")
                .about("Reverts the specified migrations")
                .arg(
                    Arg::with_name("REVERT_ALL")
                        .long("all")
                        .short("a")
                        .help("Reverts previously run migration files.")
                        .takes_value(false)
                        .conflicts_with("REVERT_NUMBER"),
                )
                .arg(
                    Arg::with_name("REVERT_NUMBER")
                        .long("number")
                        .short("n")
                        .help("Reverts the last `n` migration files")
                        .long_help(
                            "When this option is specified the last `n` migration files \
                             will be reverted. By default revert the last one.",
                        )
                        // TODO : when upgrading to clap 3.0 add default_value("1").
                        // Then update code in main.rs for the revert subcommand.
                        // See https://github.com/clap-rs/clap/issues/1605
                        .takes_value(true)
                        .validator(is_positive_int)
                        .conflicts_with("REVERT_ALL"),
                ),
        )
        .subcommand(
            SubCommand::with_name("redo")
                .about(
                    "Reverts and re-runs the latest migration. Useful \
                     for testing that a migration can in fact be reverted.",
                )
                .arg(
                    Arg::with_name("REDO_ALL")
                        .long("all")
                        .short("a")
                        .help("Reverts and re-runs all migrations.")
                        .long_help(
                            "When this option is specified all migrations \
                             will be reverted and re-runs. Useful for testing \
                             that your migrations can be reverted and applied.",
                        )
                        .takes_value(false)
                        .conflicts_with("REDO_NUMBER"),
                )
                .arg(
                    Arg::with_name("REDO_NUMBER")
                        .long("number")
                        .short("n")
                        .help("Redo the last `n` migration files")
                        .long_help(
                            "When this option is specified the last `n` migration files \
                             will be reverted and re-runs. By default redo the last migration.",
                        )
                        // TODO : when upgrading to clap 3.0 add default_value("1").
                        // Then update code in main.rs for the revert subcommand.
                        // See https://github.com/clap-rs/clap/issues/1605
                        .takes_value(true)
                        .validator(is_positive_int)
                        .conflicts_with("REDO_ALL"),
                ),
        )
        .subcommand(
            SubCommand::with_name("list")
                .about("Lists all available migrations, marking those that have been applied."),
        )
        .subcommand(
            SubCommand::with_name("pending")
                .about("Returns true if there are any pending migrations."),
        )
        .subcommand(
            SubCommand::with_name("generate")
                .about(
                    "Generate a new migration with the given name, and \
                     the current timestamp as the version",
                )
                .arg(
                    Arg::with_name("MIGRATION_NAME")
                        .help("The name of the migration to create")
                        .required(true),
                )
                .arg(
                    Arg::with_name("MIGRATION_VERSION")
                        .long("version")
                        .help(
                            "The version number to use when generating the migration. \
                             Defaults to the current timestamp, which should suffice \
                             for most use cases.",
                        )
                        .takes_value(true),
                )
                .arg(
                    Arg::with_name("MIGRATION_FORMAT")
                        .long("format")
                        .possible_values(&["sql", "barrel"])
                        .default_value("sql")
                        .takes_value(true)
                        .help("The format of the migration to be generated."),
                ),
        )
        .setting(AppSettings::SubcommandRequiredElseHelp);

    let setup_subcommand = SubCommand::with_name("setup")
        .arg(migration_dir_arg())
        .about(
            "Creates the migrations directory, creates the database \
             specified in your DATABASE_URL, and runs existing migrations.",
        );

    let database_subcommand = SubCommand::with_name("database")
        .alias("db")
        .arg(migration_dir_arg())
        .about("A group of commands for setting up and resetting your database.")
        .setting(AppSettings::VersionlessSubcommands)
        .subcommand(SubCommand::with_name("setup").about(
            "Creates the database specified in your DATABASE_URL, \
             and then runs any existing migrations.",
        ))
        .subcommand(SubCommand::with_name("reset").about(
            "Resets your database by dropping the database specified \
             in your DATABASE_URL and then running `diesel database setup`.",
        ))
        .subcommand(
            SubCommand::with_name("drop")
                .about("Drops the database specified in your DATABASE_URL.")
                .setting(AppSettings::Hidden),
        )
        .setting(AppSettings::SubcommandRequiredElseHelp);

    let generate_bash_completion_subcommand = SubCommand::with_name("bash-completion")
        .about("DEPRECATED: Generate bash completion script for the diesel command.");

    let generate_completions_subcommand = SubCommand::with_name("completions")
        .about("Generate shell completion scripts for the diesel command.")
        .arg(
            Arg::with_name("SHELL")
                .index(1)
                .required(true)
                .possible_values(&Shell::variants()),
        );

    let infer_schema_subcommand = SubCommand::with_name("print-schema")
        .setting(AppSettings::VersionlessSubcommands)
        .about("Print table definitions for database schema.")
        .arg(
            Arg::with_name("schema")
                .long("schema")
                .short("s")
                .takes_value(true)
                .help("The name of the schema."),
        )
        .arg(
            Arg::with_name("table-name")
                .index(1)
                .takes_value(true)
                .multiple(true)
                .help("Table names to filter (default only-tables if not empty)"),
        )
        .arg(
            Arg::with_name("only-tables")
                .short("o")
                .long("only-tables")
                .help("Only include tables from table-name that matches regexp")
                .conflicts_with("except-tables"),
        )
        .arg(
            Arg::with_name("except-tables")
                .short("e")
                .long("except-tables")
                .help("Exclude tables from table-name that matches regex")
                .conflicts_with("only-tables"),
        )
        .arg(
            Arg::with_name("with-docs")
                .long("with-docs")
                .help("Render documentation comments for tables and columns"),
        )
        .arg(
            Arg::with_name("column-sorting")
                .long("column-sorting")
                .help("Sort order for table columns")
                .takes_value(true)
                .possible_values(&["ordinal_position", "name"])
        )
        .arg(
            Arg::with_name("patch-file")
                .long("patch-file")
                .takes_value(true)
                .help("A unified diff file to be applied to the final schema"),
        )
        .arg(
            Arg::with_name("import-types")
                .long("import-types")
                .takes_value(true)
                .multiple(true)
                .number_of_values(1)
                .help("A list of types to import for every table, separated by commas"),
        );

    let config_arg = Arg::with_name("CONFIG_FILE")
        .long("config-file")
        .help(
            "The location of the configuration file to use. Falls back to the \
             `DIESEL_CONFIG_FILE` environment variable if unspecified. Defaults \
             to `diesel.toml` in your project root. See \
             diesel.rs/guides/configuring-diesel-cli for documentation on this file.",
        )
        .global(true)
        .takes_value(true);

    let locked_schema_arg = Arg::with_name("LOCKED_SCHEMA")
        .long("locked-schema")
        .help("Require that the schema file is up to date")
        .long_help(
            "When `print_schema.file` is specified in your config file, this \
             flag will cause Diesel CLI to error if any command would result in \
             changes to that file. It is recommended that you use this flag when \
             running migrations in CI or production.",
        )
        .global(true);

    App::new("diesel")
        .version(env!("CARGO_PKG_VERSION"))
        .setting(AppSettings::VersionlessSubcommands)
        .after_help(
            "You can also run `diesel SUBCOMMAND -h` to get more information about that subcommand.",
        )
        .arg(database_arg)
        .arg(config_arg)
        .arg(locked_schema_arg)
        .subcommand(migration_subcommand)
        .subcommand(setup_subcommand)
        .subcommand(database_subcommand)
        .subcommand(generate_bash_completion_subcommand)
        .subcommand(generate_completions_subcommand)
        .subcommand(infer_schema_subcommand)
        .setting(AppSettings::SubcommandRequiredElseHelp)
}

fn migration_dir_arg<'a, 'b>() -> Arg<'a, 'b> {
    Arg::with_name("MIGRATION_DIRECTORY")
        .long("migration-dir")
        .help(
            "The location of your migration directory. By default this \
             will look for a directory called `migrations` in the \
             current directory and its parents.",
        )
        .takes_value(true)
        .global(true)
}
<commit_msg>fixup! Allow overriding column sorting through args<commit_after>use crate::validators::num::*;
use clap::{App, AppSettings, Arg, Shell, SubCommand};

pub fn build_cli() -> App<'static, 'static> {
    let database_arg = Arg::with_name("DATABASE_URL")
        .long("database-url")
        .help(
            "Specifies the database URL to connect to. Falls back to \
             the DATABASE_URL environment variable if unspecified.",
        )
        .global(true)
        .takes_value(true);

    let migration_subcommand = SubCommand::with_name("migration")
        .about(
            "A group of commands for generating, running, and reverting \
             migrations.",
        )
        .setting(AppSettings::VersionlessSubcommands)
        .arg(migration_dir_arg())
        .subcommand(SubCommand::with_name("run").about("Runs all pending migrations"))
        .subcommand(
            SubCommand::with_name("revert")
                .about("Reverts the specified migrations")
                .arg(
                    Arg::with_name("REVERT_ALL")
                        .long("all")
                        .short("a")
                        .help("Reverts previously run migration files.")
                        .takes_value(false)
                        .conflicts_with("REVERT_NUMBER"),
                )
                .arg(
                    Arg::with_name("REVERT_NUMBER")
                        .long("number")
                        .short("n")
                        .help("Reverts the last `n` migration files")
                        .long_help(
                            "When this option is specified the last `n` migration files \
                             will be reverted. By default revert the last one.",
                        )
                        // TODO : when upgrading to clap 3.0 add default_value("1").
                        // Then update code in main.rs for the revert subcommand.
                        // See https://github.com/clap-rs/clap/issues/1605
                        .takes_value(true)
                        .validator(is_positive_int)
                        .conflicts_with("REVERT_ALL"),
                ),
        )
        .subcommand(
            SubCommand::with_name("redo")
                .about(
                    "Reverts and re-runs the latest migration. Useful \
                     for testing that a migration can in fact be reverted.",
                )
                .arg(
                    Arg::with_name("REDO_ALL")
                        .long("all")
                        .short("a")
                        .help("Reverts and re-runs all migrations.")
                        .long_help(
                            "When this option is specified all migrations \
                             will be reverted and re-runs. Useful for testing \
                             that your migrations can be reverted and applied.",
                        )
                        .takes_value(false)
                        .conflicts_with("REDO_NUMBER"),
                )
                .arg(
                    Arg::with_name("REDO_NUMBER")
                        .long("number")
                        .short("n")
                        .help("Redo the last `n` migration files")
                        .long_help(
                            "When this option is specified the last `n` migration files \
                             will be reverted and re-runs. By default redo the last migration.",
                        )
                        // TODO : when upgrading to clap 3.0 add default_value("1").
                        // Then update code in main.rs for the revert subcommand.
                        // See https://github.com/clap-rs/clap/issues/1605
                        .takes_value(true)
                        .validator(is_positive_int)
                        .conflicts_with("REDO_ALL"),
                ),
        )
        .subcommand(
            SubCommand::with_name("list")
                .about("Lists all available migrations, marking those that have been applied."),
        )
        .subcommand(
            SubCommand::with_name("pending")
                .about("Returns true if there are any pending migrations."),
        )
        .subcommand(
            SubCommand::with_name("generate")
                .about(
                    "Generate a new migration with the given name, and \
                     the current timestamp as the version",
                )
                .arg(
                    Arg::with_name("MIGRATION_NAME")
                        .help("The name of the migration to create")
                        .required(true),
                )
                .arg(
                    Arg::with_name("MIGRATION_VERSION")
                        .long("version")
                        .help(
                            "The version number to use when generating the migration. \
                             Defaults to the current timestamp, which should suffice \
                             for most use cases.",
                        )
                        .takes_value(true),
                )
                .arg(
                    Arg::with_name("MIGRATION_FORMAT")
                        .long("format")
                        .possible_values(&["sql", "barrel"])
                        .default_value("sql")
                        .takes_value(true)
                        .help("The format of the migration to be generated."),
                ),
        )
        .setting(AppSettings::SubcommandRequiredElseHelp);

    let setup_subcommand = SubCommand::with_name("setup")
        .arg(migration_dir_arg())
        .about(
            "Creates the migrations directory, creates the database \
             specified in your DATABASE_URL, and runs existing migrations.",
        );

    let database_subcommand = SubCommand::with_name("database")
        .alias("db")
        .arg(migration_dir_arg())
        .about("A group of commands for setting up and resetting your database.")
        .setting(AppSettings::VersionlessSubcommands)
        .subcommand(SubCommand::with_name("setup").about(
            "Creates the database specified in your DATABASE_URL, \
             and then runs any existing migrations.",
        ))
        .subcommand(SubCommand::with_name("reset").about(
            "Resets your database by dropping the database specified \
             in your DATABASE_URL and then running `diesel database setup`.",
        ))
        .subcommand(
            SubCommand::with_name("drop")
                .about("Drops the database specified in your DATABASE_URL.")
                .setting(AppSettings::Hidden),
        )
        .setting(AppSettings::SubcommandRequiredElseHelp);

    let generate_bash_completion_subcommand = SubCommand::with_name("bash-completion")
        .about("DEPRECATED: Generate bash completion script for the diesel command.");

    let generate_completions_subcommand = SubCommand::with_name("completions")
        .about("Generate shell completion scripts for the diesel command.")
        .arg(
            Arg::with_name("SHELL")
                .index(1)
                .required(true)
                .possible_values(&Shell::variants()),
        );

    let infer_schema_subcommand = SubCommand::with_name("print-schema")
        .setting(AppSettings::VersionlessSubcommands)
        .about("Print table definitions for database schema.")
        .arg(
            Arg::with_name("schema")
                .long("schema")
                .short("s")
                .takes_value(true)
                .help("The name of the schema."),
        )
        .arg(
            Arg::with_name("table-name")
                .index(1)
                .takes_value(true)
                .multiple(true)
                .help("Table names to filter (default only-tables if not empty)"),
        )
        .arg(
            Arg::with_name("only-tables")
                .short("o")
                .long("only-tables")
                .help("Only include tables from table-name that matches regexp")
                .conflicts_with("except-tables"),
        )
        .arg(
            Arg::with_name("except-tables")
                .short("e")
                .long("except-tables")
                .help("Exclude tables from table-name that matches regex")
                .conflicts_with("only-tables"),
        )
        .arg(
            Arg::with_name("with-docs")
                .long("with-docs")
                .help("Render documentation comments for tables and columns"),
        )
        .arg(
            Arg::with_name("column-sorting")
                .long("column-sorting")
                .help("Sort order for table columns")
                .takes_value(true)
                .possible_values(&["ordinal_position", "name"]),
        )
        .arg(
            Arg::with_name("patch-file")
                .long("patch-file")
                .takes_value(true)
                .help("A unified diff file to be applied to the final schema"),
        )
        .arg(
            Arg::with_name("import-types")
                .long("import-types")
                .takes_value(true)
                .multiple(true)
                .number_of_values(1)
                .help("A list of types to import for every table, separated by commas"),
        );

    let config_arg = Arg::with_name("CONFIG_FILE")
        .long("config-file")
        .help(
            "The location of the configuration file to use. Falls back to the \
             `DIESEL_CONFIG_FILE` environment variable if unspecified. Defaults \
             to `diesel.toml` in your project root. See \
             diesel.rs/guides/configuring-diesel-cli for documentation on this file.",
        )
        .global(true)
        .takes_value(true);

    let locked_schema_arg = Arg::with_name("LOCKED_SCHEMA")
        .long("locked-schema")
        .help("Require that the schema file is up to date")
        .long_help(
            "When `print_schema.file` is specified in your config file, this \
             flag will cause Diesel CLI to error if any command would result in \
             changes to that file. It is recommended that you use this flag when \
             running migrations in CI or production.",
        )
        .global(true);

    App::new("diesel")
        .version(env!("CARGO_PKG_VERSION"))
        .setting(AppSettings::VersionlessSubcommands)
        .after_help(
            "You can also run `diesel SUBCOMMAND -h` to get more information about that subcommand.",
        )
        .arg(database_arg)
        .arg(config_arg)
        .arg(locked_schema_arg)
        .subcommand(migration_subcommand)
        .subcommand(setup_subcommand)
        .subcommand(database_subcommand)
        .subcommand(generate_bash_completion_subcommand)
        .subcommand(generate_completions_subcommand)
        .subcommand(infer_schema_subcommand)
        .setting(AppSettings::SubcommandRequiredElseHelp)
}

fn migration_dir_arg<'a, 'b>() -> Arg<'a, 'b> {
    Arg::with_name("MIGRATION_DIRECTORY")
        .long("migration-dir")
        .help(
            "The location of your migration directory. By default this \
             will look for a directory called `migrations` in the \
             current directory and its parents.",
        )
        .takes_value(true)
        .global(true)
}
<|endoftext|>


<commit_before>pub mod parse;

use self::parse::{EpisodeData, SaveData, SeasonState};
use backend::{AnimeEntry, AnimeInfo, Status, SyncBackend};
use chrono::Local;
use error::SeriesError;
use input::{self, Answer};
use process;
use std::borrow::Cow;
use std::ops::Range;
use std::path::Path;

pub struct SeriesConfig<B>
where
    B: SyncBackend,
{
    pub offline_mode: bool,
    pub sync_service: B,
    pub season_num: usize,
}

pub struct FolderData {
    pub episodes: EpisodeData,
    pub savefile: SaveData,
}

impl FolderData {
    pub fn load_dir(path: &Path) -> Result<FolderData, SeriesError> {
        let mut savefile = SaveData::from_dir(path)?;
        let episodes = EpisodeData::parse_until_valid_pattern(path, &mut savefile.episode_matcher)?;

        Ok(FolderData { episodes, savefile })
    }

    pub fn save(&self) -> Result<(), SeriesError> {
        self.savefile.write_to_file()
    }

    pub fn populate_season_data<B>(&mut self, config: &SeriesConfig<B>) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        let num_seasons = self.seasons().len();

        if num_seasons > config.season_num {
            return Ok(());
        }

        for cur_season in num_seasons..=config.season_num {
            let info = self.fetch_series_info(config, cur_season)?;
            let entry = AnimeEntry::new(info);

            let season = SeasonState {
                state: entry,
                needs_info: config.offline_mode,
                needs_sync: config.offline_mode,
            };

            self.seasons_mut().push(season);
        }

        Ok(())
    }

    pub fn fetch_series_info<B>(
        &mut self,
        config: &SeriesConfig<B>,
        cur_season: usize,
    ) -> Result<AnimeInfo, SeriesError>
    where
        B: SyncBackend,
    {
        if config.offline_mode {
            // Return existing data if we already have it, otherwise return barebones info
            if self.seasons().len() > config.season_num {
                let info = self.seasons()[config.season_num].state.info.clone();
                Ok(info)
            } else {
                let mut info = AnimeInfo::default();
                info.title = self.episodes.series_name.clone();

                Ok(info)
            }
        } else {
            search_for_series_info(&config.sync_service, &self.episodes.series_name, cur_season)
        }
    }

    pub fn sync_remote_season_info<B>(
        &mut self,
        config: &SeriesConfig<B>,
    ) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        if config.season_num >= self.seasons().len() {
            return Ok(());
        }

        let mut season_data = self.seasons_mut()[config.season_num].clone();

        if season_data.needs_info {
            season_data.state.info = self.fetch_series_info(config, config.season_num)?;

            // We want to stay in a needs-sync state in offline mode so the "real" info
            // can be inserted when the series is played in online mode
            if !config.offline_mode {
                season_data.needs_info = false;
            }
        }

        // Sync data from the backend when not offline
        if !config.offline_mode {
            let entry = config
                .sync_service
                .get_list_entry(season_data.state.info.clone())?;

            if let Some(entry) = entry {
                // If we don't have new data to report, we should sync the data from the backend to keep up with
                // any changes made outside of the program
                if !season_data.needs_sync {
                    season_data.state = entry;
                }
            }
        }

        self.seasons_mut()[config.season_num] = season_data;
        Ok(())
    }

    pub fn calculate_season_offset(&self, mut range: Range<usize>) -> u32 {
        let num_seasons = self.savefile.season_states.len();
        range.start = num_seasons.min(range.start);
        range.end = num_seasons.min(range.end);

        let mut offset = 0;

        for i in range {
            let season = &self.savefile.season_states[i];

            match season.state.info.episodes {
                Some(eps) => offset += eps,
                None => return offset,
            }
        }

        offset
    }

    pub fn seasons(&self) -> &Vec<SeasonState> {
        &self.savefile.season_states
    }

    pub fn seasons_mut(&mut self) -> &mut Vec<SeasonState> {
        &mut self.savefile.season_states
    }
}

pub struct Series<B>
where
    B: SyncBackend,
{
    config: SeriesConfig<B>,
    dir: FolderData,
    ep_offset: u32,
}

impl<B> Series<B>
where
    B: SyncBackend,
{
    pub fn new(config: SeriesConfig<B>, dir: FolderData) -> Series<B> {
        Series {
            config,
            dir,
            ep_offset: 0,
        }
    }

    pub fn prepare(&mut self) -> Result<(), SeriesError> {
        self.dir.populate_season_data(&self.config)?;
        self.dir.sync_remote_season_info(&self.config)?;

        self.ep_offset = self.dir.calculate_season_offset(0..self.config.season_num);
        self.dir.save()?;

        self.prepare_list_entry()?;

        Ok(())
    }

    fn prepare_list_entry(&mut self) -> Result<(), SeriesError> {
        let status = self.cur_season().state.status;

        match status {
            Status::Watching | Status::Rewatching => Ok(()),
            Status::PlanToWatch => self.update_list_entry_status(Status::Watching),
            Status::Completed => self.update_list_entry_status(Status::Rewatching),
            Status::OnHold | Status::Dropped => self.prompt_to_watch_paused_series(),
        }
    }

    fn update_list_entry_status(&mut self, status: Status) -> Result<(), SeriesError> {
        match status {
            Status::Watching => {
                let entry = &mut self.cur_season_mut().state;

                // A series that was on hold probably already has a starting date, and it would make
                // more sense to use that one instead of replacing it
                if entry.status != Status::OnHold {
                    entry.start_date = Some(Local::today().naive_local());
                }

                entry.finish_date = None;
            }
            Status::Rewatching => {
                let entry = &mut self.cur_season_mut().state;

                println!("[{}] already completed", entry.info.title);
                println!("do you want to reset the start and end dates of the series? (Y/n)");

                if input::read_yn(Answer::Yes)? {
                    entry.start_date = Some(Local::today().naive_local());
                    entry.finish_date = None;
                }

                entry.watched_episodes = 0;
            }
            Status::Completed => {
                let entry = &mut self.cur_season_mut().state;

                if entry.finish_date.is_none() {
                    entry.finish_date = Some(Local::today().naive_local());
                }

                println!("[{}] completed!", entry.info.title);

                self.prompt_to_update_score();
            }
            Status::Dropped => {
                let entry = &mut self.cur_season_mut().state;

                if entry.finish_date.is_none() {
                    entry.finish_date = Some(Local::today().naive_local());
                }
            }
            Status::OnHold | Status::PlanToWatch => (),
        }

        let entry = &mut self.cur_season_mut().state;
        entry.status = status;

        self.update_list_entry()?;

        Ok(())
    }

    pub fn update_list_entry(&mut self) -> Result<(), SeriesError> {
        self.cur_season_mut().needs_sync = self.config.offline_mode;
        self.dir.save()?;

        if self.config.offline_mode {
            return Ok(());
        }

        self.config
            .sync_service
            .update_list_entry(&self.cur_season().state)?;

        Ok(())
    }

    pub fn play_episode(&mut self, episode: u32) -> Result<(), SeriesError> {
        let absolute_ep = self.ep_offset + episode;
        let path = self.dir.episodes.get_episode(absolute_ep)?.clone();

        let status = process::open_with_default(path).map_err(SeriesError::FailedToOpenPlayer)?;

        let entry = &mut self.cur_season_mut().state;
        entry.watched_episodes = episode.max(entry.watched_episodes);

        if !status.success() {
            eprintln!("video player not exited normally");
            eprintln!("do you still want to count the episode as completed? (y/N)");

            if !input::read_yn(Answer::No)? {
                return Ok(());
            }
        }

        match entry.info.episodes {
            Some(total_eps) if episode >= total_eps => {
                self.update_list_entry_status(Status::Completed)?;
                return Err(SeriesError::RequestExit);
            }
            _ => self.prompt_episode_completed()?,
        }

        Ok(())
    }

    pub fn play_all_episodes(&mut self) -> Result<(), SeriesError> {
        self.prepare()?;

        loop {
            let next_episode = self.cur_season().state.watched_episodes + 1;

            self.play_episode(next_episode)?;
            self.prompt_next_episode_options()?;
        }
    }

    fn prompt_episode_completed(&mut self) -> Result<(), SeriesError> {
        let entry = &self.cur_season().state;

        let total_episodes = entry
            .info
            .episodes
            .map(|e| Cow::Owned(e.to_string()))
            .unwrap_or_else(|| Cow::Borrowed("?"));

        println!(
            "[{}] episode {}/{} completed",
            entry.info.title, entry.watched_episodes, total_episodes
        );

        self.update_list_entry()?;
        Ok(())
    }

    fn prompt_next_episode_options(&mut self) -> Result<(), SeriesError> {
        let current_score_text: Cow<str> = match self.format_entry_score() {
            Some(score) => Cow::Owned(format!(" [{}]", score)),
            None => Cow::Borrowed(""),
        };

        println!("series options:");
        println!("\t[d] drop\n\t[h] put on hold\n\t[r] rate{}\n\t[x] exit\n\t[n] watch next episode (default)", current_score_text);

        let input = input::read_line()?.to_lowercase();

        match input.as_str() {
            "d" => {
                self.update_list_entry_status(Status::Dropped)?;
                Err(SeriesError::RequestExit)
            }
            "h" => {
                self.update_list_entry_status(Status::OnHold)?;
                Err(SeriesError::RequestExit)
            }
            "r" => {
                self.prompt_to_update_score();
                self.update_list_entry()?;

                self.prompt_next_episode_options()
            }
            "x" => Err(SeriesError::RequestExit),
            _ => Ok(()),
        }
    }

    fn prompt_to_update_score(&mut self) {
        let (min_score, max_score) = self.config.sync_service.formatted_score_range();

        println!(
            "enter your score between {} and {} (press return to skip):",
            min_score, max_score
        );

        // TODO: use read_range() with empty line bypassing
        let input = match input::read_line() {
            Ok(ref input) if input.is_empty() => return,
            Ok(input) => input,
            Err(err) => {
                eprintln!("failed to read score input: {}", err);
                return;
            }
        };

        match self.config.sync_service.parse_score(&input) {
            Ok(score) => {
                let entry = &mut self.cur_season_mut().state;
                entry.score = Some(score);
            }
            Err(err) => eprintln!("failed to parse score: {}", err),
        }
    }

    fn prompt_to_watch_paused_series(&mut self) -> Result<(), SeriesError> {
        let entry = &mut self.cur_season_mut().state;

        println!(
            "[{}] was previously put on hold or dropped",
            entry.info.title
        );

        println!("do you want to watch it from the beginning? (Y/n)");

        if input::read_yn(Answer::Yes)? {
            entry.watched_episodes = 0;
        }

        self.update_list_entry_status(Status::Watching)?;
        Ok(())
    }

    fn format_entry_score(&self) -> Option<String> {
        let entry = &self.cur_season().state;

        match entry.score {
            Some(score) => {
                let formatted_score = self.config.sync_service.format_score(score);

                match formatted_score {
                    Ok(score) => Some(score),
                    Err(err) => {
                        eprintln!("failed to read existing list entry score: {}", err);
                        None
                    }
                }
            }
            None => None,
        }
    }

    pub fn cur_season(&self) -> &SeasonState {
        &self.dir.seasons()[self.config.season_num]
    }

    pub fn cur_season_mut(&mut self) -> &mut SeasonState {
        &mut self.dir.seasons_mut()[self.config.season_num]
    }
}

pub fn search_for_series_info<B>(
    backend: &B,
    name: &str,
    season: usize,
) -> Result<AnimeInfo, SeriesError>
where
    B: SyncBackend,
{
    println!("[{}] searching on {}..", name, B::name());

    let mut found = backend.search_by_name(name)?;

    if !found.is_empty() {
        println!(
            "select season {} by entering the number next to its name:\n",
            1 + season
        );

        println!("0 [custom search]");

        for (i, series) in found.iter().enumerate() {
            println!("{} [{}]", 1 + i, series.title);
        }

        let index = input::read_range(0, found.len())?;

        if index == 0 {
            println!("enter the name you want to search for:");

            let name = input::read_line()?;
            search_for_series_info(backend, &name, season)
        } else {
            let info = found.swap_remove(index - 1);
            Ok(info)
        }
    } else {
        println!("no results found\nplease enter a custom search term:");

        let name = input::read_line()?;
        search_for_series_info(backend, &name, season)
    }
}
<commit_msg>[series] Add various options to display when a series is completed<commit_after>pub mod parse;

use self::parse::{EpisodeData, SaveData, SeasonState};
use backend::{AnimeEntry, AnimeInfo, Status, SyncBackend};
use chrono::Local;
use error::SeriesError;
use input::{self, Answer};
use process;
use std::borrow::Cow;
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};

pub struct SeriesConfig<B>
where
    B: SyncBackend,
{
    pub offline_mode: bool,
    pub sync_service: B,
    pub season_num: usize,
}

pub struct FolderData {
    pub episodes: EpisodeData,
    pub savefile: SaveData,
    pub path: PathBuf,
}

impl FolderData {
    pub fn load_dir(path: &Path) -> Result<FolderData, SeriesError> {
        let mut savefile = SaveData::from_dir(path)?;
        let episodes = EpisodeData::parse_until_valid_pattern(path, &mut savefile.episode_matcher)?;

        Ok(FolderData {
            episodes,
            savefile,
            path: PathBuf::from(path),
        })
    }

    pub fn save(&self) -> Result<(), SeriesError> {
        self.savefile.write_to_file()
    }

    pub fn populate_season_data<B>(&mut self, config: &SeriesConfig<B>) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        let num_seasons = self.seasons().len();

        if num_seasons > config.season_num {
            return Ok(());
        }

        for cur_season in num_seasons..=config.season_num {
            let info = self.fetch_series_info(config, cur_season)?;
            let entry = AnimeEntry::new(info);

            let season = SeasonState {
                state: entry,
                needs_info: config.offline_mode,
                needs_sync: config.offline_mode,
            };

            self.seasons_mut().push(season);
        }

        Ok(())
    }

    pub fn fetch_series_info<B>(
        &mut self,
        config: &SeriesConfig<B>,
        cur_season: usize,
    ) -> Result<AnimeInfo, SeriesError>
    where
        B: SyncBackend,
    {
        if config.offline_mode {
            // Return existing data if we already have it, otherwise return barebones info
            if self.seasons().len() > config.season_num {
                let info = self.seasons()[config.season_num].state.info.clone();
                Ok(info)
            } else {
                let mut info = AnimeInfo::default();
                info.title = self.episodes.series_name.clone();

                Ok(info)
            }
        } else {
            search_for_series_info(&config.sync_service, &self.episodes.series_name, cur_season)
        }
    }

    pub fn sync_remote_season_info<B>(
        &mut self,
        config: &SeriesConfig<B>,
    ) -> Result<(), SeriesError>
    where
        B: SyncBackend,
    {
        if config.season_num >= self.seasons().len() {
            return Ok(());
        }

        let mut season_data = self.seasons_mut()[config.season_num].clone();

        if season_data.needs_info {
            season_data.state.info = self.fetch_series_info(config, config.season_num)?;

            // We want to stay in a needs-sync state in offline mode so the "real" info
            // can be inserted when the series is played in online mode
            if !config.offline_mode {
                season_data.needs_info = false;
            }
        }

        // Sync data from the backend when not offline
        if !config.offline_mode {
            let entry = config
                .sync_service
                .get_list_entry(season_data.state.info.clone())?;

            if let Some(entry) = entry {
                // If we don't have new data to report, we should sync the data from the backend to keep up with
                // any changes made outside of the program
                if !season_data.needs_sync {
                    season_data.state = entry;
                }
            }
        }

        self.seasons_mut()[config.season_num] = season_data;
        Ok(())
    }

    pub fn calculate_season_offset(&self, mut range: Range<usize>) -> u32 {
        let num_seasons = self.savefile.season_states.len();
        range.start = num_seasons.min(range.start);
        range.end = num_seasons.min(range.end);

        let mut offset = 0;

        for i in range {
            let season = &self.savefile.season_states[i];

            match season.state.info.episodes {
                Some(eps) => offset += eps,
                None => return offset,
            }
        }

        offset
    }

    pub fn try_remove_dir(&self) {
        let path = self.path.to_string_lossy();

        println!("WARNING: {} will be deleted", path);
        println!("is this ok? (y/N)");

        match input::read_yn(Answer::No) {
            Ok(true) => match fs::remove_dir_all(&self.path) {
                Ok(_) => (),
                Err(err) => {
                    eprintln!("failed to remove directory: {}", err);
                }
            },
            Ok(false) => (),
            Err(err) => {
                eprintln!("failed to read input: {}", err);
            }
        }
    }

    pub fn seasons(&self) -> &Vec<SeasonState> {
        &self.savefile.season_states
    }

    pub fn seasons_mut(&mut self) -> &mut Vec<SeasonState> {
        &mut self.savefile.season_states
    }
}

pub struct Series<B>
where
    B: SyncBackend,
{
    config: SeriesConfig<B>,
    dir: FolderData,
    ep_offset: u32,
}

impl<B> Series<B>
where
    B: SyncBackend,
{
    pub fn new(config: SeriesConfig<B>, dir: FolderData) -> Series<B> {
        Series {
            config,
            dir,
            ep_offset: 0,
        }
    }

    pub fn prepare(&mut self) -> Result<(), SeriesError> {
        self.dir.populate_season_data(&self.config)?;
        self.dir.sync_remote_season_info(&self.config)?;

        self.ep_offset = self.dir.calculate_season_offset(0..self.config.season_num);
        self.dir.save()?;

        self.prepare_list_entry()?;

        Ok(())
    }

    fn prepare_list_entry(&mut self) -> Result<(), SeriesError> {
        let state = &self.cur_season().state;

        match state.status {
            Status::Watching | Status::Rewatching => {
                // Handle potential edge-case where all episodes have already been watched
                // but the series is still set in a watching state
                if let Some(total_eps) = state.info.episodes {
                    if state.watched_episodes >= total_eps {
                        self.update_list_entry_status(Status::Completed)?;
                        self.prompt_series_completed_options()?;
                    }
                }

                Ok(())
            }
            Status::PlanToWatch => self.update_list_entry_status(Status::Watching),
            Status::Completed => self.update_list_entry_status(Status::Rewatching),
            Status::OnHold | Status::Dropped => self.prompt_to_watch_paused_series(),
        }
    }

    fn update_list_entry_status(&mut self, status: Status) -> Result<(), SeriesError> {
        match status {
            Status::Watching => {
                let entry = &mut self.cur_season_mut().state;

                // A series that was on hold probably already has a starting date, and it would make
                // more sense to use that one instead of replacing it
                if entry.status != Status::OnHold {
                    entry.start_date = Some(Local::today().naive_local());
                }

                entry.finish_date = None;
            }
            Status::Rewatching => {
                let entry = &mut self.cur_season_mut().state;

                println!("[{}] starting rewatch", entry.info.title);
                println!("do you want to reset the start and end dates of the series? (Y/n)");

                if input::read_yn(Answer::Yes)? {
                    entry.start_date = Some(Local::today().naive_local());
                    entry.finish_date = None;
                }

                entry.watched_episodes = 0;
            }
            Status::Completed => {
                let entry = &mut self.cur_season_mut().state;

                if entry.finish_date.is_none() {
                    entry.finish_date = Some(Local::today().naive_local());
                }

                println!("[{}] completed!", entry.info.title);
            }
            Status::Dropped => {
                let entry = &mut self.cur_season_mut().state;

                if entry.finish_date.is_none() {
                    entry.finish_date = Some(Local::today().naive_local());
                }
            }
            Status::OnHold | Status::PlanToWatch => (),
        }

        let entry = &mut self.cur_season_mut().state;
        entry.status = status;

        self.update_list_entry()?;

        Ok(())
    }

    pub fn update_list_entry(&mut self) -> Result<(), SeriesError> {
        self.cur_season_mut().needs_sync = self.config.offline_mode;
        self.dir.save()?;

        if self.config.offline_mode {
            return Ok(());
        }

        self.config
            .sync_service
            .update_list_entry(&self.cur_season().state)?;

        Ok(())
    }

    pub fn play_episode(&mut self, episode: u32) -> Result<(), SeriesError> {
        let absolute_ep = self.ep_offset + episode;
        let path = self.dir.episodes.get_episode(absolute_ep)?.clone();

        let status = process::open_with_default(path).map_err(SeriesError::FailedToOpenPlayer)?;

        let entry = &mut self.cur_season_mut().state;
        entry.watched_episodes = episode.max(entry.watched_episodes);

        if !status.success() {
            eprintln!("video player not exited normally");
            eprintln!("do you still want to count the episode as completed? (y/N)");

            if !input::read_yn(Answer::No)? {
                return Ok(());
            }
        }

        match entry.info.episodes {
            Some(total_eps) if episode >= total_eps => {
                self.update_list_entry_status(Status::Completed)?;
                self.prompt_series_completed_options()?;

                return Err(SeriesError::RequestExit);
            }
            _ => self.prompt_episode_completed()?,
        }

        Ok(())
    }

    pub fn play_all_episodes(&mut self) -> Result<(), SeriesError> {
        self.prepare()?;

        loop {
            let next_episode = self.cur_season().state.watched_episodes + 1;

            self.play_episode(next_episode)?;
            self.prompt_next_episode_options()?;
        }
    }

    fn prompt_episode_completed(&mut self) -> Result<(), SeriesError> {
        let entry = &self.cur_season().state;

        let total_episodes = entry
            .info
            .episodes
            .map(|e| Cow::Owned(e.to_string()))
            .unwrap_or_else(|| Cow::Borrowed("?"));

        println!(
            "[{}] episode {}/{} completed",
            entry.info.title, entry.watched_episodes, total_episodes
        );

        self.update_list_entry()?;
        Ok(())
    }

    fn prompt_next_episode_options(&mut self) -> Result<(), SeriesError> {
        let current_score_text: Cow<str> = match self.format_entry_score() {
            Some(score) => Cow::Owned(format!(" [{}]", score)),
            None => Cow::Borrowed(""),
        };

        println!("series options:");
        println!("\t[d] drop\n\t[h] put on hold\n\t[r] rate{}\n\t[x] exit\n\t[n] watch next episode (default)", current_score_text);

        let input = input::read_line()?.to_lowercase();

        match input.as_str() {
            "d" => {
                self.update_list_entry_status(Status::Dropped)?;
                Err(SeriesError::RequestExit)
            }
            "h" => {
                self.update_list_entry_status(Status::OnHold)?;
                Err(SeriesError::RequestExit)
            }
            "r" => {
                self.prompt_to_update_score();
                self.update_list_entry()?;

                self.prompt_next_episode_options()
            }
            "x" => Err(SeriesError::RequestExit),
            _ => Ok(()),
        }
    }

    fn prompt_series_completed_options(&mut self) -> Result<(), SeriesError> {
        let current_score_text: Cow<str> = match self.format_entry_score() {
            Some(score) => Cow::Owned(format!(" [{}]", score)),
            None => Cow::Borrowed(""),
        };

        println!("series options:");
        println!(
            "\t[r] rate{}\n\t[w] rewatch\n\t[d] delete local files\n\t[x] exit",
            current_score_text
        );

        let input = input::read_line()?.to_lowercase();

        match input.as_str() {
            "r" => {
                self.prompt_to_update_score();
                self.update_list_entry()?;

                self.prompt_series_completed_options()
            }
            "w" => {
                self.update_list_entry_status(Status::Rewatching)?;
                self.play_all_episodes()
            }
            "d" => {
                self.dir.try_remove_dir();
                Ok(())
            }
            "x" => Err(SeriesError::RequestExit),
            _ => Ok(()),
        }
    }

    fn prompt_to_update_score(&mut self) {
        let (min_score, max_score) = self.config.sync_service.formatted_score_range();

        println!(
            "enter your score between {} and {} (press return to skip):",
            min_score, max_score
        );

        // TODO: use read_range() with empty line bypassing
        let input = match input::read_line() {
            Ok(ref input) if input.is_empty() => return,
            Ok(input) => input,
            Err(err) => {
                eprintln!("failed to read score input: {}", err);
                return;
            }
        };

        match self.config.sync_service.parse_score(&input) {
            Ok(score) => {
                let entry = &mut self.cur_season_mut().state;
                entry.score = Some(score);
            }
            Err(err) => eprintln!("failed to parse score: {}", err),
        }
    }

    fn prompt_to_watch_paused_series(&mut self) -> Result<(), SeriesError> {
        let entry = &mut self.cur_season_mut().state;

        println!(
            "[{}] was previously put on hold or dropped",
            entry.info.title
        );

        println!("do you want to watch it from the beginning? (Y/n)");

        if input::read_yn(Answer::Yes)? {
            entry.watched_episodes = 0;
        }

        self.update_list_entry_status(Status::Watching)?;
        Ok(())
    }

    fn format_entry_score(&self) -> Option<String> {
        let entry = &self.cur_season().state;

        match entry.score {
            Some(score) => {
                let formatted_score = self.config.sync_service.format_score(score);

                match formatted_score {
                    Ok(score) => Some(score),
                    Err(err) => {
                        eprintln!("failed to read existing list entry score: {}", err);
                        None
                    }
                }
            }
            None => None,
        }
    }

    pub fn cur_season(&self) -> &SeasonState {
        &self.dir.seasons()[self.config.season_num]
    }

    pub fn cur_season_mut(&mut self) -> &mut SeasonState {
        &mut self.dir.seasons_mut()[self.config.season_num]
    }
}

pub fn search_for_series_info<B>(
    backend: &B,
    name: &str,
    season: usize,
) -> Result<AnimeInfo, SeriesError>
where
    B: SyncBackend,
{
    println!("[{}] searching on {}..", name, B::name());

    let mut found = backend.search_by_name(name)?;

    if !found.is_empty() {
        println!(
            "select season {} by entering the number next to its name:\n",
            1 + season
        );

        println!("0 [custom search]");

        for (i, series) in found.iter().enumerate() {
            println!("{} [{}]", 1 + i, series.title);
        }

        let index = input::read_range(0, found.len())?;

        if index == 0 {
            println!("enter the name you want to search for:");

            let name = input::read_line()?;
            search_for_series_info(backend, &name, season)
        } else {
            let info = found.swap_remove(index - 1);
            Ok(info)
        }
    } else {
        println!("no results found\nplease enter a custom search term:");

        let name = input::read_line()?;
        search_for_series_info(backend, &name, season)
    }
}
<|endoftext|>