1use std::{
4 fs::File,
5 io::{self, Cursor, Read, Seek, SeekFrom},
6 path::Path,
7};
8
9use crate::{
10 formats::{Text, prelude::*},
11 game::{self as native, Tag},
12};
13
14use super::{
15 archive::{self, Access, Kind, Member},
16 game, index, player, table, tournament,
17};
18
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
21 #[error("invalid ChessBase database")]
22 Data,
23 #[error(transparent)]
24 Archive(#[from] archive::Error),
25 #[error(transparent)]
26 Convert(#[from] game::convert::Error),
27 #[error(transparent)]
28 Io(#[from] io::Error),
29}
30
31impl From<FrameError<ContextMode>> for Error {
32 fn from(error: FrameError<ContextMode>) -> Self {
33 match error {
34 FrameError::Error(_) => Self::Data,
35 FrameError::Io(error) => Self::Io(error),
36 }
37 }
38}
39
40pub type Result<T, E = Error> = core::result::Result<T, E>;
41
42pub struct Database<R: Reader> {
44 pub reader: R,
45}
46
47pub trait Reader {
49 type Entries<'a>: Iterator<Item = Result<Entry>>
50 where
51 Self: 'a;
52
53 fn entries(&mut self) -> Result<Self::Entries<'_>>;
54}
55
56pub trait IndexReader: Reader {
58 fn entry(&mut self, index: usize) -> Result<Option<Entry>>;
59}
60
61pub struct Archive<I> {
63 pub index: Access<I>,
64 pub data: Access<I>,
65 pub members: Vec<Member>,
66 pub games_header: game::Header,
67 pub index_header: index::Header,
68 pub players_header: Option<table::Header>,
69 pub tournaments_header: Option<table::Header>,
70}
71
72pub struct Games<I> {
74 pub input: I,
75 pub header: game::Header,
76}
77
78pub struct Bundle<I> {
80 pub games: I,
81 pub games_header: game::Header,
82 pub index: I,
83 pub index_header: index::Header,
84 pub players: Option<I>,
85 pub players_header: Option<table::Header>,
86 pub tournaments: Option<I>,
87 pub tournaments_header: Option<table::Header>,
88}
89
90pub trait ReadSeek: Read + Seek {}
92
93pub struct Entry {
95 pub game: game::Game,
96 pub index: Option<Indexed>,
97}
98
99pub struct Indexed {
101 pub index: index::Game,
102 pub black: Option<player::Player>,
103 pub tournament: Option<tournament::Tournament>,
104 pub white: Option<player::Player>,
105}
106
107pub struct Entries<'a, I> {
109 input: &'a mut I,
110 body: Vec<u8>,
111 count: usize,
112 end: u64,
113 failed: bool,
114}
115
116pub struct BundleEntries<'a, I> {
118 games: &'a mut I,
119 index: &'a mut I,
120 index_len: usize,
121 players: Option<(&'a mut I, table::Header)>,
122 tournaments: Option<(&'a mut I, table::Header)>,
123 body: Vec<u8>,
124 count: usize,
125 failed: bool,
126}
127
128pub struct ArchiveEntries<'a, I> {
130 index: archive::Reader<'a, I>,
131 data: &'a mut Access<I>,
132 index_len: usize,
133 players_header: Option<table::Header>,
134 tournaments_header: Option<table::Header>,
135 body: Vec<u8>,
136 count: usize,
137 failed: bool,
138}
139
140impl<R: Reader> Database<R> {
141 pub fn games(&mut self) -> Result<impl Iterator<Item = Result<crate::Game>> + '_> {
142 Ok(self.reader.entries()?.map(|entry| entry.and_then(Entry::into_game)))
143 }
144}
145
146impl<R: IndexReader> Database<R> {
147 pub fn game(&mut self, index: usize) -> Result<Option<crate::Game>> {
148 self.reader.entry(index)?.map(Entry::into_game).transpose()
149 }
150}
151
152impl Entry {
153 fn into_game(self) -> Result<crate::Game> {
154 let mut game = crate::Game::from_chessbase(self.game)?;
155 if let Some(indexed) = self.index {
156 indexed.enrich(&mut game);
157 }
158 Ok(game)
159 }
160}
161
162impl Indexed {
163 fn enrich(self, game: &mut crate::Game) {
164 let Self { index, black, tournament, white } = self;
165
166 game.update_roster(|roster| {
167 roster.white = white.and_then(player_name);
168 roster.black = black.and_then(player_name);
169 roster.date = date(index.date);
170 roster.round = round(index.round, index.subround);
171 });
172 game.set_outcome(match index.outcome {
173 index::Outcome::WhiteWins => native::Outcome::White,
174 index::Outcome::BlackWins => native::Outcome::Black,
175 index::Outcome::Draw => native::Outcome::Draw,
176 _ => native::Outcome::Unknown,
177 });
178
179 if let Some(white_elo) = index.white_elo {
180 game.update_tags(|tags| tags.push(tag("WhiteElo", white_elo)));
181 }
182 if let Some(black_elo) = index.black_elo {
183 game.update_tags(|tags| tags.push(tag("BlackElo", black_elo)));
184 }
185 if let Some(tournament) = tournament {
186 game.update_roster(|roster| {
187 roster.event = Text::new(tournament.title).ok();
188 roster.site = Text::new(tournament.place).ok();
189 });
190 if let Some(date) = date(tournament.date) {
191 game.update_tags(|tags| {
192 tags.push(Tag {
193 key: Text::new("EventDate").expect("tag name is non-empty"),
194 value: date.to_string(),
195 });
196 });
197 }
198 }
199 }
200}
201
202fn player_name(player: player::Player) -> Option<Text> {
203 let name = match (player.last_name.is_empty(), player.first_name.is_empty()) {
204 (false, false) => format!("{},{}", player.last_name, player.first_name),
205 (false, true) => player.last_name,
206 (true, false) => player.first_name,
207 (true, true) => return None,
208 };
209 Text::new(name).ok()
210}
211
212fn date(date: index::Date) -> Option<Text> {
213 (date.year.is_some() || date.month.is_some() || date.day.is_some())
214 .then(|| Text::new(date.pgn()).expect("formatted date is non-empty"))
215}
216
217fn round(round: Option<u8>, subround: Option<u8>) -> Option<Text> {
218 match (round, subround) {
219 (Some(round), Some(subround)) => Text::new(format!("{round}.{subround}")).ok(),
220 (Some(round), None) => Text::new(round.to_string()).ok(),
221 (None, Some(subround)) => Text::new(format!("?.{subround}")).ok(),
222 (None, None) => None,
223 }
224}
225
226fn tag(key: &'static str, value: impl ToString) -> Tag {
227 Tag { key: Text::new(key).expect("tag name is non-empty"), value: value.to_string() }
228}
229
230impl<T: Read + Seek> ReadSeek for T {}
231
232impl<I: ReadSeek> Reader for Games<I> {
233 type Entries<'a>
234 = Entries<'a, I>
235 where
236 Self: 'a;
237
238 fn entries(&mut self) -> Result<Self::Entries<'_>> {
239 self.input
240 .seek(SeekFrom::Start(u64::from(self.header.first_game)))
241 .map_err(|_| Error::Data)?;
242
243 Ok(Entries {
244 input: &mut self.input,
245 body: Vec::new(),
246 count: 0,
247 end: u64::from(self.header.len),
248 failed: false,
249 })
250 }
251}
252
253impl<I: ReadSeek> Reader for Bundle<I> {
254 type Entries<'a>
255 = BundleEntries<'a, I>
256 where
257 Self: 'a;
258
259 fn entries(&mut self) -> Result<Self::Entries<'_>> {
260 self.entries_from(0)
261 }
262}
263
264impl<I: ReadSeek> IndexReader for Bundle<I> {
265 fn entry(&mut self, index: usize) -> Result<Option<Entry>> {
266 if index >= self.index_header.len {
267 return Ok(None);
268 }
269 self.entries_from(index)?.next().transpose()
270 }
271}
272
273impl<I: ReadSeek> Reader for Archive<I> {
274 type Entries<'a>
275 = ArchiveEntries<'a, I>
276 where
277 Self: 'a;
278
279 fn entries(&mut self) -> Result<Self::Entries<'_>> {
280 self.entries_from(0)
281 }
282}
283
284impl<I: ReadSeek> IndexReader for Archive<I> {
285 fn entry(&mut self, index: usize) -> Result<Option<Entry>> {
286 if index >= self.index_header.len {
287 return Ok(None);
288 }
289 self.entries_from(index)?.next().transpose()
290 }
291}
292
293impl<I: ReadSeek> Iterator for Entries<'_, I> {
294 type Item = Result<Entry>;
295
296 fn next(&mut self) -> Option<Self::Item> {
297 if self.failed {
298 return None;
299 }
300
301 let result = self.next_entry();
302 if result.is_err() {
303 self.failed = true;
304 }
305 result.transpose()
306 }
307}
308
309impl<I: ReadSeek> Entries<'_, I> {
310 fn next_entry(&mut self) -> Result<Option<Entry>> {
311 if self.count > 0 && self.count.is_multiple_of(100) {
313 self.input.seek(SeekFrom::Current(1024)).map_err(|_| Error::Data)?;
314 }
315
316 let position = self.input.stream_position().map_err(|_| Error::Data)?;
317 if position == self.end {
318 return Ok(None);
319 }
320 if position > self.end {
321 return Err(Error::Data);
322 }
323
324 let game = framed(self.input, &mut self.body, game::info, game::game)?;
325 let end = self.input.stream_position().map_err(|_| Error::Data)?;
326 if end > self.end {
327 return Err(Error::Data);
328 }
329 self.count += 1;
330
331 Ok(Some(Entry { game, index: None }))
332 }
333}
334
335impl<I: ReadSeek> Iterator for BundleEntries<'_, I> {
336 type Item = Result<Entry>;
337
338 fn next(&mut self) -> Option<Self::Item> {
339 if self.failed || self.count == self.index_len {
340 return None;
341 }
342
343 let result = self.next_entry();
344 if result.is_err() {
345 self.failed = true;
346 }
347 Some(result)
348 }
349}
350
351impl<I: ReadSeek> BundleEntries<'_, I> {
352 fn next_entry(&mut self) -> Result<Entry> {
353 let indexed = read_index(self.index)?;
354 let game = read_game(self.games, indexed.offsets.data, &mut self.body)?;
355
356 let white = match self.players.as_mut() {
357 Some((input, header)) => {
358 Some(read_table(input, *header, indexed.offsets.white, player::player)?)
359 }
360 None => None,
361 };
362 let black = match self.players.as_mut() {
363 Some((input, header)) => {
364 Some(read_table(input, *header, indexed.offsets.black, player::player)?)
365 }
366 None => None,
367 };
368 let tournament = match self.tournaments.as_mut() {
369 Some((input, header)) => Some(read_table(
370 input,
371 *header,
372 indexed.offsets.tournament,
373 tournament::tournament,
374 )?),
375 None => None,
376 };
377
378 self.count += 1;
379 Ok(Entry { game, index: Some(Indexed { index: indexed, black, tournament, white }) })
380 }
381}
382
383impl<I: ReadSeek> Iterator for ArchiveEntries<'_, I> {
384 type Item = Result<Entry>;
385
386 fn next(&mut self) -> Option<Self::Item> {
387 if self.failed || self.count == self.index_len {
388 return None;
389 }
390
391 let result = self.next_entry();
392 if result.is_err() {
393 self.failed = true;
394 }
395 Some(result)
396 }
397}
398
399impl<I: ReadSeek> ArchiveEntries<'_, I> {
400 fn next_entry(&mut self) -> Result<Entry> {
401 let indexed = read_index(&mut self.index)?;
402 let game = {
403 let mut games = self.data.reader(Kind::Games).ok_or(Error::Data)?;
404 read_game(&mut games, indexed.offsets.data, &mut self.body)?
405 };
406 let white = match self.players_header {
407 Some(header) => {
408 let mut players = self.data.reader(Kind::Players).ok_or(Error::Data)?;
409 Some(read_table(&mut players, header, indexed.offsets.white, player::player)?)
410 }
411 None => None,
412 };
413 let black = match self.players_header {
414 Some(header) => {
415 let mut players = self.data.reader(Kind::Players).ok_or(Error::Data)?;
416 Some(read_table(&mut players, header, indexed.offsets.black, player::player)?)
417 }
418 None => None,
419 };
420 let tournament = match self.tournaments_header {
421 Some(header) => {
422 let mut tournaments = self.data.reader(Kind::Tournaments).ok_or(Error::Data)?;
423 Some(read_table(
424 &mut tournaments,
425 header,
426 indexed.offsets.tournament,
427 tournament::tournament,
428 )?)
429 }
430 None => None,
431 };
432
433 self.count += 1;
434 Ok(Entry { game, index: Some(Indexed { index: indexed, black, tournament, white }) })
435 }
436}
437
438fn read_index(input: &mut impl ReadSeek) -> Result<index::Game> {
439 let mut bytes = [0; index::Game::LEN];
440 input.read_exact(&mut bytes).map_err(|_| Error::Data)?;
441 index::game.parse(bytes.as_slice()).map_err(|_| Error::Data)
442}
443
444fn read_game(input: &mut impl ReadSeek, offset: usize, body: &mut Vec<u8>) -> Result<game::Game> {
445 input.seek(SeekFrom::Start(offset as u64)).map_err(|_| Error::Data)?;
446 Ok(framed(input, body, game::info, game::game)?)
447}
448
449fn read_table<I: ReadSeek, T>(
450 input: &mut I,
451 header: table::Header,
452 index: usize,
453 mut parser: impl FnMut(&mut &[u8]) -> winnow::ModalResult<T>,
454) -> Result<T> {
455 if index > header.len {
456 return Err(Error::Data);
457 }
458 let len = header.record_len + 9;
459 let offset = header
460 .first_record
461 .checked_add(index.checked_mul(len).ok_or(Error::Data)?)
462 .ok_or(Error::Data)?;
463 input.seek(SeekFrom::Start(offset as u64)).map_err(|_| Error::Data)?;
464 let mut bytes = vec![0; len];
465 input.read_exact(&mut bytes).map_err(|_| Error::Data)?;
466 parser(&mut bytes.as_slice()).map_err(|_| Error::Data)
467}
468
469impl Archive<File> {
470 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
471 Self::new(
472 File::open(path.as_ref()).map_err(|_| Error::Data)?,
473 File::open(path).map_err(|_| Error::Data)?,
474 )
475 }
476}
477
478impl<'a> Archive<Cursor<&'a [u8]>> {
479 pub fn from_slice(input: &'a [u8]) -> Result<Self> {
480 Self::new(Cursor::new(input), Cursor::new(input))
481 }
482}
483
484impl<I: ReadSeek> Archive<I> {
485 fn entries_from(&mut self, count: usize) -> Result<ArchiveEntries<'_, I>> {
486 let mut index = self.index.reader(Kind::Index).ok_or(Error::Data)?;
487 index.seek(SeekFrom::Start(index_offset(count)?)).map_err(|_| Error::Data)?;
488
489 Ok(ArchiveEntries {
490 index,
491 data: &mut self.data,
492 index_len: self.index_header.len,
493 players_header: self.players_header,
494 tournaments_header: self.tournaments_header,
495 body: Vec::new(),
496 count,
497 failed: false,
498 })
499 }
500
501 pub fn new(mut index: I, data: I) -> Result<Self> {
502 index.rewind().map_err(|_| Error::Data)?;
503 let header = archive::read_header(&mut index)?;
504 let members = header.members;
505 let mut index = Access::new(index, &members)?;
506 let mut data = Access::new(data, &members)?;
507 let games_header = {
508 let mut games = data.reader(Kind::Games).ok_or(Error::Data)?;
509 read_header::<{ game::Header::LEN }, _, _>(&mut games, game::header)?
510 };
511 let index_header = {
512 let mut input = index.reader(Kind::Index).ok_or(Error::Data)?;
513 read_header::<{ index::Header::LEN }, _, _>(&mut input, index::header)?
514 };
515 let players_header = match data.reader(Kind::Players) {
516 Some(mut players) => Some(read_table_header(&mut players)?),
517 None => None,
518 };
519 let tournaments_header = match data.reader(Kind::Tournaments) {
520 Some(mut tournaments) => Some(read_table_header(&mut tournaments)?),
521 None => None,
522 };
523 Ok(Self {
524 index,
525 data,
526 members,
527 games_header,
528 index_header,
529 players_header,
530 tournaments_header,
531 })
532 }
533}
534
535impl<I: ReadSeek> Games<I> {
536 pub fn new(mut input: I) -> Result<Self> {
537 input.rewind().map_err(|_| Error::Data)?;
538 let mut bytes = [0; 26];
539 input.read_exact(&mut bytes).map_err(|_| Error::Data)?;
540 let header = game::header.parse(bytes.as_slice()).map_err(|_| Error::Data)?;
541 Ok(Self { input, header })
542 }
543}
544
545impl Games<File> {
546 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
547 Self::new(File::open(path).map_err(|_| Error::Data)?)
548 }
549}
550
551impl<'a> Games<Cursor<&'a [u8]>> {
552 pub fn from_slice(input: &'a [u8]) -> Result<Self> {
553 Self::new(Cursor::new(input))
554 }
555}
556
557impl<I: ReadSeek> Bundle<I> {
558 fn entries_from(&mut self, count: usize) -> Result<BundleEntries<'_, I>> {
559 self.index.seek(SeekFrom::Start(index_offset(count)?)).map_err(|_| Error::Data)?;
560
561 Ok(BundleEntries {
562 games: &mut self.games,
563 index: &mut self.index,
564 index_len: self.index_header.len,
565 players: self.players.as_mut().zip(self.players_header),
566 tournaments: self.tournaments.as_mut().zip(self.tournaments_header),
567 body: Vec::new(),
568 count,
569 failed: false,
570 })
571 }
572
573 pub fn new(mut games: I, mut index: I) -> Result<Self> {
574 let games_header = read_header::<{ game::Header::LEN }, _, _>(&mut games, game::header)?;
575 let index_header = read_header::<{ index::Header::LEN }, _, _>(&mut index, index::header)?;
576 Ok(Self {
577 games,
578 games_header,
579 index,
580 index_header,
581 players: None,
582 players_header: None,
583 tournaments: None,
584 tournaments_header: None,
585 })
586 }
587
588 fn set_players(&mut self, mut input: I) -> Result<()> {
589 self.players_header = Some(read_table_header(&mut input)?);
590 self.players = Some(input);
591 Ok(())
592 }
593
594 fn set_tournaments(&mut self, mut input: I) -> Result<()> {
595 self.tournaments_header = Some(read_table_header(&mut input)?);
596 self.tournaments = Some(input);
597 Ok(())
598 }
599}
600
601impl Bundle<File> {
602 pub fn from_files(games: impl AsRef<Path>, index: impl AsRef<Path>) -> Result<Self> {
603 Self::new(
604 File::open(games).map_err(|_| Error::Data)?,
605 File::open(index).map_err(|_| Error::Data)?,
606 )
607 }
608
609 pub fn with_players(mut self, players: impl AsRef<Path>) -> Result<Self> {
610 self.set_players(File::open(players).map_err(|_| Error::Data)?)?;
611 Ok(self)
612 }
613
614 pub fn with_tournaments(mut self, tournaments: impl AsRef<Path>) -> Result<Self> {
615 self.set_tournaments(File::open(tournaments).map_err(|_| Error::Data)?)?;
616 Ok(self)
617 }
618}
619
620impl<'a> Bundle<Cursor<&'a [u8]>> {
621 pub fn from_slices(games: &'a [u8], index: &'a [u8]) -> Result<Self> {
622 Self::new(Cursor::new(games), Cursor::new(index))
623 }
624
625 pub fn with_players(mut self, players: &'a [u8]) -> Result<Self> {
626 self.set_players(Cursor::new(players))?;
627 Ok(self)
628 }
629
630 pub fn with_tournaments(mut self, tournaments: &'a [u8]) -> Result<Self> {
631 self.set_tournaments(Cursor::new(tournaments))?;
632 Ok(self)
633 }
634}
635
636fn read_header<const N: usize, I: ReadSeek, T>(
637 input: &mut I,
638 mut parser: impl FnMut(&mut &[u8]) -> winnow::ModalResult<T>,
639) -> Result<T> {
640 input.rewind().map_err(|_| Error::Data)?;
641 let mut bytes = [0; N];
642 input.read_exact(&mut bytes).map_err(|_| Error::Data)?;
643 parser(&mut bytes.as_slice()).map_err(|_| Error::Data)
644}
645
646fn read_table_header(input: &mut impl ReadSeek) -> Result<table::Header> {
647 input.rewind().map_err(|_| Error::Data)?;
648 let mut bytes = [0; 32];
649 input.read_exact(&mut bytes).map_err(|_| Error::Data)?;
650 table::header.parse_next(&mut bytes.as_slice()).map_err(|_| Error::Data)
651}
652
653fn index_offset(index: usize) -> Result<u64> {
654 let offset = index
655 .checked_mul(index::Game::LEN)
656 .and_then(|offset| offset.checked_add(index::Header::LEN))
657 .ok_or(Error::Data)?;
658 u64::try_from(offset).map_err(|_| Error::Data)
659}
660
661#[cfg(test)]
662mod tests {
663 use hex_literal::hex;
664
665 use crate::formats::twic;
666
667 use super::{archive::Range, *};
668
669 const GAME: [u8; 109] = hex!(
670 "0000006d0b08dc870210c19a0a11008182882659a72344d4124253447cf81804
671 98bb07014e172835883cab0772e32431097526317d4384464d3e0a1490fc4c78
672 fa61f88cb815f05b46c97dc02979f1e2462aafd3c9de2f9d06aa9914c7ffa818
673 54e1b5db120fc36c1c9b9f2674"
674 );
675 const INDEX_GAME: [u8; index::Game::LEN] = hex!(
676 "010000001a000000000000000000010000000000000000010fd355000003250a
677 240ac7cd80000000000000000034"
678 );
679 const WHITE: [u8; 67] = hex!(
680 "7e010000ffffffffff4368656e6700686f007200006c000ade01000000000080
681 000000009e8600426f007400617370726f00646f736f00000000000500000001
682 000000"
683 );
684 const BLACK: [u8; 67] = hex!(
685 "ffffffffffffffff00476972690069204a007300657200007900790079007900
686 000000008f86004100616e656d006c696e61006e6f7300010000000500000001
687 000000"
688 );
689 const TOURNAMENT: [u8; 99] = hex!(
690 "ffffffffffffffff003430746820454343204f70656e20323032350032303235
691 0000009f0dde010000e8f9e176de01000052686f646573204752450052410000
692 74204155540001000080d8c0b9f87f53d30f0000013700000007005802000001
693 000000"
694 );
695 const GAMES_HEADER: [u8; game::Header::LEN] =
696 hex!("001a000000870000000000000000000000870000000000000000");
697 const INDEX_HEADER: [u8; index::Header::LEN] = hex!(
698 "00002c002e050000000200000000000000000000000000000000000000000000
699 0000000000000000000000020000"
700 );
701
702 fn games() -> Vec<u8> {
703 let mut input = GAMES_HEADER.to_vec();
704 input.extend(GAME);
705 input
706 }
707
708 fn index() -> Vec<u8> {
709 let mut input = INDEX_HEADER.to_vec();
710 input.extend(INDEX_GAME);
711 input
712 }
713
714 fn table(records: &[&[u8]], record_len: u32) -> Vec<u8> {
715 let len = u32::try_from(records.len() - 1).expect("compact table length fits u32");
716 let mut input = Vec::new();
717 input.extend(len.to_le_bytes());
718 input.extend(0u32.to_le_bytes());
719 input.extend(1_234_567_890u32.to_le_bytes());
720 input.extend(record_len.to_le_bytes());
721 input.extend((-1i32).to_le_bytes());
722 input.extend(u32::try_from(records.len()).unwrap().to_le_bytes());
723 input.extend(0u32.to_le_bytes());
724 for record in records {
725 input.extend_from_slice(record);
726 }
727 input
728 }
729
730 #[test]
731 fn games_from_slice() {
732 let input = games();
733 let mut games = Games::from_slice(&input).unwrap();
734 assert_eq!(games.entries().unwrap().count(), 1);
735
736 let first = games.entries().unwrap().next().expect("first CBG game");
737 first.expect("can reread first CBG game");
738 }
739
740 #[test]
741 fn bundle_from_slices() {
742 let games = games();
743 let index = index();
744 let players = table(&[&WHITE, &BLACK], 58);
745 let tournaments = table(&[&TOURNAMENT], 90);
746 let mut bundle = Bundle::from_slices(&games, &index)
747 .unwrap()
748 .with_players(&players)
749 .unwrap()
750 .with_tournaments(&tournaments)
751 .unwrap();
752
753 let entry = bundle.entries().unwrap().next().expect("first indexed game").unwrap();
754 let indexed = entry.index.expect("index data");
755 assert_eq!(indexed.index.offsets.data, 26);
756 assert!(indexed.white.is_some());
757 assert!(indexed.black.is_some());
758 assert!(indexed.tournament.is_some());
759
760 let mut database = Database { reader: bundle };
761 assert!(database.game(0).unwrap().is_some());
762 assert!(database.game(1).unwrap().is_none());
763 let game = database.games().unwrap().next().expect("first game").unwrap();
764 assert!(game.roster().white.is_some());
765 assert!(game.roster().black.is_some());
766 assert!(game.roster().event.is_some());
767 assert!(game.roster().site.is_some());
768 }
769
770 #[test]
771 #[ignore = "exhaustive test against the external TWIC fixture"]
772 fn twic_archive_from_slice() {
773 let cbv = twic::data(1616, "cbv");
774 let mut archive = Archive::from_slice(&cbv).unwrap();
775 let index = archive
776 .members
777 .iter()
778 .find(|member| member.name.ends_with(".cbh"))
779 .expect("CBH member");
780 let games = archive
781 .members
782 .iter()
783 .find(|member| member.name.ends_with(".cbg"))
784 .expect("CBG member");
785
786 assert_eq!(index.packed, Range::from(2430..130450));
787 assert_eq!(games.packed, Range::from(131267..724157));
788
789 let unpacked_len = games.len;
790 let mut games = archive.data.reader(Kind::Games).expect("CBG reader");
791 let mut unpacked = Vec::new();
792 games.read_to_end(&mut unpacked).unwrap();
793 assert_eq!(unpacked.len(), unpacked_len);
794
795 let mut database = Database { reader: archive };
796 assert!(database.game(0).unwrap().is_some());
797 let game = database.games().unwrap().next().expect("first archived game").unwrap();
798 assert!(game.roster().white.is_some());
799 assert!(game.roster().black.is_some());
800 assert!(game.roster().event.is_some());
801 assert!(game.roster().site.is_some());
802 }
803
804 #[test]
805 #[ignore = "exhaustive test against the external TWIC fixture"]
806 fn twic_archive_seek() {
807 let cbv = twic::data(1616, "cbv");
808 let expected = twic::data(1616, "cbg");
809 let mut archive = Archive::from_slice(&cbv).unwrap();
810 let mut games = archive.data.reader(Kind::Games).expect("CBG reader");
811
812 for position in [0, 61_400, 122_900, 70_000, 300_000, 100, expected.len() - 257] {
813 games.seek(SeekFrom::Start(position as u64)).unwrap();
814 let mut actual = [0; 257];
815 games.read_exact(&mut actual).unwrap();
816 assert_eq!(actual.as_slice(), &expected[position..position + actual.len()]);
817 }
818 }
819
820 #[test]
821 #[ignore = "exhaustive comparison against the external TWIC fixture"]
822 fn twic_archive_bundle() {
823 let cbv = twic::data(1616, "cbv");
824 let cbg = twic::data(1616, "cbg");
825 let cbh = twic::data(1616, "cbh");
826 let cbp = twic::data(1616, "cbp");
827 let cbt = twic::data(1616, "cbt");
828 let archive = Archive::from_slice(cbv.as_slice()).unwrap();
829 let bundle = Bundle::from_slices(cbg.as_slice(), cbh.as_slice())
830 .unwrap()
831 .with_players(cbp.as_slice())
832 .unwrap()
833 .with_tournaments(cbt.as_slice())
834 .unwrap();
835 let mut archive = Database { reader: archive };
836 let mut bundle = Database { reader: bundle };
837 let mut archive = archive.games().unwrap();
838 let mut bundle = bundle.games().unwrap();
839 let mut count = 0;
840
841 loop {
842 match (archive.next(), bundle.next()) {
843 (Some(archive), Some(bundle)) => {
844 let archive = archive.unwrap_or_else(|_| panic!("archived game {count}"));
845 let bundle = bundle.unwrap_or_else(|_| panic!("bundled game {count}"));
846 assert!(archive == bundle, "archived and bundled game {count} differ");
847 count += 1;
848 }
849 (None, None) => break,
850 _ => panic!("archive and bundle lengths differ after game {count}"),
851 }
852 }
853 assert_eq!(count, 6251);
854 }
855
856 #[test]
857 #[ignore = "test against the external TWIC fixture"]
858 fn twic_archive_from_file() {
859 let archive = Archive::from_file(twic::path(1616, "cbv")).unwrap();
860 let mut database = Database { reader: archive };
861 database.games().unwrap().next().expect("first archived game").unwrap();
862 }
863}