1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
pub mod block;
pub mod config;

pub use block::Block;
pub use config::Config;

use crate::util::Hash;
use crate::verify_features;
use eyre::{eyre, Context, Result};
use itertools::Itertools;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};

pub static ROOT_DIR: OnceCell<PathBuf> = OnceCell::new();
pub static BASE_CFG: OnceCell<Config> = OnceCell::new();

#[derive(Deserialize, Debug, Default, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Task {
    name: String,
    version: String,
    blocks: Vec<Block>,
    #[serde(default)]
    config: Config,
    #[serde(default)]
    description: String,
}

impl Task {
    pub fn new(root_dir: &Path) -> Result<Self> {
        ROOT_DIR.set(root_dir.to_owned()).unwrap();

        let path = root_dir.join("task.ron");
        let content =
            fs::read_to_string(&path).wrap_err("Failed to read task description file.")?;

        verify_features(&content)?;

        ron::from_str::<Task>(&content)
            .wrap_err_with(|| eyre!("Failed to deserialize task file ({path:?})."))?
            .init(root_dir)
    }

    pub fn init(mut self, root_dir: &Path) -> Result<Self> {
        for block in self.blocks.iter_mut() {
            block
                .init()
                .wrap_err_with(|| eyre!("Failed to verify block ({}).", block.label()))?;
        }

        for (name, count) in self.block_labels().into_iter().counts() {
            if count > 1 {
                Err(eyre!(
                    "Block names have to be unique within a task ('{name}' is repeated)."
                ))?;
            }
        }

        if self.description.is_empty() {
            let path = root_dir.join("description.txt");
            let description = fs::read_to_string(&path)
                .wrap_err_with(|| format!("Unable to open task description file ({path:?})."))?;
            self.description = description;
        }

        self.config.init()?;
        self.config.verify_checksum(self.hash())?;
        BASE_CFG.set(self.config.clone()).unwrap();

        Ok(self)
    }

    #[inline(always)]
    pub fn name(&self) -> &String {
        &self.name
    }

    #[inline(always)]
    pub fn version(&self) -> &String {
        &self.version
    }

    #[inline(always)]
    pub fn title(&self) -> String {
        format!("{} ({})", self.name, self.version)
    }

    #[inline(always)]
    pub fn config(&self) -> &Config {
        &self.config
    }

    #[inline(always)]
    pub fn block(&self, i: usize) -> &Block {
        &self.blocks[i]
    }

    pub fn block_labels(&self) -> Vec<String> {
        self.blocks.iter().map(|b| b.label().to_string()).collect()
    }

    #[inline(always)]
    pub fn description(&self) -> &str {
        &self.description
    }
}

impl Hash for Task {
    fn hash(&self) -> String {
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::default();
        let blocks: Vec<_> = self.blocks.iter().map(|b| b.hash()).collect();
        hasher.update(&serde_cbor::to_vec(&blocks).unwrap());
        hex::encode(hasher.finalize())
    }
}