rsshogi 1.2.2

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
#![cfg(any(feature = "book", feature = "position-serialization", feature = "records"))]

#[cfg(feature = "book")]
use std::{
    fs,
    path::PathBuf,
    time::{SystemTime, UNIX_EPOCH},
};

#[cfg(feature = "position-serialization")]
use rsshogi::board::{PackedSfen, Position};
#[cfg(feature = "book")]
use rsshogi::book::{
    BookCandidate, BookDatabase, BookDatabaseEntry, BookEntryMetadata, BookMoveMetadata,
    BookPosition, YaneuraOuAccessMode, YaneuraOuBook, YaneuraOuBookOpenOptions,
    YaneuraOuDb2016WriteOptions, YbbBook,
};
#[cfg(feature = "records")]
use rsshogi::records::formats::pack::{PackError, PackGameResult, decode_game};
#[cfg(feature = "book")]
use rsshogi::types::Move;

#[cfg(feature = "book")]
fn temp_path(suffix: &str) -> PathBuf {
    let nonce = SystemTime::now().duration_since(UNIX_EPOCH).expect("clock after epoch").as_nanos();
    std::env::temp_dir()
        .join(format!("rsshogi-task0026-malformed-{}-{nonce}-{suffix}", std::process::id()))
}

#[cfg(feature = "position-serialization")]
#[test]
fn all_zero_packed_sfen_is_an_error_and_never_panics() {
    let packed = PackedSfen::from_bytes([0; 32]);
    let result = std::panic::catch_unwind(|| {
        let mut position = Position::empty();
        position.set_packed_sfen(&packed, false, 0)
    });
    assert!(result.is_ok(), "malformed PackedSfen must not panic");
    assert!(result.expect("catch result").is_err());
}

#[cfg(feature = "records")]
#[test]
fn pack_rejects_structurally_invalid_raw_moves() {
    for raw in [0x0204_u16, 0xc081_u16, 0x6880_u16, 0x2c00_u16] {
        let bytes = [1, raw as u8, (raw >> 8) as u8, 0, 0, 0, 0, 0];
        assert!(decode_game(&bytes).is_err(), "raw {raw:#06x} must be rejected");
    }
}

#[cfg(feature = "records")]
#[test]
fn pack_accepts_structurally_valid_apery_promotion() {
    let bytes = [1, 0x80, 0x48, 0, 0, 0x81, 0, 0];
    let (game, consumed) = decode_game(&bytes).expect("Apery promotion must decode");
    assert_eq!(game.plies[0].mv.raw(), 0x4880);
    assert_eq!(consumed, bytes.len());
}

#[cfg(feature = "records")]
#[test]
fn pack_rejects_inconsistent_terminal_outcome_during_decode() {
    assert!(matches!(
        decode_game(&[1, 0, 0, 0]),
        Err(PackError::InconsistentOutcome { result: PackGameResult::Draw, .. })
    ));
}

#[cfg(feature = "records")]
#[test]
fn pack_validates_hcp_start_payload_during_decode() {
    let mut bytes = vec![0];
    bytes.extend_from_slice(&[0xff; 32]);
    bytes.extend_from_slice(&1_u16.to_le_bytes());
    bytes.extend_from_slice(&0x81_u16.to_le_bytes());
    bytes.push(0);
    assert!(decode_game(&bytes).is_err());
}

#[cfg(feature = "book")]
#[test]
fn db2016_reports_invalid_drop_piece_for_that_entry() {
    let path = temp_path("invalid.db");
    let text = "#YANEURAOU-DB2016 1.00\n\
sfen 4k4/9/9/9/9/9/9/9/4K4 b - 1\n\
K*5e none 0 0 0\n";
    fs::write(&path, text).expect("fixture write");
    let options = YaneuraOuBookOpenOptions::with_access_mode(YaneuraOuAccessMode::ScanOnly);
    let book = YaneuraOuBook::open_with_options(&path, options).expect("open structural book");
    assert!(book.iter_entries().expect("iterator").next().expect("entry result").is_err());
    let _ = fs::remove_file(path);
}

