1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
7 //---------- public types ----------
9 pub use crate::prelude::Sha512_256 as Digester;
10 pub type DigestWrite<W> = digestrw::DigestWrite<Digester, W>;
12 #[derive(Copy,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
13 pub struct Hash(pub [u8; 32]);
15 #[derive(Debug,Copy,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
16 #[derive(EnumString,strum::Display,Ord,PartialOrd)]
18 #[strum(to_string="zip")] Zip,
19 // #[strum(to_string="game.toml")] GameSpec, // identification problems
21 impl Kind { pub fn only() -> Self { Kind::Zip } }
23 #[derive(Copy,Clone,Default,Debug,Hash,PartialEq,Eq,Ord,PartialOrd)]
24 #[derive(Serialize,Deserialize)]
26 pub struct Index(u16);
28 #[derive(Copy,Clone,Debug,Hash,PartialEq,Eq,Ord,PartialOrd)]
29 #[derive(Serialize,Deserialize)]
30 pub struct Id { pub index: Index, pub kind: Kind, }
32 #[derive(Debug,Clone,Default)]
33 pub struct InstanceBundles {
34 bundles: Vec<Option<Note>>,
37 pub type FileInBundleId = (Id, ZipIndex);
38 pub type SpecsInBundles = HashMap<UniCase<String>, FileInBundleId>;
40 #[derive(Debug,Clone,Serialize,Deserialize)]
46 #[derive(Debug,Clone,Serialize,Deserialize)]
50 pub hash: bundles::Hash,
53 #[derive(Debug,Clone,Serialize,Deserialize,Default)]
54 pub struct HashCache {
55 hashes: Vec<Option<Hash>>,
58 /// returned by start_upload
59 pub struct Uploading {
61 instance: Arc<InstanceName>,
62 file: DigestWrite<BufWriter<fs::File>>,
65 /// returned by start_upload
66 pub struct Uploaded<'p> {
69 for_progress_box: Box<dyn progress::Originator + 'p>,
72 #[derive(Debug,Copy,Clone,Error)]
75 pub struct NotBundle(&'static str);
77 #[derive(Error,Debug)]
79 #[error("bad bundle: {0}")] BadBundle(BadBundle),
80 #[error("internal error: {0}")] IE(#[from] IE),
85 // GameState Instance Note main file .d
86 // pieces &c libs, HashCache
89 // ABSENT unused absent no,maybe None absent absent
90 // NEARLY-ABSENT unused absent maybe Uploading absent absent
91 // WRECKAGE unused absent maybe Uploading maybe .tmp wreckage
92 // BROKEN unused absent maybe Loaded .zip populated
93 // UNUSED unused available yes,yes Loaded .zip populated
94 // USED used available yes,yes .zip populated
96 //---------- private definitions ----------
98 pub type ZipArchive = zipfile::read::ZipArchive<BufReader<File>>;
100 define_index_type!{ pub struct LibInBundleI = usize; }
105 libs: IndexVec<LibInBundleI, shapelib::Catalogue>,
106 specs: SpecsInBundles,
114 newlibs: IndexVec<LibInBundleI, ForProcessLib>,
118 struct ForProcessLib {
121 need_svgs: Vec<SvgNoted>,
124 #[derive(Debug,Clone)]
130 const BUNDLES_MAX: Index = Index(64);
132 #[derive(Debug,Clone,Serialize,Deserialize)]
138 pub type BadBundle = String;
142 #[derive(Debug,Copy,Clone)]
143 enum BundleSavefile {
145 PreviousUploadFailed(Index),
148 //---------- straightformward impls ----------
150 impl From<Index> for usize {
151 fn from(i: Index) -> usize { i.0.into() }
153 impl TryFrom<usize> for Index {
154 type Error = TryFromIntError;
155 #[throws(Self::Error)]
156 fn try_from(i: usize) -> Index { Index(i.try_into()?) }
158 impl Display for Index {
159 #[throws(fmt::Error)]
160 fn fmt(&self, f: &mut Formatter) {
161 write!(f, "{:05}", self.0)?;
164 impl FromStr for Index {
165 type Err = std::num::ParseIntError;
167 fn from_str(s: &str) -> Index { Index(u16::from_str(s)?) }
169 hformat_as_display!{Id}
171 impl From<&'static str> for NotBundle {
172 fn from(s: &'static str) -> NotBundle {
173 unsafe { mem::transmute(s) }
177 impl From<LoadError> for MgmtError {
178 fn from(le: LoadError) -> MgmtError { match le {
179 LE::BadBundle(why) => ME::BadBundle(why),
180 LE::IE(ie) => ME::from(ie),
185 fn badlib(libname: &str, e: &dyn Display) -> LoadError {
186 LE::BadBundle(format!("bad library: {}: {}", libname, e))
190 impl BundleSavefile {
191 pub fn index(&self) -> Index {
192 use BundleSavefile::*;
194 Bundle(id) => id.index,
195 &PreviousUploadFailed(index) => index,
200 format_by_fmt_hex!{Debug, for Hash, .0}
201 impl Display for Hash {
202 #[throws(fmt::Error)]
203 fn fmt(&self, f: &mut Formatter) {
204 fmt_hex(f, &self.0[0..12])?;
209 //---------- pathname handling (including Id leafname) ----------
211 pub fn b_dir(instance: &InstanceName) -> String {
212 savefilename(instance, "b-", "")
214 fn b_file<S>(instance: &InstanceName, index: Index, suffix: S) -> String
215 where S: Display + Debug
218 savefilename(instance, "b-", ""),
222 impl Display for Id {
223 #[throws(fmt::Error)]
224 fn fmt(&self, f: &mut fmt::Formatter) {
225 write!(f, "{}.{}", self.index, self.kind)?
229 impl FromStr for BundleSavefile {
230 type Err = NotBundle;
232 fn from_str(fleaf: &str) -> BundleSavefile {
233 let [lhs, rhs] = fleaf.splitn(2, '.')
234 .collect::<ArrayVec<&str,2>>()
235 .into_inner().map_err(|_| "no dot")?;
236 let index = lhs.parse().map_err(|_| "bad index")?;
237 if rhs == "tmp" { return BundleSavefile::PreviousUploadFailed(index) }
238 let kind = rhs.parse().map_err(|_| "bad extension")?;
239 BundleSavefile::Bundle(Id { index, kind })
242 impl FromStr for Id {
243 type Err = NotBundle;
245 fn from_str(fleaf: &str) -> Id {
246 match fleaf.parse()? {
247 BundleSavefile::Bundle(id) => id,
248 BundleSavefile::PreviousUploadFailed(_) => throw!(NotBundle("tmp")),
254 fn path_tmp(&self, instance: &InstanceName) -> String {
255 b_file(instance, self.index, "tmp")
258 fn path_(&self, instance: &InstanceName) -> String {
259 b_file(instance, self.index, self.kind)
262 fn path_dir(&self, instance: &InstanceName) -> String {
263 b_file(instance, self.index, "d")
266 pub fn path(&self, instance: &Unauthorised<InstanceGuard<'_>, InstanceName>,
267 auth: Authorisation<Id>) -> String {
268 self.path_(&instance.by_ref(auth.so_promise()).name)
272 pub fn open_by_name(&self, instance_name: &InstanceName,
273 _: Authorisation<Id>) -> Option<fs::File> {
274 let path = self.path_(instance_name);
275 match File::open(&path) {
277 Err(e) if e.kind() == ErrorKind::NotFound => None,
278 Err(e) => void::unreachable(
279 Err::<Void,_>(e).context(path).context("open bundle")?
285 pub fn open(&self, instance: &Instance) -> Option<fs::File> {
286 let name = &*instance.name;
287 let auth = Authorisation::promise_for(name).bundles();
288 self.open_by_name(name, auth)?
291 pub fn token(&self, instance: &Instance) -> AssetUrlToken {
292 instance.asset_url_key.token("bundle", &(&*instance.name, *self))
296 //---------- displaing/presenting/authorising ----------
299 impl Authorisation<InstanceName> {
300 fn bundles(self) -> Authorisation<Id> { self.so_promise() }
303 impl Display for State {
304 #[throws(fmt::Error)]
305 fn fmt(&self, f: &mut Formatter) {
307 State::Loaded(Loaded{ meta, size, hash }) => {
308 let BundleMeta { title, mformat:_ } = meta;
309 write!(f, "Loaded {:10} {} {:?}", size, hash, title)?;
311 other => write!(f, "{:?}", other)?,
316 impl DebugIdentify for InstanceBundles {
317 #[throws(fmt::Error)]
318 fn debug_identify_type(f: &mut fmt::Formatter) {
319 write!(f, "InstanceBundles")?;
324 impl MgmtBundleList {
326 fn info_pane(&self, ig: &Instance) -> Html {
327 #[derive(Serialize,Debug)]
329 bundles: Vec<RenderBundle>,
331 #[derive(Serialize,Debug)]
332 struct RenderBundle {
337 let bundles = self.iter().filter_map(|(&id, state)| {
338 if_let!{ State::Loaded(Loaded { meta,.. }) = state; else return None; }
339 let BundleMeta { title, mformat:_ } = meta;
340 let title = Html::from_txt(title);
341 let token = id.token(ig);
342 let url = hformat!("/_/bundle/{}/{}?{}", &*ig.name, &id, &token);
343 let id = hformat!("{}", id);
344 Some(RenderBundle { id, url, title })
347 Html::from_html_string(
348 nwtemplates::render("bundles-info-pane.tera", &RenderPane { bundles })?
353 //---------- loading ----------
355 trait ReadSeek: Read + io::Seek { }
356 impl<T> ReadSeek for T where T: Read + io::Seek { }
358 impl From<ZipError> for LoadError {
359 fn from(ze: ZipError) -> LoadError {
361 ZipError::Io(ioe) => IE::from(
362 AE::from(ioe).context("zipfile io error")
364 _ => LE::BadBundle(format!("bad zipfile: {}", ze))
369 #[derive(Debug,Deref,DerefMut)]
370 pub struct IndexedZip {
371 #[deref] #[deref_mut] za: ZipArchive,
372 members: BTreeMap<UniCase<String>, usize>,
375 #[derive(Debug,Copy,Clone,Hash,Eq,PartialEq,Ord,PartialOrd)]
376 pub struct ZipIndex(pub usize);
377 impl Display for ZipIndex {
378 #[throws(fmt::Error)]
379 fn fmt(&self, f: &mut Formatter) { Display::fmt(&self.0,f)? }
382 impl IndexedZip where {
384 pub fn new(file: File) -> Self {
385 let file = BufReader::new(file);
386 let mut za = ZipArchive::new(file)?;
387 let mut members = BTreeMap::new();
388 for i in 0..za.len() {
389 let entry = za.by_index_raw(i)?;
390 let sname = entry.name().to_owned();
391 let uname = UniCase::new(sname.to_owned());
392 if let Some(previously) = members.insert(uname, i) {
394 let previously = za.by_index_raw(previously)?;
395 let previously = previously.name();
396 throw!(LE::BadBundle(format!(
397 "duplicate files, differing only in case, {:?} vs {:?}",
402 IndexedZip { za, members }
406 pub fn by_name_caseless<'a, S>(&'a mut self, name: S) -> Option<ZipFile<'a>>
407 where S: Into<String>
409 if_let!{ Some(&i) = self.members.get(&UniCase::new(name.into()));
410 else return Ok(None) }
411 Some(self.za.by_index(i)?)
418 fn i<'z>(&'z mut self, i: ZipIndex) -> ZipFile<'z> {
423 impl<'z> IntoIterator for &'z IndexedZip {
424 type Item = (&'z UniCase<String>, ZipIndex);
425 type IntoIter = Box<dyn Iterator<Item=Self::Item> + 'z>;
426 fn into_iter(self) -> Self::IntoIter {
428 self.members.iter().map(|(name,&index)| (name, ZipIndex(index)))
433 trait BundleParseErrorHandling: Copy {
435 fn required<XE,T,F>(self, f:F) -> Result<T, Self::Err>
436 where XE: Into<LoadError>,
437 F: FnOnce() -> Result<T,XE>;
439 fn besteffort<XE,T,F,G>(self, f:F, g:G) -> Result<T, Self::Err>
440 where XE: Into<LoadError>,
441 F: FnOnce() -> Result<T,XE>,
445 #[derive(Debug,Error)]
448 Unloadable(BadBundle),
450 display_as_debug!{ReloadError}
452 #[derive(Debug,Copy,Clone)]
453 struct BundleParseReload<'s>{ pub bpath: &'s str }
454 impl BundleParseErrorHandling for BundleParseReload<'_> {
455 type Err = ReloadError;
456 fn required<XE,T,F>(self, f:F) -> Result<T,ReloadError>
457 where XE: Into<LoadError>,
458 F: FnOnce() -> Result<T,XE>
460 use ReloadError as RLE;
462 let le: LE = xe.into();
464 LE::BadBundle(why) => RLE::Unloadable(
465 format!("{}: {}", self.bpath, &why)
467 LE::IE(IE::Anyhow(ae)) => RLE::IE(IE::Anyhow(
468 ae.context(self.bpath.to_owned())
470 LE::IE(ie) => RLE::IE(ie),
475 fn besteffort<XE,T,F,G>(self, f:F, g:G) -> Result<T,ReloadError>
476 where XE: Into<LoadError>,
477 F: FnOnce() -> Result<T,XE>,
480 Ok(f().unwrap_or_else(|e| {
483 error!("reloading, error, partially skipping {}: {}",
486 LE::BadBundle(why) => {
487 warn!("reloading, partially skipping {}: {}",
496 #[derive(Debug,Copy,Clone)]
497 struct BundleParseUpload;
498 impl BundleParseErrorHandling for BundleParseUpload {
499 type Err = LoadError;
500 fn required<XE,T,F>(self, f:F) -> Result<T,LoadError>
501 where XE: Into<LoadError>,
502 F: FnOnce() -> Result<T,XE>
504 f().map_err(Into::into)
507 fn besteffort<XE,T,F,G>(self, f:F, _:G) -> Result<T,LoadError>
508 where XE: Into<LoadError>,
509 F: FnOnce() -> Result<T,XE>,
512 f().map_err(Into::into)
516 #[derive(Copy,Clone,Debug,EnumCount,EnumMessage,ToPrimitive)]
518 #[strum(message="transfer upload data")] Upload,
519 #[strum(message="scan")] Scan,
520 #[strum(message="process piece images")] Pieces,
521 #[strum(message="finish")] Finish,
523 impl progress::Enum for Phase { }
525 #[derive(Copy,Clone,Debug,EnumCount,EnumMessage,ToPrimitive)]
526 enum FinishProgress {
527 #[strum(message="reaquire game lock")] Reaquire,
528 #[strum(message="incorporate into game state")] Incorporate,
529 #[strum(message="install confirmed bundle")] Install,
531 impl progress::Enum for FinishProgress { }
534 fn parse_bundle<EH>(id: Id, instance: &InstanceName,
535 file: File, size: usize, hash: &'_ Hash, eh: EH,
536 mut for_progress: &mut dyn progress::Originator)
537 -> (ForProcess, Parsed)
538 where EH: BundleParseErrorHandling,
540 match id.kind { Kind::Zip => () }
542 #[derive(Copy,Clone,Debug,EnumCount,EnumMessage,ToPrimitive)]
544 #[strum(message="zipfile member names")] Names,
545 #[strum(message="metadata")] Meta,
546 #[strum(message="relevant zipfile members")] Contents,
547 #[strum(message="parse shape catalogues")] ParseLibs,
549 impl progress::Enum for ToScan { }
551 for_progress.phase_item(Phase::Scan, ToScan::Names);
552 let mut za = eh.required(||{
553 IndexedZip::new(file)
556 for_progress.phase_item(Phase::Scan, ToScan::Meta);
558 let meta = eh.besteffort(||{
559 const META: &str = "otter.toml";
560 let mut mf = za.by_name_caseless(META)?
561 .ok_or_else(|| LE::BadBundle(format!("bundle missing {}", META)))?;
562 let mut meta = String::new();
563 mf.read_to_string(&mut meta).map_err(
564 |e| LE::BadBundle(format!("access toml zip member: {}", e)))?;
565 let meta = meta.parse().map_err(
566 |e| LE::BadBundle(format!("parse zip member as toml: {}", e)))?;
567 let meta = toml_de::from_value(&meta).map_err(
568 |e| LE::BadBundle(format!("interpret zip member metadata: {}", e)))?;
572 title: "[bundle metadata could not be reloaded]".to_owned(),
573 mformat: materials_format::Version::CURRENT, // dummy value
577 for_progress.phase_item(Phase::Scan, ToScan::Contents);
586 let mut libs = Vec::new();
587 let mut specs = HashMap::new();
588 for (name,i) in &za {
589 eh.besteffort(|| Ok::<_,LE>(if_chain!{
590 let mut split = name.as_ref().split('/');
591 if let Some(dir) = split.next();
592 if let Some(file) = split.next();
593 if let None = split.next();
595 if unicase::eq(dir, "library") { if_chain!{
596 if let Some((base, ext)) = file.rsplit_once('.');
597 if unicase::eq(ext, "toml");
599 libs.push(LibScanned {
600 dir_inzip: format!("{}/{}", &dir, &base),
601 libname: base.to_lowercase(),
605 }} else if unicase::eq(dir, "specs") { if_chain!{
606 let mut split = file.rsplitn(3,'.');
607 if let Some(ext) = split.next(); if unicase::eq(ext, "toml");
608 if let Some(ext) = split.next(); if unicase::eq(ext, "game");
609 if let Some(base) = split.next();
611 use hash_map::Entry::*;
612 match specs.entry(base.to_owned().into()) {
613 Occupied(oe) => throw!(LE::BadBundle(format!(
614 "duplicate spec {:?} vs {:?} - files varying only in case!",
616 Vacant(ve) => { ve.insert((id,i)); }
624 for_progress.phase_item(Phase::Scan, ToScan::ParseLibs);
626 let mut newlibs = Vec::new();
628 #[derive(Debug,Clone)]
629 struct LibraryInBundle<'l> {
630 catalogue_data: String,
632 need_svgs: Vec<SvgNoted>,
634 mformat: materials_format::Version,
637 impl shapelib::LibrarySvgNoter for LibraryInBundle<'_> {
638 #[throws(shapelib::SubstError)]
639 fn note_svg(&mut self, basename: &GoodItemName,
640 src_name: Result<&str, &shapelib::SubstError>) {
641 let src_name = if src_name.unwrap_or_else(|e| e.input.as_str()) == "-" {
642 basename.as_str().to_string()
644 src_name.map_err(Clone::clone)?.to_string()
646 let item = basename.clone();
647 self.need_svgs.push(SvgNoted { item, src_name });
650 impl shapelib::LibrarySource for LibraryInBundle<'_> {
651 fn catalogue_data(&self) -> &str { &self.catalogue_data }
652 fn svg_dir(&self) -> String { self.svg_dir.clone() }
653 fn bundle(&self) -> Option<bundles::Id> { Some(*self.id) }
655 #[throws(materials_format::VersionError)]
656 fn default_materials_format(&self) -> materials_format::Version {
659 fn svg_noter(&mut self) -> &mut dyn shapelib::LibrarySvgNoter { self }
662 for LibScanned { libname, dir_inzip, inzip } in libs {
663 eh.besteffort(|| Ok::<_,LE>({
664 let svg_dir = format!("{}/lib{:06}", id.path_dir(instance), &inzip);
666 let mut zf = za.i(inzip)?;
667 let mut catalogue_data = String::new();
668 zf.read_to_string(&mut catalogue_data)
669 .map_err(|e| LE::badlib(&libname, &e))?;
670 let mut src = LibraryInBundle {
673 need_svgs: Vec::new(),
675 mformat: meta.mformat,
677 let contents = shapelib::load_catalogue(&libname, &mut src)
678 .map_err(|e| LE::badlib(&libname, &e))?;
682 need_svgs: src.need_svgs,
689 let (libs, newlibs) = newlibs.into_iter().unzip();
691 (ForProcess { za, newlibs },
692 Parsed { meta, libs, specs, size, hash: *hash })
696 fn process_bundle(ForProcess { mut za, mut newlibs }: ForProcess,
697 id: Id, instance: &InstanceName,
698 mut for_progress: &mut dyn progress::Originator)
700 let dir = id.path_dir(instance);
702 .with_context(|| dir.clone()).context("mkdir").map_err(IE::from)?;
704 let svg_count = newlibs.iter().map(|pl| pl.need_svgs.len()).sum();
706 for_progress.phase(Phase::Pieces, svg_count);
708 let instance_name = instance.to_string();
709 let bundle_name = id.to_string();
711 let mut svg_count = 0;
712 for ForProcessLib { need_svgs, svg_dir, dir_inzip, .. } in &mut newlibs {
714 fs::create_dir(&svg_dir)
715 .with_context(|| svg_dir.clone()).context("mkdir").map_err(IE::from)?;
717 for SvgNoted { item, src_name } in mem::take(need_svgs) {
718 make_usvg(&instance_name, &bundle_name,
719 &mut za, &mut svg_count, for_progress,
720 dir_inzip, svg_dir, &item, &src_name)?;
725 //---------- piece image processing ----------
727 #[derive(Copy,Clone,Debug,Display,EnumIter)]
728 // In preference order
734 #[derive(Serialize,Copy,Clone,Debug)]
735 struct Base64Meta<'r,'c:'r> {
739 #[serde(flatten)] ctx: &'r Base64Context<'c>,
742 #[derive(Serialize,Copy,Clone,Debug)]
743 struct Base64Context<'r> {
747 item: &'r GoodItemName,
751 fn image_usvg(ctx: &Base64Context, input: File, output: File,
752 format: image::ImageFormat, ctype: &'static str) {
753 let mut input = BufReader::new(input);
755 let image = image::io::Reader::with_format(&mut input, format);
756 let (width, height) = image.into_dimensions().map_err(
757 |e| LE::BadBundle(format!("{}: image examination failed: {}",
760 let render = Base64Meta {
762 height: height.into(),
765 base64_usvg(&render, input, output)?;
769 fn base64_usvg(meta: &Base64Meta, mut input: BufReader<File>, output: File) {
770 input.rewind().context("rewind input").map_err(IE::from)?;
771 let mut output = BufWriter::new(output);
773 let rendered = nwtemplates::render("image-usvg.tera", meta)
775 let (head, tail) = rendered.rsplit_once("@DATA@").ok_or_else(
776 || IE::from(anyhow!("image-usvg template did not produce @DATA@")))?;
778 write!(output,"{}",head).context("write head to output").map_err(IE::from)?;
779 let charset = base64::CharacterSet::Standard;
780 let b64cfg = base64::Config::new(charset,true);
781 let mut output = base64::write::EncoderWriter::new(output, b64cfg);
782 io::copy(&mut input, &mut output).map_err(|e| LE::BadBundle(format!(
783 "{}: read and base64-encode image data: {}", meta.ctx.zfname, e)))?;
784 let mut output = output.finish().context("finish b64").map_err(IE::from)?;
785 write!(output,"{}",tail).context("write tail to output").map_err(IE::from)?;
786 output.flush().context("flush output?").map_err(IE::from)?;
789 #[throws(InternalError)]
790 fn usvg_size(f: &mut BufReader<File>) -> PosC<f64> {
792 let mut buf = [0; 1024];
793 f.read(&mut buf).context("read start of usvg")?;
795 let s = str::from_utf8(&buf).unwrap_or_else(
796 |e| str::from_utf8(&buf[0.. e.valid_up_to()]).unwrap());
798 let size = svg_parse_size(HtmlStr::from_html_str(s))?;
801 })().context("looking for width/height attributes")?
805 fn make_usvg(instance_name: &str, bundle_name: &str, za: &mut IndexedZip,
806 progress_count: &mut usize,
807 mut for_progress: &mut dyn progress::Originator,
808 dir_inzip: &str, svg_dir: &str,
809 item: &GoodItemName, src_name: &str) {
810 let (format, mut zf) = 'format: loop {
811 for format in PictureFormat::iter() {
812 let input_basename = format!("{}/{}.{}", dir_inzip, src_name, format);
813 if let Some(zf) = za.by_name_caseless(input_basename)? {
814 break 'format (format, zf);
817 throw!(LE::BadBundle(format!(
818 "missing image file, looked for one of {}/{}.{}", dir_inzip, item,
819 PictureFormat::iter().map(|s| s.to_string().to_lowercase()).join(" ."),
823 for_progress.item(*progress_count, zf.name());
824 *progress_count += 1;
826 let mut input = tempfile::tempfile_in(&svg_dir)
827 .context("create").map_err(IE::from)?;
828 io::copy(&mut zf, &mut input)
829 .context("copy from zip").with_context(|| zf.name().to_owned())
830 .map_err(|e| LE::BadBundle(e.to_string()))?;
832 .context("rewind").map_err(IE::from)?;
834 let usvg_path = format!("{}/{}.usvg", svg_dir, item);
835 let output = File::create(&usvg_path)
836 .with_context(|| usvg_path.clone()).context("create").map_err(IE::from)?;
838 use PictureFormat as PF;
839 use image::ImageFormat as IF;
841 let ctx = &Base64Context {
849 let mut usvg1 = tempfile::tempfile_in(&svg_dir)
850 .context("create temporary usvg").map_err(IE::from)?;
852 let mut cmd = Command::new(&config().usvg_bin);
853 cmd.args(usvg_default_args());
854 cmd.args(&["-","-c"])
856 .stdout(usvg1.try_clone().context("dup usvg1").map_err(IE::from)?);
858 .output().context("run usvg").map_err(IE::from)?;
859 if ! got.status.success() {
860 throw!(LE::BadBundle(format!(
861 "{}: usvg conversion failed: {}: {}",
862 zf.name(), got.status, String::from_utf8_lossy(&got.stderr)
866 usvg1.rewind().context("rewind temporary usvg").map_err(IE::from)?;
867 let mut usvg1 = BufReader::new(usvg1);
868 let PosC { coords: [width,height] } = usvg_size(&mut usvg1)?;
870 let render = Base64Meta { width, height, ctype: "image/svg+xml", ctx };
871 base64_usvg(&render, usvg1, output)?;
874 image_usvg(ctx, input, output, IF::Png, "image/png")?;
879 //---------- specs ----------
881 #[throws(anyhow::Error)]
882 pub fn spec_macroexpand(
884 report: &mut dyn FnMut(&'static str, &str) -> Result<(),AE>,
886 if ! input.starts_with("{#") { return input }
888 let templates: Vec<(&str, Cow<str>)> = match input.rfind("\n{% endmacro") {
889 None => vec![ ("spec", input.into()) ],
891 let endm_sol = endm_base + 1;
892 let endm_end = endm_sol + 1 +
893 input[endm_sol..].find('\n')
894 .ok_or_else(|| anyhow!("endmacro line not terminated"))?;
895 let mac_data = &input[0..endm_end];
897 r#"{% import "m" as m %}"#.to_string()
898 + &"\n".repeat(mac_data.matches('\n').count())
899 + &input[endm_end..];
900 vec![ ("m", mac_data .into()),
901 ("spec", spec_data.into()) ]
905 for (nomfile, data) in &templates { report(nomfile, data)?; }
907 let mut tera = Tera::default();
908 tera.add_raw_templates(templates).context("load")?;
909 let mut out: Vec<u8> = vec![];
910 tera.render_to("spec", &default(), &mut out).context("render")?;
911 let out = String::from_utf8(out).context("reparse as utf-8")?;
913 report("out", &out)?;
919 pub fn load_spec_to_read(ig: &Instance, spec_name: &str) -> String {
921 fn read_from_read(spec_f: &mut dyn Read,
922 e_f: &mut dyn FnMut(io::Error) -> MgmtError) -> String {
923 let mut buf = String::new();
924 spec_f.read_to_string(&mut buf).map_err(|e| match e.kind() {
925 ErrorKind::InvalidData => ME::GameSpecInvalidData,
926 ErrorKind::UnexpectedEof => ME::BadBundle(e.to_string()),
930 spec_macroexpand(buf, &mut |_,_|Ok(()))
931 .map_err(|ae| ME::BadBundle(
932 format!("process spec as Tera template: {}", ae.d())
936 let spec_leaf = format!("{}.game.toml", spec_name);
938 if let Some((id, index)) = ig.bundle_specs.get(&UniCase::from(spec_name)) {
942 let fpath = id.path_(&ig.name);
943 let f = File::open(&fpath)
944 .with_context(|| fpath.clone()).context("reopen bundle")
947 let mut za = ZipArchive::new(BufReader::new(f)).map_err(
948 |e| LE::BadBundle(format!("re-examine zipfile: {}", e)))?;
949 let mut f = za.i(*index).map_err(
950 |e| LE::BadBundle(format!("re-find zipfile member: {}", e)))?;
951 return read_from_read(&mut f, &mut |e|{
952 LE::BadBundle(format!("read zipfile member: {}", e))}.into())?;
958 if spec_name.chars().all(
959 |c| c.is_ascii_alphanumeric() || c=='-' || c =='_'
961 let path = format!("{}/{}", config().specs_dir, &spec_leaf);
962 debug!("{}: trying to loading builtin spec from {}",
964 match File::open(&path) {
966 return read_from_read(&mut f, &mut |e| {
968 AE::from(e).context(path.clone()).context("read spec")
972 Err(e) if e.kind() == ErrorKind::NotFound => { },
973 Err(e) => throw!(IE::from(
974 AE::from(e).context(path).context("try open game spec")
979 Err(ME::GameSpecNotFound)?
982 //---------- scanning/incorporating/uploading ----------
984 #[throws(InternalError)]
985 fn incorporate_bundle(ib: &mut InstanceBundles, ig: &mut Instance,
986 id: Id, parsed: Parsed) {
987 let Parsed { meta, libs, specs, size, hash } = parsed;
989 let iu: usize = id.index.into();
990 let slot = &mut ib.bundles[iu];
993 Some(Note { kind:_, state: State::Uploading }) => { },
994 Some(ref note) => throw!(IE::DuplicateBundle {
996 kinds: [note.kind, id.kind],
1001 ig.local_libs.add(lib);
1003 ig.bundle_specs.extend(specs);
1005 let state = State::Loaded(Loaded { meta, size, hash });
1006 *slot = Some(Note { kind: id.kind, state });
1009 impl InstanceBundles {
1010 pub fn new() -> Self { default() }
1012 fn iter(&self) -> impl Iterator<Item=(Id, &State)> {
1013 self.bundles.iter().enumerate().filter_map(|(index, slot)| {
1014 let Note { kind, ref state } = *slot.as_ref()?;
1015 let index = index.try_into().unwrap();
1016 Some((Id { index, kind }, state))
1020 fn updated(&self, ig: &mut Instance) {
1021 ig.bundle_list = self.iter().map(|(id, state)| {
1025 let new_info_pane = ig.bundle_list.info_pane(ig).unwrap_or_else(|e|{
1026 let m = "error rendering bundle list";
1027 error!("{}: {}", m, e);
1030 let new_info_pane = Arc::new(new_info_pane);
1031 let mut prepub = PrepareUpdatesBuffer::new(ig, Some(1));
1032 prepub.raw_updates(vec![
1033 PUE::UpdateBundles { new_info_pane }
1038 fn scan_game_bundles(instance: &InstanceName)
1039 -> impl Iterator<Item=Result<
1040 (String, Result<BundleSavefile, NotBundle>),
1044 let bd = b_dir(instance);
1045 let mo = glob::MatchOptions {
1046 require_literal_leading_dot: true,
1050 glob::glob_with(&format!("{}/*", bd), mo)
1051 .context("pattern for bundle glob")?
1053 let fpath = fpath.context("bundle glob")?;
1055 .to_str().ok_or_else(|| anyhow!("glob unicode conversion"))?;
1056 let fleaf = fpath.rsplitn(2, '/').next().unwrap();
1057 let parsed: Result<BundleSavefile, NotBundle> = fleaf.parse();
1058 Ok::<_,IE>((fpath.to_owned(), parsed))
1063 pub fn reload_game_bundles(ig: &mut Instance) -> Self {
1064 let mut ib = InstanceBundles::new();
1066 for entry in InstanceBundles::scan_game_bundles(&ig.name)? {
1067 let (fpath, parsed) = entry?;
1068 let parsed: BundleSavefile = match parsed {
1070 Err(NotBundle(why)) => {
1071 debug!("bundle file {:?} skippping {}", &fpath, why);
1076 let iu: usize = parsed.index().into();
1077 ib.bundles.get_or_extend_with(iu, default);
1079 let hash = match ig.bundle_hashes.hashes.get(iu) {
1080 Some(Some(hash)) => hash,
1082 error!("bundle hash missing for {} {:?}", &ig.name, &parsed);
1087 if_let!{ BundleSavefile::Bundle(id) = parsed;
1090 let file = File::open(&fpath)
1091 .with_context(|| fpath.clone()).context("open zipfile")
1092 .map_err(IE::from)?;
1094 let size = file.metadata()
1095 .with_context(|| fpath.clone()).context("fstat zipfile")
1098 .with_context(|| fpath.clone()).context("zipfile too long!")?;
1100 let eh = BundleParseReload { bpath: &fpath };
1101 let (_za, parsed) = match
1102 parse_bundle(id, &ig.name, file, size, hash, eh, &mut ()) {
1105 debug!("bundle file {:?} reload failed {}", &fpath, e);
1110 incorporate_bundle(&mut ib, ig, id, parsed)?;
1112 debug!("loaded bundles {} {:?}", &ig.name, ib);
1117 #[throws(MgmtError)]
1118 pub fn start_upload(&mut self, ig: &mut Instance, kind: Kind)
1120 // todo: if bundle hash is here already, simply promote it
1121 let state = State::Uploading;
1122 let slot = Some(Note { kind, state });
1123 let index = self.bundles.len();
1124 if index >= usize::from(BUNDLES_MAX) { throw!(ME::TooManyBundles) }
1125 let index = index.try_into().unwrap();
1126 self.bundles.push(slot);
1127 let id = Id { kind, index };
1128 let tmp = id.path_tmp(&ig.name);
1130 let file = (|| Ok::<_,AE>({
1132 fs::OpenOptions::new()
1133 .read(true).write(true).create_new(true)
1138 Err(e) if e.kind() == ErrorKind::NotFound => {
1139 let d = b_dir(&ig.name);
1140 fs::create_dir(&d).context("create containing directory")?;
1141 mkf().context("crate (after creating containing directory)")?
1143 e@ Err(_) => e.context("create")?,
1146 .with_context(|| tmp.to_owned()).context("upload file")
1147 .map_err(IE::from)?;
1149 let file = BufWriter::new(file);
1150 let file = DigestWrite::new(file);
1151 let instance = ig.name.clone();
1153 Uploading { file, instance, id }
1158 #[throws(MgmtError)]
1159 pub fn bulk<'p,R,PW>(self, data: R, size: usize, expected: &Hash,
1160 progress_mode: ProgressUpdateMode,
1161 progress_stream: &'p mut ResponseWriter<PW>)
1163 where R: Read, PW: Write
1165 let mut for_progress_box: Box<dyn progress::Originator> =
1166 if progress_mode >= PUM::Simplex {
1167 Box::new(progress::ResponseOriginator::new(
1169 |pi: ProgressInfo<'_>| MgmtResponse::Progress(pi.into_owned()),
1175 let Uploading { id, mut file, instance } = self;
1176 let tmp = id.path_tmp(&instance);
1178 let mut null_progress = ();
1179 let for_progress_upload: &mut dyn progress::Originator =
1180 if progress_mode >= PUM::Duplex
1181 { &mut *for_progress_box } else { &mut null_progress };
1183 let mut data_reporter = progress::ReadOriginator::new(
1184 for_progress_upload, Phase::Upload, size, data);
1186 let copied_size = match io::copy(&mut data_reporter, &mut file) {
1187 Err(e) if e.kind() == ErrorKind::TimedOut => throw!(ME::UploadTimeout),
1190 .with_context(|| tmp.clone())
1191 .context("copy").map_err(IE::from)?;
1193 let (hash, file) = file.finish();
1195 if copied_size != size as u64 { throw!(ME::UploadTruncated) }
1197 let mut file = file.into_inner().map_err(|e| e.into_error())
1198 .with_context(|| tmp.clone()).context("flush").map_err(IE::from)?;
1200 let hash = hash.try_into().unwrap();
1201 let hash = Hash(hash);
1202 if &hash != expected { throw!(ME::UploadCorrupted) }
1204 file.rewind().context("rewind"). map_err(IE::from)?;
1206 let mut for_progress = &mut *for_progress_box;
1208 let (za, parsed) = parse_bundle(id, &instance,
1209 file, size, &hash, BundleParseUpload,
1212 process_bundle(za, id, &*instance, for_progress)?;
1214 for_progress.phase_item(Phase::Finish, FinishProgress::Reaquire);
1216 Uploaded { id, parsed, for_progress_box }
1220 impl InstanceBundles {
1221 #[throws(MgmtError)]
1222 pub fn finish_upload(&mut self, ig: &mut InstanceGuard,
1223 Uploaded { id, parsed, mut for_progress_box }: Uploaded)
1225 let tmp = id.path_tmp(&ig.name);
1226 let install = id.path_(&ig.name);
1227 let mut for_progress = &mut *for_progress_box;
1229 *ig.bundle_hashes.hashes
1230 .get_or_extend_with(id.index.into(), default)
1231 = Some(parsed.hash);
1234 for_progress.phase_item(Phase::Finish, FinishProgress::Incorporate);
1236 incorporate_bundle(self, ig, id, parsed)?;
1238 for_progress.phase_item(Phase::Finish, FinishProgress::Install);
1241 match self.bundles.get(usize::from(id.index)) {
1242 Some(Some(Note { state: State::Loaded(..), .. })) => {
1243 fs::rename(&tmp, &install)
1244 .with_context(||install.clone())
1245 .context("install").map_err(IE::from)?;
1247 ref x => panic!("unexpected {:?}", x),
1253 //---------- clearing ----------
1255 impl InstanceBundles {
1256 pub fn truncate_all_besteffort(instance: &InstanceName) {
1258 Ok(bundles) = InstanceBundles::scan_game_bundles(instance);
1260 error!("failed to scan game bundles for {}: {}", instance, e);
1264 for entry in bundles {
1266 Ok((fpath,_what)) = entry;
1268 error!("failed to make sense of a pathname for {}: {}", instance, e);
1273 Ok(_) = File::create(&fpath);
1275 if e.raw_os_error() == Some(libc::EISDIR) {
1277 warn!("failed to truncate a bundle for {}: {}: {}",
1278 instance, fpath, e);
1286 #[throws(MgmtError)]
1287 pub fn clear(&mut self, ig: &mut Instance) {
1289 // Check we are not in bundle state USED
1290 let checks: &[(_,_, Option<&dyn Debug>)] = &[
1291 ( "pieces", ig.gs.pieces.is_empty(), None ),
1292 ( "piece aliases", ig.pcaliases.is_empty(), None ),
1293 ( "pieces - occults", ig.gs.occults.is_empty(), Some(&ig.gs.occults) ),
1294 ( "pieces - ipieces", ig.ipieces.is_empty(), Some(&ig.ipieces ) ),
1295 ( "pieces - ioccults", ig.ioccults.is_empty(), Some(&ig.ioccults ) ),
1297 for (m, ok, pr) in checks {
1299 if let Some(pr) = pr {
1300 error!("{}: failed to clear because leftover {}: {:?}",
1303 throw!(ME::BundlesInUse(m.to_string()))
1307 // If we are in UNUSED, become BROKEN
1308 ig.local_libs.clear();
1311 // If we are in BROKEN, become WRECKAGE
1313 : Vec<(&dyn Fn(&str) -> io::Result<()>, String)>
1315 for (index, slot) in self.bundles.iter_mut().enumerate() {
1316 if_let!{ Some(note) = slot; else continue }
1318 index: index.try_into()
1319 .map_err(|_e| internal_error_bydebug(&(index, ¬e)))?,
1322 let tmp = id.path_tmp(&ig.name);
1323 let install = id.path_(&ig.name);
1324 match fs::rename(&install, &tmp) {
1325 Err(e) if e.kind() == ErrorKind::NotFound => { }
1326 x => x.with_context(|| tmp.clone())
1327 .with_context(|| install.clone())
1328 .context("rename away")?
1330 note.state = State::Uploading;
1331 // These will happen in order, turning each bundle from
1332 // WRECKAGE into NEARLY-ABSENT
1333 to_clean.push((&|p| fs::remove_dir_all(p), id.path_dir(&ig.name) ));
1334 to_clean.push((&|p| fs::remove_file (p), tmp ));
1337 InstanceBundles::truncate_all_besteffort(&ig.name);
1339 // Actually try to clean up WRECKAGE into NEARLY-ABSENT
1340 for (f,p) in to_clean {
1342 Err(e) if e.kind() == ErrorKind::NotFound => { }
1343 x => x.with_context(|| p.clone()).context("clean up")?
1347 let new_asset_key = AssetUrlKey::new_random()?;
1349 // Right, everything is at most NEARLY-ASENT, make them ABSENT
1350 self.bundles.clear();
1351 ig.bundle_specs.clear();
1352 ig.bundle_hashes.hashes.clear();
1354 // Prevent old, removed, players from accessing any new bundles.
1355 ig.asset_url_key = new_asset_key;
1365 fn id_file_parse() {
1366 let check_y = |s,index,kind| {
1367 let id = Id { index, kind };
1368 assert_eq!(Id::from_str(s).unwrap(), id);
1369 assert_eq!(id.to_string(), s);
1371 let check_n = |s,m| {
1372 assert_eq!(Id::from_str(s).unwrap_err().to_string(), m)
1374 check_y("00000.zip", Index(0), Kind::Zip);
1375 check_n("00000zip", "no dot");
1376 check_n("womba.zip", "bad index");
1377 check_n("00000.xyz", "bad extension");
1378 check_n("65536.zip", "bad index");
1383 fn test_digest_write() {
1384 let ibuffer = b"xyz";
1385 let exp = Sha512_256::digest(&ibuffer[..]);
1386 let mut obuffer = [0;4];
1387 let inner = &mut obuffer[..];
1388 let mut dw = bundles::DigestWrite::new(inner);
1389 assert_eq!( dw.write(&ibuffer[..]).unwrap(), 3);
1390 let (got, recov) = dw.finish();
1391 assert_eq!( recov, b"\0" );
1392 assert_eq!( got, exp );
1393 assert_eq!( &obuffer, b"xyz\0" );