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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
#![warn(missing_docs)]

//! Encode and decode Octo cartridges or "Octocarts", CHIP-8 game cartridges for the
//! [Octo](https://github.com/JohnEarnest/Octo) environment.
//!
//! Use cases:
//!
//! * Decoding: You can extract the program source code and runtime settings from an
//!   Octocart file. The source code can be assembled into CHIP-8 bytecode with Octo or
//!   [`decasm`](https://crates.io/crates/decasm). The runtime settings can be given to a CHIP-8 interpreter like Octo or
//!   [`deca`](https://crates.io/crates/deca), or saved as JSON for the [CHIP-8
//!   Archive](https://github.com/JohnEarnest/chip8Archive), as an `.octo.rc` file for C-Octo or
//!   [`termin-8`](https://crates.io/crates/termin-8), etc.
//! * Encoding: TODO
//!
//! Octo cartridge files are GIF89a images with a payload steganographically
//! embedded in one or more animation frames. Data is stored in the least significant
//! bits of colors, 1 from the red/blue channels and 2 from the green channel,
//! allowing us to pack a hidden byte into every 2 successive pixels.
//!
//! The payload consists of a 32-bit length, followed by a sequence of ASCII bytes
//! consisting of the JSON-encoded options dictionary and source text.
//!
//! An Octo cartridge contains the source code of an Octo program, and a set of
//! options for the Octo runtime on how to run the program.
//!
//! * To compile/assemble the source code, check out the [`decasm`](https://crates.io/crates/decasm) crate.
//! * To interpret an assembled program, check out the [`deca`](https://crates.io/crates/deca) crate (backend) or
//!   a program like [`termin-8`](https://crates.io/crates/termin-8) (frontend and graphics).

use octopt::Options;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::str::FromStr;
use std::u8;
use thiserror::Error;

/// Representation of the payload in the Octo cartridge.
#[derive(Serialize, Deserialize, Debug)]
pub struct OctoCart {
    /// The source code of the `.8o` file used to generated the Octocart, as a string of ASCII characters
    pub program: String,
    /// Representation of the Octo runtime settings required to run this program correctly
    pub options: Options,
}

/// Represents the types of errors that can occur during decoding of an Octocart.
#[derive(Error, Debug)]
pub enum Error {
    /// IO error while reading Octocart file
    #[error("Failed to open file")]
    IoError(#[from] std::io::Error),
    /// Decoding error while reading decoding payload from Octocart
    #[error("Failed to decode file")]
    DecodingError(#[from] gif::DecodingError),
    /// Decoding error while deserializing data from payload
    #[error("Failed to parse payload")]
    ParsingError(#[from] serde_json::Error),
    /// Palette error
    #[error("Failed to parse palette")]
    PaletteError,
}

impl FromStr for OctoCart {
    type Err = serde_json::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        serde_json::from_str(s)
    }
}

impl fmt::Display for OctoCart {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match serde_json::to_string(self) {
            Ok(string) => write!(f, "{}", string),
            _ => Err(fmt::Error),
        }
    }
}

/// Read and decode Octocart from a file path
///
/// # Errors
///
/// Returns `Err` if opening the file or decoding the Octocart fails.
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<OctoCart, Error> {
    let file = File::open(path)?;
    let string = decode_octocart(file)?;
    Ok(OctoCart::from_str(&string)?)
}

/// Decodes an Octocart, and returns the decoded JSON payload as a string.
///
/// Example
/// ```no_run
/// let file = std::fs::File::open("test_octocart.gif").unwrap();
/// let payload: String = decart::decode_octocart(file).unwrap();
/// ```
/// You can deserialize this string as an [`OctoCart`]:
/// ```no_run
/// # let payload = "{\"tickrate\":7,\"maxSize\":3215,\"screenRotation\":0,\"fontStyle\":\"octo\",\"touchInputMode\":\"none\",\"fillColor\"#FFCC00\",\"fillColor2\":\"#FF6600\",\"blendColor\":\"#662200\",\"backgroundColor\"\"#996600\",\"buzzColor\":\"#FFAA00\",\"quietColor\":\"#000000\",\"shiftQuirks\":0,\"loadStoreQuirks\":0,\"jumpQuirks\":0,\"logicQuirks\":true,\"clipQuirks\":true,\"vBlankQuirks\":true}";
/// # use std::str::FromStr;
/// use decart::OctoCart;
/// let cart: OctoCart = OctoCart::from_str(payload).unwrap();
/// ```
/// Note that you can also deserialize from a file directly with [`from_file`]:
/// ```no_run
/// use decart::*;
/// let cart: OctoCart = from_file("test_octocart.gif").unwrap();
/// ```
/// # Errors
///
/// Returns `Err` if there is a GIF decoding error.
pub fn decode_octocart<R: Read>(input: R) -> Result<String, Error> {
    let mut decoder = gif::DecodeOptions::new().read_info(input)?;
    let global_palette = decoder
        .global_palette()
        .ok_or(Error::PaletteError)?
        .to_vec();
    let mut size: u32 = 0;
    let mut first_frame = true;
    let mut json_string = String::new();

    'frame_loop: while let Some(frame) = decoder.read_next_frame()? {
        let palette = frame.palette.as_ref().unwrap_or(&global_palette);
        if first_frame {
            size = ((u32::from(byte(&frame.buffer, palette, 0))) << 24)
                | ((u32::from(byte(&frame.buffer, palette, 2))) << 16)
                | ((u32::from(byte(&frame.buffer, palette, 4))) << 8)
                | u32::from(byte(&frame.buffer, palette, 6));
            json_string = String::with_capacity(size as usize);
        }
        for pixel in (0..frame.buffer.len()).step_by(2) {
            if size == 0 {
                break 'frame_loop;
            }
            if first_frame && pixel >= 8 {
                first_frame = false;
            }
            json_string.push(byte(&frame.buffer, palette, pixel) as char);
            size -= 1;
        }
    }
    Ok(json_string)
}

fn nybble((r, g, b): (u8, u8, u8)) -> u8 {
    ((r << 3) & 8) | ((g << 1) & 6) | b & 1
}

fn byte(buffer: &[u8], palette: &[u8], i: usize) -> u8 {
    (nybble(pixel_to_color(buffer[i], palette)) << 4)
        | nybble(pixel_to_color(buffer[i + 1], palette))
}

fn pixel_to_color(pixel: u8, palette: &[u8]) -> (u8, u8, u8) {
    (
        palette[(pixel * 3) as usize],
        palette[(pixel * 3) as usize + 1],
        palette[(pixel * 3) as usize + 2],
    )
}