legacylisten 0.2.0

A simple CLI audio player with strange features.
Documentation
use std::{
    fmt::{self, Display, Formatter},
    mem,
    path::Path,
    sync::Arc,
};

use diskit::{diskit_extend::DiskitExt, Diskit};

use crate::{
    config::ArcConfig,
    err::Error,
    l10n::L10n,
    songs::{L10nHelper, Song, Songs},
};

#[derive(Clone, Debug)]
pub struct Csv
{
    pub entries: Vec<Vec<String>>,
}

#[derive(Debug, Clone, Copy)]
enum CsvParserState
{
    StartOfEntry,
    Normal,
    EntryQuoted,
    InEscaping,
}

impl Display for Csv
{
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error>
    {
        for line in &self.entries
        {
            let mut first = true;
            for field in line
            {
                if !first
                {
                    write!(f, ",")?;
                }
                first = false;

                // This quotes always everthing.  This isn't necessary,
                // but makes life easier.
                write!(f, "\"")?;
                for c in field.chars()
                {
                    match c
                    {
                        '\"' => write!(f, "\"\"")?,
                        _ => write!(f, "{}", c)?,
                    }
                }
                write!(f, "\"")?;
            }
            writeln!(f)?;
        }

        Ok(())
    }
}

impl Csv
{
    // Out of backwards compatibility reasons this is still here.
    fn read_pseudo_csv(buf: &[u8]) -> Result<Self, Error>
    {
        let mut csv = vec![];
        let mut entry = vec![];
        let mut field = vec![];
        let mut quoted = false;

        for &c in buf
        {
            if quoted
            {
                field.push(c);
                quoted = false;
            }
            else if c == b'\\'
            {
                quoted = true;
                continue;
            }
            else if c == b','
            {
                entry.push(String::from_utf8(mem::take(&mut field))?);
            }
            else if c == b'\n'
            {
                if !field.is_empty()
                {
                    return Err(Error::MalformattedSongsCsv);
                }
                csv.push(mem::take(&mut entry));
            }
            else
            {
                field.push(c);
            }
        }

        Ok(Self { entries: csv })
    }

    // Look in lib.rs for justification.
    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn new<P: AsRef<Path>, D>(path: P, diskit: D) -> Result<Self, Error>
    where
        D: Diskit,
    {
        // All variants are used and this is so per definitionem, so I
        // think that's better.  Also it's a pedantic lint.
        #[allow(clippy::enum_glob_use)]
        use CsvParserState::*;

        let buf = diskit.read_to_string(path)?;

        if let Ok(csv) = Self::read_pseudo_csv(buf.as_bytes())
        {
            return Ok(csv);
        }

        let mut csv = vec![];
        let mut line = vec![];
        let mut field = String::new();

        let mut state = StartOfEntry;

        for c in buf.chars()
        {
            match (state, c)
            {
                (StartOfEntry, '"') => state = EntryQuoted,
                (Normal, '"') => return Err(Error::MalformattedSongsCsv),
                (StartOfEntry | Normal | InEscaping, ',') =>
                {
                    line.push(mem::take(&mut field));
                    state = StartOfEntry;
                }
                (StartOfEntry | Normal | InEscaping, '\n') =>
                {
                    if field.is_empty() && line.is_empty()
                    {
                        // Empty lines are skiped.
                        continue;
                    }
                    line.push(mem::take(&mut field));
                    csv.push(mem::take(&mut line));
                    state = StartOfEntry;
                }
                (StartOfEntry | Normal, _) =>
                {
                    field.push(c);
                    state = Normal;
                }
                (EntryQuoted, '"') => state = InEscaping,
                (EntryQuoted, _) | (InEscaping, '"') =>
                {
                    field.push(c);
                    state = EntryQuoted;
                }
                (InEscaping, _) => return Err(Error::MalformattedSongsCsv),
            }
        }

        Ok(Self { entries: csv })
    }

    #[must_use]
    pub(crate) fn from<D>(songs: &Songs<D>) -> Self
    where
        D: Diskit,
    {
        let x = songs
            .songs
            .iter()
            .map(|song| {
                vec![
                    song.name.clone(),
                    song.num.to_string(),
                    song.loud.to_string(),
                ]
            })
            .collect();

        Self { entries: x }
    }

    #[must_use]
    pub(crate) fn get_songs<D>(self, config: Arc<ArcConfig>, l10n: L10n, diskit: D) -> Option<Songs<D>>
    where
        D: Diskit,
    {
        if self.entries.iter().all(|song| song.len() == 3)
        {
            Some(Songs {
                songs: self
                    .entries
                    .into_iter()
                    .map(|mut song| Song {
                        name: mem::take(&mut song[0]),
                        num: song[1].parse().unwrap(),
                        loud: song[2].parse().unwrap(),
                    })
                    .collect(),
                config,
                l10n_helper: L10nHelper::new(l10n),
                diskit,
            })
        }
        else
        {
            None
        }
    }
}