factorio_blueprint/
lib.rs

1use base64::{read::DecoderReader as Base64Decoder, write::EncoderWriter as Base64Encoder};
2use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression};
3use objects::{Blueprint, BlueprintBook};
4use serde::{Deserialize, Serialize};
5use std::io::prelude::*;
6use thiserror::Error;
7use version_prefix::{VersionPrefixReader, VersionPrefixWriter};
8use whitespace_remover::WhitespaceRemover;
9
10pub mod objects;
11pub mod version_prefix;
12pub mod whitespace_remover;
13
14/// `Container`s are the primary entry point for this library: they contain
15/// either a single blueprint, or a blueprint book.
16#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Container {
19    BlueprintBook(BlueprintBook),
20    Blueprint(Blueprint),
21}
22
23impl From<BlueprintBook> for Container {
24    fn from(b: BlueprintBook) -> Container {
25        Container::BlueprintBook(b)
26    }
27}
28
29impl From<Blueprint> for Container {
30    fn from(b: Blueprint) -> Container {
31        Container::Blueprint(b)
32    }
33}
34
35impl Container {
36    pub fn decode<R: Read>(reader: R) -> Result<Self> {
37        BlueprintCodec::decode(reader)
38    }
39
40    pub fn encode<W: Write>(&self, writer: W) -> Result<()> {
41        BlueprintCodec::encode(writer, self)
42    }
43}
44
45#[derive(Error, Debug)]
46pub enum Error {
47    #[error("json problem")]
48    Json(#[from] serde_json::Error),
49    #[error("failed to write valid utf8")]
50    Utf8(#[from] std::string::FromUtf8Error),
51    #[error("io troubles; probably transient")]
52    Io(#[from] std::io::Error),
53    #[error("unexpected blueprint string version byte")]
54    UnknownVersion,
55    #[error("failed to read any data")]
56    NoData,
57}
58
59pub type Result<T> = std::result::Result<T, Error>;
60
61/// Utility class which knows how to convert JSON to and from Factorio's blueprint string format.
62pub struct BlueprintCodec;
63
64impl BlueprintCodec {
65    /// writer adaptor which encodes json data to blueprint string format
66    ///
67    /// typically it is more useful to use `encode` instead, but this method
68    /// provides some extra flexibility.
69    pub fn encode_writer<W, F>(writer: W, inner: F) -> Result<()>
70    where
71        W: Write,
72        F: FnOnce(ZlibEncoder<&mut Base64Encoder<VersionPrefixWriter<W>>>) -> std::io::Result<()>,
73    {
74        // the final step before sending the data out is to prepend a 0.
75        let mut writer = VersionPrefixWriter::new('0', writer);
76        // before we prepend that 0, we need to base64-encode the stream
77        let mut writer = Base64Encoder::new(&mut writer, base64::STANDARD);
78        // note: we can't just hand this off, because we'll need to call its
79        // `finish` method later
80        {
81            // before we base64 it, we should compress it
82            let writer = ZlibEncoder::new(writer.by_ref(), Compression::new(9));
83            // hand it off to the inner closure
84            inner(writer)?;
85        }
86        writer.finish().map_err(|e| e.into())
87    }
88
89    /// write the blueprint string to the given writer
90    pub fn encode<W: Write>(writer: W, container: &Container) -> Result<()> {
91        Self::encode_writer(writer, |writer| {
92            // actually write this struct to the stream
93            serde_json::to_writer(writer, container).map_err(|e| e.into())
94        })
95    }
96
97    /// produce a new owned string containing the blueprint string
98    pub fn encode_string(container: &Container) -> Result<String> {
99        let mut out = Vec::new();
100        Self::encode(&mut out, container)?;
101        String::from_utf8(out).map_err(|e| e.into())
102    }
103
104    /// reader adaptor which decodes a blueprint string to json
105    ///
106    /// typically it is more useful to use `decode` instead, but this method
107    /// gives flexibility in the event that it is required
108    pub fn decode_reader<R, F>(reader: R, inner: F) -> Result<()>
109    where
110        R: Read,
111        F: FnOnce(
112            ZlibDecoder<Base64Decoder<VersionPrefixReader<WhitespaceRemover<R>>>>,
113        ) -> std::io::Result<()>,
114    {
115        // first, get rid of all whitespace. We know that the blueprint is
116        // base64-encoded, and that character set has no whitespace, so this
117        // just makes things a lot more robust.
118        let reader = WhitespaceRemover::new(reader);
119        // the first step is to take off the initial byte and check it
120        let mut reader = VersionPrefixReader::new('0', reader);
121        // note: we can't just hand this off, because we'll need to call its
122        // `had_expected_version` method later
123        {
124            // decode base64
125            let reader = Base64Decoder::new(reader.by_ref(), base64::STANDARD);
126            // decompress it
127            let reader = ZlibDecoder::new(reader);
128            // hand it off to the inner closure
129            inner(reader)?;
130        }
131        if !reader.had_expected_version().ok_or(Error::NoData)? {
132            Err(Error::UnknownVersion)?;
133        }
134        Ok(())
135    }
136
137    /// read the blueprint string from the given reader
138    pub fn decode<R: Read>(reader: R) -> Result<Container> {
139        let mut out = Err(Error::NoData);
140        Self::decode_reader(reader, |reader| {
141            out = serde_json::from_reader(reader).map_err(|e| e.into());
142            Ok(())
143        })?;
144        out
145    }
146
147    /// read the blueprint string from the given input
148    pub fn decode_string(blueprint: &str) -> Result<Container> {
149        Self::decode(blueprint.as_bytes())
150    }
151}