Skip to main content

chess/formats/chessbase/
database.rs

1//! Access to ChessBase databases.
2
3use 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
42/// A ChessBase database backed by a reader.
43pub struct Database<R: Reader> {
44    pub reader: R,
45}
46
47/// Input containing ChessBase game data.
48pub 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
56/// Input containing a ChessBase game index as well as game data.
57pub trait IndexReader: Reader {
58    fn entry(&mut self, index: usize) -> Result<Option<Entry>>;
59}
60
61/// A ChessBase archive.
62pub 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
72/// A standalone ChessBase game file.
73pub struct Games<I> {
74    pub input: I,
75    pub header: game::Header,
76}
77
78/// A ChessBase game and index file bundle, with optional metadata tables.
79pub 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
90/// Input accepted by a ChessBase database reader.
91pub trait ReadSeek: Read + Seek {}
92
93/// One game and its available index-derived data.
94pub struct Entry {
95    pub game: game::Game,
96    pub index: Option<Indexed>,
97}
98
99/// Data joined to a game through its ChessBase index entry.
100pub 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
107/// Iterator over standalone ChessBase game records.
108pub struct Entries<'a, I> {
109    input: &'a mut I,
110    body: Vec<u8>,
111    count: usize,
112    end: u64,
113    failed: bool,
114}
115
116/// Iterator over indexed ChessBase game records.
117pub 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
128/// Iterator over archived ChessBase game records.
129pub 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        // CBG reserves 1 KiB for growth after every 100 physical game records.
312        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}