#[cfg(feature = "book")]
#[test]
fn db2016_writer_rejects_noncanonical_raw_drop_source() {
    let position = BookPosition::from_position(&rsshogi::board::hirate_position(), Some(1));
    let candidate = BookCandidate::new(
        Move::from_raw(0x4880),
        None,
        None,
        None,
        None,
        None,
        BookMoveMetadata::new(),
    );
    let entry = BookDatabaseEntry::new(position, vec![candidate], BookEntryMetadata::new());
    let database = BookDatabase::try_from_entries(vec![entry]).expect("database");
    assert!(database.to_yaneuraou_db2016_string(&YaneuraOuDb2016WriteOptions::new()).is_err());
}

#[cfg(feature = "book")]
#[test]
fn db2016_unsorted_checked_prefix_cannot_cause_lookup_miss() {
    let path = temp_path("unsorted.db");
    let later = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1";
    let earlier = "4k4/9/9/9/9/9/9/9/4K4 b - 1";
    let text = format!("#YANEURAOU-DB2016 1.00\nsfen {later}\nsfen {earlier}\n");
    fs::write(&path, text).expect("fixture write");
    let options =
        YaneuraOuBookOpenOptions::with_access_mode(YaneuraOuAccessMode::AssumeSortedAfterPrefix {
            prefix_rows: 2,
        });
    let result =
        YaneuraOuBook::open_with_options(&path, options).and_then(|book| book.lookup_sfen(earlier));
    assert!(result.is_err() || result.expect("lookup result").is_some());
    let _ = fs::remove_file(path);
}

#[cfg(feature = "book")]
#[test]
fn ybb_rejects_same_square_board_move() {
    let path = temp_path("invalid.ybb");
    let position = rsshogi::board::hirate_position();
    let mut bytes = Vec::new();
    bytes.extend_from_slice(b"YANE-BINBOOK-V1\0");
    bytes.extend_from_slice(&1_u64.to_le_bytes());
    bytes.extend_from_slice(&0_u64.to_le_bytes());
    bytes.extend_from_slice(position.to_packed_sfen().as_bytes());
    bytes.extend_from_slice(&0_u64.to_le_bytes());
    bytes.extend_from_slice(&position.game_ply().to_le_bytes());
    bytes.extend_from_slice(&1_u16.to_le_bytes());
    bytes.extend_from_slice(&0x0204_u16.to_le_bytes());
    bytes.extend_from_slice(&0_i16.to_le_bytes());
    fs::write(&path, bytes).expect("fixture write");
    assert!(YbbBook::open(&path).is_err());
    let _ = fs::remove_file(path);
}

#[cfg(feature = "book")]
#[test]
fn ybb_rejects_noncanonical_raw_drop_source() {
    let path = temp_path("noncanonical-drop.ybb");
    let position = rsshogi::board::hirate_position();
    let mut bytes = Vec::new();
    bytes.extend_from_slice(b"YANE-BINBOOK-V1\0");
    bytes.extend_from_slice(&1_u64.to_le_bytes());
    bytes.extend_from_slice(&0_u64.to_le_bytes());
    bytes.extend_from_slice(position.to_packed_sfen().as_bytes());
    bytes.extend_from_slice(&0_u64.to_le_bytes());
    bytes.extend_from_slice(&position.game_ply().to_le_bytes());
    bytes.extend_from_slice(&1_u16.to_le_bytes());
    bytes.extend_from_slice(&0x4880_u16.to_le_bytes());
    bytes.extend_from_slice(&0_i16.to_le_bytes());
    fs::write(&path, bytes).expect("fixture write");
    assert!(YbbBook::open(&path).is_err());
    let _ = fs::remove_file(path);
}