chiark / gitweb /
usvg: Pass default options even during bundle processing.
[otter.git] / src / bundles.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use crate::prelude::*;
6
7 //---------- public types ----------
8
9 pub use crate::prelude::Sha512_256 as Digester;
10 pub type DigestWrite<W> = digestrw::DigestWrite<Digester, W>;
11
12 #[derive(Copy,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
13 pub struct Hash(pub [u8; 32]);
14
15 #[derive(Debug,Copy,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
16 #[derive(EnumString,strum::Display,Ord,PartialOrd)]
17 pub enum Kind {
18   #[strum(to_string="zip")]       Zip,
19 //  #[strum(to_string="game.toml")] GameSpec, // identification problems
20 }
21 impl Kind { pub fn only() -> Self { Kind::Zip } }
22
23 #[derive(Copy,Clone,Default,Debug,Hash,PartialEq,Eq,Ord,PartialOrd)]
24 #[derive(Serialize,Deserialize)]
25 #[serde(transparent)]
26 pub struct Index(u16);
27
28 #[derive(Copy,Clone,Debug,Hash,PartialEq,Eq,Ord,PartialOrd)]
29 #[derive(Serialize,Deserialize)]
30 pub struct Id { pub index: Index, pub kind: Kind, }
31
32 #[derive(Debug,Clone,Default)]
33 pub struct InstanceBundles {
34   bundles: Vec<Option<Note>>,
35 }
36
37 pub type FileInBundleId = (Id, ZipIndex);
38 pub type SpecsInBundles = HashMap<UniCase<String>, FileInBundleId>;
39
40 #[derive(Debug,Clone,Serialize,Deserialize)]
41 pub enum State {
42   Uploading,
43   Loaded(Loaded),
44 }
45
46 #[derive(Debug,Clone,Serialize,Deserialize)]
47 pub struct Loaded {
48   pub meta: BundleMeta,
49   pub size: usize,
50   pub hash: bundles::Hash,
51 }
52
53 #[derive(Debug,Clone,Serialize,Deserialize,Default)]
54 pub struct HashCache {
55   hashes: Vec<Option<Hash>>,
56 }
57
58 /// returned by start_upload
59 pub struct Uploading {
60   id: Id,
61   instance: Arc<InstanceName>,
62   file: DigestWrite<BufWriter<fs::File>>,
63 }
64
65 /// returned by start_upload
66 pub struct Uploaded<'p> {
67   id: Id,
68   parsed: Parsed,
69   for_progress_box: Box<dyn progress::Originator + 'p>,
70 }
71
72 #[derive(Debug,Copy,Clone,Error)]
73 #[error("{0}")]
74 #[repr(transparent)]
75 pub struct NotBundle(&'static str);
76
77 #[derive(Error,Debug)]
78 pub enum LoadError {
79   #[error("bad bundle: {0}")]     BadBundle(BadBundle),
80   #[error("internal error: {0}")] IE(#[from] IE),
81 }
82
83 // Bundle states:
84 //
85 //             GameState     Instance        Note       main file    .d
86 //             pieces &c  libs,   HashCache
87 //                        specs    mem,aux
88 //
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
95
96 //---------- private definitions ----------
97
98 pub type ZipArchive = zipfile::read::ZipArchive<BufReader<File>>;
99
100 define_index_type!{ pub struct LibInBundleI = usize; }
101
102 #[derive(Debug)]
103 struct Parsed {
104   meta: BundleMeta,
105   libs: IndexVec<LibInBundleI, shapelib::Catalogue>,
106   specs: SpecsInBundles,
107   size: usize,
108   hash: Hash,
109 }
110
111 #[derive(Debug)]
112 struct ForProcess {
113   za: IndexedZip,
114   newlibs: IndexVec<LibInBundleI, ForProcessLib>,
115 }
116
117 #[derive(Debug)]
118 struct ForProcessLib {
119   dir_inzip: String,
120   svg_dir: String,
121   need_svgs: Vec<SvgNoted>,
122 }
123
124 #[derive(Debug,Clone)]
125 struct SvgNoted {
126   item: GoodItemName,
127   src_name: String,
128 }
129
130 const BUNDLES_MAX: Index = Index(64);
131
132 #[derive(Debug,Clone,Serialize,Deserialize)]
133 struct Note {
134   pub kind: Kind,
135   pub state: State,
136 }
137
138 pub type BadBundle = String;
139
140 use LoadError as LE;
141
142 #[derive(Debug,Copy,Clone)]
143 enum BundleSavefile {
144   Bundle(Id),
145   PreviousUploadFailed(Index),
146 }
147
148 //---------- straightformward impls ----------
149
150 impl From<Index> for usize {
151   fn from(i: Index) -> usize { i.0.into() }
152 }
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()?) }
157 }
158 impl Display for Index {
159   #[throws(fmt::Error)]
160   fn fmt(&self, f: &mut Formatter) {
161     write!(f, "{:05}", self.0)?;
162   }
163 }
164 impl FromStr for Index {
165   type Err = std::num::ParseIntError;
166   #[throws(Self::Err)]
167   fn from_str(s: &str) -> Index { Index(u16::from_str(s)?) }
168 }
169 hformat_as_display!{Id}
170
171 impl From<&'static str> for NotBundle {
172   fn from(s: &'static str) -> NotBundle {
173     unsafe { mem::transmute(s) }
174   }
175 }
176
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),
181   } }
182 }
183
184 impl LoadError {
185   fn badlib(libname: &str, e: &dyn Display) -> LoadError {
186     LE::BadBundle(format!("bad library: {}: {}", libname, e))
187   }
188 }
189
190 impl BundleSavefile {
191   pub fn index(&self) -> Index {
192     use BundleSavefile::*;
193     match self {
194       Bundle(id) => id.index,
195       &PreviousUploadFailed(index) => index,
196     }
197   }
198 }
199
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])?;
205     write!(f,"..")?;
206   }
207 }
208
209 //---------- pathname handling (including Id leafname) ----------
210
211 pub fn b_dir(instance: &InstanceName) -> String {
212   savefilename(instance, "b-", "")
213 }
214 fn b_file<S>(instance: &InstanceName, index: Index, suffix: S) -> String
215 where S: Display + Debug
216 {
217   format!("{}/{}.{}",
218           savefilename(instance, "b-", ""),
219           index, suffix)
220 }
221
222 impl Display for Id {
223   #[throws(fmt::Error)]
224   fn fmt(&self, f: &mut fmt::Formatter) {
225     write!(f, "{}.{}", self.index, self.kind)?
226   }
227 }
228
229 impl FromStr for BundleSavefile {
230   type Err = NotBundle;
231   #[throws(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 })
240   }
241 }
242 impl FromStr for Id {
243   type Err = NotBundle;
244   #[throws(NotBundle)]
245   fn from_str(fleaf: &str) -> Id {
246     match fleaf.parse()? {
247       BundleSavefile::Bundle(id) => id,
248       BundleSavefile::PreviousUploadFailed(_) => throw!(NotBundle("tmp")),
249     }
250   }
251 }
252
253 impl Id {
254   fn path_tmp(&self, instance: &InstanceName) -> String {
255     b_file(instance, self.index, "tmp")
256   }
257
258   fn path_(&self, instance: &InstanceName) -> String {
259     b_file(instance, self.index, self.kind)
260   }
261
262   fn path_dir(&self, instance: &InstanceName) -> String {
263     b_file(instance, self.index, "d")
264   }
265
266   pub fn path(&self, instance: &Unauthorised<InstanceGuard<'_>, InstanceName>,
267           auth: Authorisation<Id>) -> String {
268     self.path_(&instance.by_ref(auth.so_promise()).name)
269   }
270
271   #[throws(IE)]
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) {
276       Ok(f) => Some(f),
277       Err(e) if e.kind() == ErrorKind::NotFound => None,
278       Err(e) => void::unreachable(
279         Err::<Void,_>(e).context(path).context("open bundle")?
280       ),
281     }
282   }
283
284   #[throws(IE)]
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)?
289   }
290
291   pub fn token(&self, instance: &Instance) -> AssetUrlToken {
292     instance.asset_url_key.token("bundle", &(&*instance.name, *self))
293   }
294 }
295
296 //---------- displaing/presenting/authorising ----------
297
298 #[ext(pub)]
299 impl Authorisation<InstanceName> {
300   fn bundles(self) -> Authorisation<Id> { self.so_promise() }
301 }
302
303 impl Display for State {
304   #[throws(fmt::Error)]
305   fn fmt(&self, f: &mut Formatter) {
306     match self {
307       State::Loaded(Loaded{ meta, size, hash }) => {
308         let BundleMeta { title, mformat:_ } = meta;
309         write!(f, "Loaded {:10} {} {:?}", size, hash, title)?;
310       }
311       other => write!(f, "{:?}", other)?,
312     }
313   }
314 }
315
316 impl DebugIdentify for InstanceBundles {
317   #[throws(fmt::Error)]
318   fn debug_identify_type(f: &mut fmt::Formatter) {
319     write!(f, "InstanceBundles")?;
320   }
321 }
322
323 #[ext(pub)]
324 impl MgmtBundleList {
325   #[throws(IE)]
326   fn info_pane(&self, ig: &Instance) -> Html {
327     #[derive(Serialize,Debug)]
328     struct RenderPane {
329       bundles: Vec<RenderBundle>,
330     }
331     #[derive(Serialize,Debug)]
332     struct RenderBundle {
333       id: Html,
334       url: Html,
335       title: Html,
336     }
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 })
345     }).collect();
346
347     Html::from_html_string(
348       nwtemplates::render("bundles-info-pane.tera", &RenderPane { bundles })?
349     )
350   }
351 }
352
353 //---------- loading ----------
354
355 trait ReadSeek: Read + io::Seek { }
356 impl<T> ReadSeek for T where T: Read + io::Seek { }
357
358 impl From<ZipError> for LoadError {
359   fn from(ze: ZipError) -> LoadError {
360     match ze {
361       ZipError::Io(ioe) => IE::from(
362         AE::from(ioe).context("zipfile io error")
363       ).into(),
364       _ => LE::BadBundle(format!("bad zipfile: {}", ze))
365     }
366   }
367 }
368
369 #[derive(Debug,Deref,DerefMut)]
370 pub struct IndexedZip {
371   #[deref] #[deref_mut] za: ZipArchive,
372   members: BTreeMap<UniCase<String>, usize>,
373 }
374
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)? }
380 }
381
382 impl IndexedZip where {
383   #[throws(LoadError)]
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) {
393         drop(entry);
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 {:?}",
398           &previously, sname,
399         )));
400       }
401     }
402     IndexedZip { za, members }
403   }
404
405   #[throws(LoadError)]
406   pub fn by_name_caseless<'a, S>(&'a mut self, name: S) -> Option<ZipFile<'a>>
407   where S: Into<String>
408   {
409     if_let!{ Some(&i) = self.members.get(&UniCase::new(name.into()));
410              else return Ok(None) }
411     Some(self.za.by_index(i)?)
412   }
413 }
414
415 #[ext(pub)]
416 impl ZipArchive {
417   #[throws(LoadError)]
418   fn i<'z>(&'z mut self, i: ZipIndex) -> ZipFile<'z> {
419     self.by_index(i.0)?
420   }
421 }
422
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 {
427     Box::new(
428       self.members.iter().map(|(name,&index)| (name, ZipIndex(index)))
429     ) as _
430   }
431 }
432
433 trait BundleParseErrorHandling: Copy {
434   type Err;
435   fn required<XE,T,F>(self, f:F) -> Result<T, Self::Err>
436   where XE: Into<LoadError>,
437         F: FnOnce() -> Result<T,XE>;
438
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>,
442         G: FnOnce() -> T;
443 }
444
445 #[derive(Debug,Error)]
446 enum ReloadError {
447   IE(IE),
448   Unloadable(BadBundle),
449 }
450 display_as_debug!{ReloadError}
451
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>
459   {
460     use ReloadError as RLE;
461     f().map_err(|xe| {
462       let le: LE = xe.into();
463       match le {
464         LE::BadBundle(why) => RLE::Unloadable(
465           format!("{}: {}", self.bpath, &why)
466         ),
467         LE::IE(IE::Anyhow(ae)) => RLE::IE(IE::Anyhow(
468           ae.context(self.bpath.to_owned())
469         )),
470         LE::IE(ie) => RLE::IE(ie),
471       }
472     })
473   }
474
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>,
478         G: FnOnce() -> T,
479   {
480     Ok(f().unwrap_or_else(|e| {
481       match e.into() {
482         LE::IE(ie) => {
483           error!("reloading, error, partially skipping {}: {}",
484                  self.bpath, ie);
485         },
486         LE::BadBundle(why) => {
487           warn!("reloading, partially skipping {}: {}",
488                 self.bpath, why);
489         },
490       }
491       g()
492     }))
493   }
494 }
495
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>
503   {
504     f().map_err(Into::into)
505   }
506
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>,
510         G: FnOnce() -> T,
511   {
512     f().map_err(Into::into)
513   }
514 }
515
516 #[derive(Copy,Clone,Debug,EnumCount,EnumMessage,ToPrimitive)]
517 enum Phase {
518   #[strum(message="transfer upload data")]   Upload,
519   #[strum(message="scan")]                   Scan,
520   #[strum(message="process piece images")]   Pieces,
521   #[strum(message="finish")]                 Finish,
522 }
523 impl progress::Enum for Phase { }
524
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,
530 }
531 impl progress::Enum for FinishProgress { }
532
533 #[throws(EH::Err)]
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,
539 {
540   match id.kind { Kind::Zip => () }
541
542   #[derive(Copy,Clone,Debug,EnumCount,EnumMessage,ToPrimitive)]
543   enum ToScan {
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,
548   }
549   impl progress::Enum for ToScan { }
550
551   for_progress.phase_item(Phase::Scan, ToScan::Names);
552   let mut za = eh.required(||{
553     IndexedZip::new(file)
554   })?;
555
556   for_progress.phase_item(Phase::Scan, ToScan::Meta);
557   
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)))?;
569     Ok::<_,LE>(meta)
570   }, ||{
571     BundleMeta {
572       title: "[bundle metadata could not be reloaded]".to_owned(),
573       mformat: materials_format::Version::CURRENT, // dummy value
574     }
575   })?;
576
577   for_progress.phase_item(Phase::Scan, ToScan::Contents);
578
579   #[derive(Debug)]
580   struct LibScanned {
581     libname: String,
582     dir_inzip: String,
583     inzip: ZipIndex,
584   }
585
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();
594       then {
595         if unicase::eq(dir, "library") { if_chain!{
596           if let Some((base, ext)) = file.rsplit_once('.');
597           if unicase::eq(ext, "toml");
598           then {
599             libs.push(LibScanned {
600               dir_inzip: format!("{}/{}", &dir, &base),
601               libname: base.to_lowercase(),
602               inzip: i,
603             });
604           }
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();
610           then {
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!",
615                 file, oe.key()))),
616               Vacant(ve) => { ve.insert((id,i)); }
617             }
618           }
619         }}
620       }
621     }), ||())?;
622   }
623
624   for_progress.phase_item(Phase::Scan, ToScan::ParseLibs);
625
626   let mut newlibs = Vec::new();
627
628   #[derive(Debug,Clone)]
629   struct LibraryInBundle<'l> {
630     catalogue_data: String,
631     svg_dir: &'l String,
632     need_svgs: Vec<SvgNoted>,
633     id: &'l Id,
634     mformat: materials_format::Version,
635   }
636
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()
643       } else {
644         src_name.map_err(Clone::clone)?.to_string()
645       };
646       let item = basename.clone();
647       self.need_svgs.push(SvgNoted { item, src_name });
648     }
649   }
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) }
654
655     #[throws(materials_format::VersionError)]
656     fn default_materials_format(&self) -> materials_format::Version {
657       self.mformat
658     }
659     fn svg_noter(&mut self) -> &mut dyn shapelib::LibrarySvgNoter { self }
660   }
661
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);
665
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 {
671         catalogue_data,
672         svg_dir: &svg_dir,
673         need_svgs: Vec::new(),
674         id: &id,
675         mformat: meta.mformat,
676       };
677       let contents = shapelib::load_catalogue(&libname, &mut src)
678         .map_err(|e| LE::badlib(&libname, &e))?;
679       newlibs.push((
680         contents,
681         ForProcessLib {
682           need_svgs: src.need_svgs,
683           svg_dir, dir_inzip,
684         }
685       ));
686     }), ||())?;
687   }
688
689   let (libs, newlibs) = newlibs.into_iter().unzip();
690
691   (ForProcess { za, newlibs },
692    Parsed { meta, libs, specs, size, hash: *hash })
693 }
694
695 #[throws(LE)]
696 fn process_bundle(ForProcess { mut za, mut newlibs }: ForProcess,
697                   id: Id, instance: &InstanceName,
698                   mut for_progress: &mut dyn progress::Originator)
699 {
700   let dir = id.path_dir(instance);
701   fs::create_dir(&dir)
702     .with_context(|| dir.clone()).context("mkdir").map_err(IE::from)?;
703   
704   let svg_count = newlibs.iter().map(|pl| pl.need_svgs.len()).sum();
705
706   for_progress.phase(Phase::Pieces, svg_count);
707   
708   let instance_name = instance.to_string();
709   let bundle_name = id.to_string();
710
711   let mut svg_count = 0;
712   for ForProcessLib { need_svgs, svg_dir, dir_inzip, .. } in &mut newlibs {
713
714     fs::create_dir(&svg_dir)
715       .with_context(|| svg_dir.clone()).context("mkdir").map_err(IE::from)?;
716
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)?;
721     }
722   }
723 }
724
725 //---------- piece image processing ----------
726
727 #[derive(Copy,Clone,Debug,Display,EnumIter)]
728 // In preference order
729 enum PictureFormat {
730   Svg,
731   Png,
732 }
733
734 #[derive(Serialize,Copy,Clone,Debug)]
735 struct Base64Meta<'r,'c:'r> {
736   width: f64,
737   height: f64,
738   ctype: &'static str,
739   #[serde(flatten)] ctx: &'r Base64Context<'c>,
740 }
741
742 #[derive(Serialize,Copy,Clone,Debug)]
743 struct Base64Context<'r> {
744   bundle: &'r str,
745   game: &'r str,
746   zfname: &'r str,
747   item: &'r GoodItemName,
748 }
749
750 #[throws(LE)]
751 fn image_usvg(ctx: &Base64Context, input: File, output: File,
752               format: image::ImageFormat, ctype: &'static str) {
753   let mut input = BufReader::new(input);
754
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: {}",
758                               ctx.zfname, e)))?;
759
760   let render = Base64Meta {
761     width: width.into(),
762     height: height.into(),
763     ctype, ctx,
764   };
765   base64_usvg(&render, input, output)?;
766 }
767
768 #[throws(LE)]
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);
772
773   let rendered = nwtemplates::render("image-usvg.tera", meta)
774     .map_err(IE::from)?;
775   let (head, tail) = rendered.rsplit_once("@DATA@").ok_or_else(
776     || IE::from(anyhow!("image-usvg template did not produce @DATA@")))?;
777
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)?;
787 }
788
789 #[throws(InternalError)]
790 fn usvg_size(f: &mut BufReader<File>) -> PosC<f64> {
791   (||{
792     let mut buf = [0; 1024];
793     f.read(&mut buf).context("read start of usvg")?;
794
795     let s = str::from_utf8(&buf).unwrap_or_else(
796       |e| str::from_utf8(&buf[0.. e.valid_up_to()]).unwrap());
797
798     let size = svg_parse_size(HtmlStr::from_html_str(s))?;
799
800     Ok::<_,AE>(size)
801   })().context("looking for width/height attributes")?
802 }
803
804 #[throws(LE)]
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);
815       }
816     }
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(" ."),
820     )));
821   };
822
823   for_progress.item(*progress_count, zf.name());
824   *progress_count += 1;
825
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()))?;
831   input.rewind()
832     .context("rewind").map_err(IE::from)?;
833
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)?;
837
838   use PictureFormat as PF;
839   use image::ImageFormat as IF;
840
841   let ctx = &Base64Context {
842     zfname: zf.name(),
843     game: instance_name,
844     bundle: bundle_name,
845     item,
846   };
847   match format {
848     PF::Svg => {
849       let mut usvg1 = tempfile::tempfile_in(&svg_dir)
850         .context("create temporary usvg").map_err(IE::from)?;
851
852       let mut cmd = Command::new(&config().usvg_bin);
853       cmd.args(usvg_default_args());
854       cmd.args(&["-","-c"])
855         .stdin(input)
856         .stdout(usvg1.try_clone().context("dup usvg1").map_err(IE::from)?);
857       let got = cmd
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)
863         )));
864       }
865
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)?;
869
870       let render = Base64Meta { width, height, ctype: "image/svg+xml", ctx };
871       base64_usvg(&render, usvg1, output)?;
872     },
873     PF::Png => {
874       image_usvg(ctx, input, output, IF::Png, "image/png")?;
875     },
876   }
877 }
878
879 //---------- specs ----------
880
881 #[throws(anyhow::Error)]
882 pub fn spec_macroexpand(
883   input: String,
884   report: &mut dyn FnMut(&'static str, &str) -> Result<(),AE>,
885 ) -> String {
886   if ! input.starts_with("{#") { return input }
887
888   let templates: Vec<(&str, Cow<str>)> = match input.rfind("\n{% endmacro") {
889     None => vec![ ("spec", input.into()) ],
890     Some(endm_base) => {
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];
896       let spec_data = 
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()) ]
902     },
903   };
904
905   for (nomfile, data) in &templates { report(nomfile, data)?; }
906
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")?;
912
913   report("out", &out)?;
914
915   out
916 }
917
918 #[throws(MgmtError)]
919 pub fn load_spec_to_read(ig: &Instance, spec_name: &str) -> String {
920   #[throws(MgmtError)]
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()),
927       _ => e_f(e),
928     })?;
929
930     spec_macroexpand(buf, &mut |_,_|Ok(()))
931       .map_err(|ae| ME::BadBundle(
932         format!("process spec as Tera template: {}", ae.d())
933       ))?
934   }
935
936   let spec_leaf = format!("{}.game.toml", spec_name);
937
938   if let Some((id, index)) = ig.bundle_specs.get(&UniCase::from(spec_name)) {
939     match id.kind {
940       Kind::Zip => {
941
942         let fpath = id.path_(&ig.name);
943         let f = File::open(&fpath)
944           .with_context(|| fpath.clone()).context("reopen bundle")
945           .map_err(IE::from)?;
946
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())?;
953
954       }
955     }
956   }
957
958   if spec_name.chars().all(
959     |c| c.is_ascii_alphanumeric() || c=='-' || c =='_'
960   ) {
961     let path = format!("{}/{}", config().specs_dir, &spec_leaf);
962     debug!("{}: trying to loading builtin spec from {}",
963            &ig.name, &path);
964     match File::open(&path) {
965       Ok(mut f) => {
966         return read_from_read(&mut f, &mut |e| {
967           IE::from(
968             AE::from(e).context(path.clone()).context("read spec")
969           ).into()
970         })?;
971       },
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")
975       )),
976     }
977   }
978
979   Err(ME::GameSpecNotFound)?
980 }
981
982 //---------- scanning/incorporating/uploading ----------
983
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;
988
989   let iu: usize = id.index.into();
990   let slot = &mut ib.bundles[iu];
991   match slot {
992     None => { },
993     Some(Note { kind:_, state: State::Uploading }) => { },
994     Some(ref note) => throw!(IE::DuplicateBundle {
995       index: id.index,
996       kinds: [note.kind, id.kind],
997     })
998   };
999
1000   for lib in libs {
1001     ig.local_libs.add(lib);
1002   }
1003   ig.bundle_specs.extend(specs);
1004
1005   let state = State::Loaded(Loaded { meta, size, hash });
1006   *slot = Some(Note { kind: id.kind, state });
1007 }
1008
1009 impl InstanceBundles {
1010   pub fn new() -> Self { default() }
1011
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))
1017     })
1018   }
1019
1020   fn updated(&self, ig: &mut Instance) {
1021     ig.bundle_list = self.iter().map(|(id, state)| {
1022       (id, state.clone())
1023     }).collect();
1024
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);
1028       Html::from_txt(m)
1029     });
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 }
1034     ]);
1035   }
1036
1037   #[throws(IE)]
1038   fn scan_game_bundles(instance: &InstanceName)
1039                        -> impl Iterator<Item=Result<
1040       (String, Result<BundleSavefile, NotBundle>),
1041       IE
1042       >>
1043   {
1044     let bd = b_dir(instance);
1045     let mo = glob::MatchOptions {
1046       require_literal_leading_dot: true,
1047       ..default()
1048     };
1049     
1050     glob::glob_with(&format!("{}/*", bd), mo)
1051       .context("pattern for bundle glob")?
1052       .map(|fpath|{
1053         let fpath = fpath.context("bundle glob")?;
1054         let fpath = fpath
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))
1059       })
1060   }
1061
1062   #[throws(IE)]
1063   pub fn reload_game_bundles(ig: &mut Instance) -> Self {
1064     let mut ib = InstanceBundles::new();
1065
1066     for entry in InstanceBundles::scan_game_bundles(&ig.name)? {
1067       let (fpath, parsed) = entry?;
1068       let parsed: BundleSavefile = match parsed {
1069         Ok(y) => y,
1070         Err(NotBundle(why)) => {
1071           debug!("bundle file {:?} skippping {}", &fpath, why);
1072           continue;
1073         },
1074       };
1075
1076       let iu: usize = parsed.index().into();
1077       ib.bundles.get_or_extend_with(iu, default);
1078
1079       let hash = match ig.bundle_hashes.hashes.get(iu) {
1080         Some(Some(hash)) => hash,
1081         _ => {
1082           error!("bundle hash missing for {} {:?}", &ig.name, &parsed);
1083           continue;
1084         }
1085       };
1086
1087       if_let!{ BundleSavefile::Bundle(id) = parsed;
1088                else continue; }
1089
1090       let file = File::open(&fpath)
1091         .with_context(|| fpath.clone()).context("open zipfile")
1092         .map_err(IE::from)?;
1093
1094       let size = file.metadata()
1095         .with_context(|| fpath.clone()).context("fstat zipfile")
1096         .map_err(IE::from)?
1097         .len().try_into()
1098         .with_context(|| fpath.clone()).context("zipfile too long!")?;
1099
1100       let eh = BundleParseReload { bpath: &fpath };
1101       let (_za, parsed) = match
1102         parse_bundle(id, &ig.name, file, size, hash, eh, &mut ()) {
1103         Ok(y) => y,
1104         Err(e) => {
1105           debug!("bundle file {:?} reload failed {}", &fpath, e);
1106           continue;
1107         }
1108       };
1109
1110       incorporate_bundle(&mut ib, ig, id, parsed)?;
1111     }
1112     debug!("loaded bundles {} {:?}", &ig.name, ib);
1113     ib.updated(ig);
1114     ib
1115   }
1116
1117   #[throws(MgmtError)]
1118   pub fn start_upload(&mut self, ig: &mut Instance, kind: Kind)
1119                       -> Uploading {
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);
1129
1130     let file = (|| Ok::<_,AE>({
1131       let mkf = || {
1132         fs::OpenOptions::new()
1133           .read(true).write(true).create_new(true)
1134           .open(&tmp)
1135       };
1136       match mkf() {
1137         Ok(f) => f,
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)")?
1142         }
1143         e@ Err(_) => e.context("create")?,
1144       }
1145     }))()
1146       .with_context(|| tmp.to_owned()).context("upload file")
1147       .map_err(IE::from)?;
1148     
1149     let file = BufWriter::new(file);
1150     let file = DigestWrite::new(file);
1151     let instance = ig.name.clone();
1152     self.updated(ig);
1153     Uploading { file, instance, id }
1154   }
1155 }
1156
1157 impl Uploading {
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>)
1162                     -> Uploaded<'p>
1163   where R: Read, PW: Write
1164   {
1165     let mut for_progress_box: Box<dyn progress::Originator> =
1166       if progress_mode >= PUM::Simplex {
1167         Box::new(progress::ResponseOriginator::new(
1168           progress_stream,
1169           |pi: ProgressInfo<'_>| MgmtResponse::Progress(pi.into_owned()),
1170         ))
1171       } else {
1172         Box::new(())
1173       };
1174
1175     let Uploading { id, mut file, instance } = self;
1176     let tmp = id.path_tmp(&instance);
1177
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 };
1182     
1183     let mut data_reporter = progress::ReadOriginator::new(
1184       for_progress_upload, Phase::Upload, size, data);
1185
1186     let copied_size = match io::copy(&mut data_reporter, &mut file) {
1187       Err(e) if e.kind() == ErrorKind::TimedOut => throw!(ME::UploadTimeout),
1188       x => x,
1189     }
1190       .with_context(|| tmp.clone())
1191       .context("copy").map_err(IE::from)?;
1192
1193     let (hash, file) = file.finish();
1194
1195     if copied_size != size as u64 { throw!(ME::UploadTruncated) }
1196
1197     let mut file = file.into_inner().map_err(|e| e.into_error())
1198       .with_context(|| tmp.clone()).context("flush").map_err(IE::from)?;
1199
1200     let hash = hash.try_into().unwrap();
1201     let hash = Hash(hash);
1202     if &hash != expected { throw!(ME::UploadCorrupted) }
1203
1204     file.rewind().context("rewind"). map_err(IE::from)?;
1205
1206     let mut for_progress = &mut *for_progress_box;
1207
1208     let (za, parsed) = parse_bundle(id, &instance,
1209                                     file, size, &hash, BundleParseUpload,
1210                                     for_progress)?;
1211
1212     process_bundle(za, id, &*instance, for_progress)?;
1213
1214     for_progress.phase_item(Phase::Finish, FinishProgress::Reaquire);
1215
1216     Uploaded { id, parsed, for_progress_box }
1217   }
1218 }
1219
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)
1224                        -> Id {
1225     let tmp = id.path_tmp(&ig.name);
1226     let install = id.path_(&ig.name);
1227     let mut for_progress = &mut *for_progress_box;
1228
1229     *ig.bundle_hashes.hashes
1230       .get_or_extend_with(id.index.into(), default)
1231       = Some(parsed.hash);
1232     ig.save_aux_now()?;
1233
1234     for_progress.phase_item(Phase::Finish, FinishProgress::Incorporate);
1235
1236     incorporate_bundle(self, ig, id, parsed)?;
1237
1238     for_progress.phase_item(Phase::Finish, FinishProgress::Install);
1239
1240     self.updated(ig);
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)?;
1246       }
1247       ref x => panic!("unexpected {:?}", x),
1248     };
1249     id
1250   }
1251 }
1252
1253 //---------- clearing ----------
1254
1255 impl InstanceBundles {
1256   pub fn truncate_all_besteffort(instance: &InstanceName) {
1257     if_let!{
1258       Ok(bundles) = InstanceBundles::scan_game_bundles(instance);
1259       Err(e) => {
1260         error!("failed to scan game bundles for {}: {}", instance, e);
1261         return;
1262       }
1263     };
1264     for entry in bundles {
1265       if_let!{
1266         Ok((fpath,_what)) = entry;
1267         Err(e) => {
1268           error!("failed to make sense of a pathname for {}: {}", instance, e);
1269           continue;
1270         }
1271       };
1272       if_let!{
1273         Ok(_) = File::create(&fpath);
1274         Err(e) => {
1275           if e.raw_os_error() == Some(libc::EISDIR) {
1276           } else {
1277             warn!("failed to truncate a bundle for {}: {}: {}",
1278                   instance, fpath, e);
1279           }
1280           continue;
1281         }
1282       }
1283     }
1284   }
1285
1286   #[throws(MgmtError)]
1287   pub fn clear(&mut self, ig: &mut Instance) {
1288
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  ) ),
1296     ];
1297     for (m, ok, pr) in checks {
1298       if ! ok {
1299         if let Some(pr) = pr {
1300           error!("{}: failed to clear because leftover {}: {:?}",
1301                  &ig.name, m, pr);
1302         }
1303         throw!(ME::BundlesInUse(m.to_string()))
1304       }
1305     }
1306
1307     // If we are in UNUSED, become BROKEN
1308     ig.local_libs.clear();
1309
1310     (||{
1311       // If we are in BROKEN, become WRECKAGE
1312       let mut to_clean
1313         : Vec<(&dyn Fn(&str) -> io::Result<()>, String)>
1314         = vec![];
1315       for (index, slot) in self.bundles.iter_mut().enumerate() {
1316         if_let!{ Some(note) = slot; else continue }
1317         let id = Id {
1318           index: index.try_into()
1319             .map_err(|_e| internal_error_bydebug(&(index, &note)))?,
1320           kind: note.kind,
1321         };
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")?
1329         }
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                   ));
1335       }
1336
1337       InstanceBundles::truncate_all_besteffort(&ig.name);
1338
1339       // Actually try to clean up WRECKAGE into NEARLY-ABSENT
1340       for (f,p) in to_clean {
1341         match f(&p) {
1342           Err(e) if e.kind() == ErrorKind::NotFound => { }
1343           x => x.with_context(|| p.clone()).context("clean up")?
1344         }
1345       }
1346
1347       let new_asset_key = AssetUrlKey::new_random()?;
1348
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();
1353
1354       // Prevent old, removed, players from accessing any new bundles.
1355       ig.asset_url_key = new_asset_key;
1356
1357       self.updated(ig);
1358
1359       Ok::<_,IE>(())
1360     })()?;
1361   }
1362 }
1363
1364 #[test]
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);
1370   };
1371   let check_n = |s,m| {
1372     assert_eq!(Id::from_str(s).unwrap_err().to_string(), m)
1373   };
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");
1379 }
1380
1381 #[test]
1382 #[cfg(not(miri))]
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" );
1394 }