From b5856a77870f7cdd560068df6e57987510ccc891 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 7 Oct 2026 22:53:51 +0300 Subject: [PATCH] Fix FAT chain termination and harden SD driver error handling --- _sd/body_syms.inc | 845 +++++++++++++++++----------------- _sd/tests/README.md | 49 ++ _sd/tests/test_driver.py | 362 +++++++++++++++ _sd/tests/test_fat_offsets.py | 36 +- _sd/tests/test_resident.py | 80 ++++ _sd/zc_sd_driver.asm | 8 + _sd/zc_sd_driver_body.asm | 65 ++- _spg/ZIFI/zifi.spg | Bin 44544 -> 44544 bytes 8 files changed, 1002 insertions(+), 443 deletions(-) create mode 100644 _sd/tests/README.md create mode 100644 _sd/tests/test_driver.py create mode 100644 _sd/tests/test_resident.py diff --git a/_sd/body_syms.inc b/_sd/body_syms.inc index 2e1de26..8883ed9 100644 --- a/_sd/body_syms.inc +++ b/_sd/body_syms.inc @@ -1,403 +1,406 @@ -body_end: EQU 0x00001FAA -cmkf_open_stream: EQU 0x00001F75 -cmkf_final: EQU 0x00001F5F -cmkf_buildlfn: EQU 0x00001F43 -cmkf_entry: EQU 0x00001F01 -cmkf_havecl: EQU 0x00001ED3 -cmkf_alloc: EQU 0x00001EBD -cmkf_havelen: EQU 0x00001EA4 -cmkf_lendone: EQU 0x00001E9B -cmkf_lenloop: EQU 0x00001E8E -core_mkfile: EQU 0x00001E73 -cmkf_len_lo: EQU 0x00001E71 -cmkf_len_hi: EQU 0x00001E6F -cmkd_final: EQU 0x00001E5A -cmkd_buildlfn: EQU 0x00001E3E -cmkd_ownentry: EQU 0x00001E0B -dotdot_name: EQU 0x00001F91 -cmkd_dot_ok: EQU 0x00001DBF -dot_name: EQU 0x00001F86 -clear_dir_entry: EQU 0x00001F9C -cmkd_dots: EQU 0x00001D6B -cmkd_zf: EQU 0x00001D55 -cmkd_have_cl: EQU 0x00001D27 -cmkd_alloc: EQU 0x00001D11 -cmkd_toolong: EQU 0x00001CF0 -cmkd_lendone: EQU 0x00001CF4 -core_mkdir: EQU 0x00001CE3 -cmk_sec_lo: EQU 0x00001CE1 -cmk_sec_hi: EQU 0x00001CDF -cmk_cluster_lo: EQU 0x00001CDD -cmk_cluster_hi: EQU 0x00001CDB -cmk_name_ptr: EQU 0x00001CD9 -sv512_nowrap: EQU 0x00001CD5 -lv512_nowrap: EQU 0x00001CA8 -lv512_advance: EQU 0x00001C92 -lv512_eoc: EQU 0x00001CB5 -lv512_done: EQU 0x00001CAC -lv512_loop: EQU 0x00001C71 -core_load512: EQU 0x00001C66 -lv512_count: EQU 0x00001C65 -lv512_off: EQU 0x00001C63 -lv512_page: EQU 0x00001C62 -sv512_advance: EQU 0x00001CBF -sv512_eoc: EQU 0x00001C58 -sv512_done: EQU 0x00001C4F -sv512_loop: EQU 0x00001C2E -core_save512: EQU 0x00001C23 -sv512_count: EQU 0x00001C22 +body_end: EQU 0x00001FE7 +cmkf_open_stream: EQU 0x00001FB2 +cmkf_final: EQU 0x00001F9C +cmkf_buildlfn: EQU 0x00001F80 +cmkf_entry: EQU 0x00001F3E +cmkf_havecl: EQU 0x00001F10 +cmkf_alloc: EQU 0x00001EFA +cmkf_havelen: EQU 0x00001EE1 +cmkf_lendone: EQU 0x00001ED8 +cmkf_lenloop: EQU 0x00001ECB +core_mkfile: EQU 0x00001EB0 +cmkf_len_lo: EQU 0x00001EAE +cmkf_len_hi: EQU 0x00001EAC +cmkd_final: EQU 0x00001E97 +cmkd_buildlfn: EQU 0x00001E7B +cmkd_ownentry: EQU 0x00001E48 +dotdot_name: EQU 0x00001FCE +cmkd_dot_ok: EQU 0x00001DFC +dot_name: EQU 0x00001FC3 +clear_dir_entry: EQU 0x00001FD9 +cmkd_dots: EQU 0x00001DA8 +cmkd_zf: EQU 0x00001D92 +cmkd_have_cl: EQU 0x00001D64 +cmkd_alloc: EQU 0x00001D4E +cmkd_toolong: EQU 0x00001D2D +cmkd_lendone: EQU 0x00001D31 +core_mkdir: EQU 0x00001D20 +cmk_sec_lo: EQU 0x00001D1E +cmk_sec_hi: EQU 0x00001D1C +cmk_cluster_lo: EQU 0x00001D1A +cmk_cluster_hi: EQU 0x00001D18 +cmk_name_ptr: EQU 0x00001D16 +sv512_nowrap: EQU 0x00001D12 +lv512_nowrap: EQU 0x00001CE5 +lv512_advance: EQU 0x00001CCF +lv512_eoc: EQU 0x00001CF2 +lv512_done: EQU 0x00001CE9 +lv512_loop: EQU 0x00001CAE +core_load512: EQU 0x00001CA3 +lv512_count: EQU 0x00001CA2 +lv512_off: EQU 0x00001CA0 +lv512_page: EQU 0x00001C9F +sv512_advance: EQU 0x00001CFC +sv512_eoc: EQU 0x00001C95 +sv512_done: EQU 0x00001C8C +sv512_loop: EQU 0x00001C6B +core_save512: EQU 0x00001C60 +sv512_count: EQU 0x00001C5F page1_port: EQU 0x000011AF -core_fentry: EQU 0x00001C0E -core_setdir: EQU 0x00001C01 -core_setroot: EQU 0x00001BF4 -active_dir_lo: EQU 0x00001BF2 -active_dir_hi: EQU 0x00001BF0 -fbn_tm_yes: EQU 0x00001BB2 -fbn_tm_short: EQU 0x00001BA0 -fbn_acc_null: EQU 0x00001B72 -fbn_acc_pad: EQU 0x00001B7D -fbn_acc_charloop: EQU 0x00001B46 -fbn_acc_mul: EQU 0x00001B36 -fbn_acc_mul_done: EQU 0x00001B3C -fbn_matched_setrun: EQU 0x00001B07 -fbn_matched_norun: EQU 0x00001B04 -fbn_accumulate_lfn: EQU 0x00001B0D -fbn_lfn_cont: EQU 0x00001ACE -fbn_next: EQU 0x00001AD9 -fbn_matched: EQU 0x00001AF2 -fbn_try_match: EQU 0x00001B80 -fbn_is_lfn: EQU 0x00001AC0 -fbn_deleted: EQU 0x00001AD3 -fbn_notfound_pop: EQU 0x00001AEF -fbn_entloop: EQU 0x00001A9F -fbn_notfound: EQU 0x00001AF0 -fbn_sector_loop: EQU 0x00001A7F -find_by_name: EQU 0x00001A6F -fbn_match_run_start_lo: EQU 0x00001A6D -fbn_match_index_lo: EQU 0x00001A6B -fbn_run_start_lo: EQU 0x00001A69 -fbn_cur_index_lo: EQU 0x00001A67 -fbn_render_buf: EQU 0x00001A5A -fbn_attr: EQU 0x00001A59 -fbn_size_lo: EQU 0x00001A57 -fbn_size_hi: EQU 0x00001A55 -fbn_cluster_hi: EQU 0x00001A51 -fbn_acc_dest: EQU 0x00001A4F -fbn_lfn_seen: EQU 0x00001A4E -fbn_lfn_checksum: EQU 0x00001A4D -fbn_lfn_buf: EQU 0x000019E4 -fbn_name_ptr: EQU 0x000019E2 -rsn_ext: EQU 0x000019D4 -rsn_done: EQU 0x000019DE -rsn_ext_prep: EQU 0x000019C9 -rsn_base_skip: EQU 0x000019C6 -rsn_base: EQU 0x000019BA -render_shortname: EQU 0x000019B7 -seci_eq: EQU 0x000019B5 -seci_ne: EQU 0x000019B2 -str_eq_ci: EQU 0x0000199F -to_upper: EQU 0x00001996 -wne_w_skip_write: EQU 0x0000198D -wne_w_notinrange: EQU 0x0000195F -wne_w_entloop: EQU 0x00001929 -wne_w_fail: EQU 0x00001993 -wne_w_continue: EQU 0x000018F4 -wne_w_done: EQU 0x000018F2 -wne_w_sector_loop: EQU 0x000018DD -write_name_entries: EQU 0x000018D4 -wne_w_this_sec_lo: EQU 0x000018D2 -wne_w_this_sec_hi: EQU 0x000018D0 -wne_w_dirty: EQU 0x000018CF -wne_w_entcount: EQU 0x000018CE -wne_w_entptr: EQU 0x000018CC -ffr_no_hi_carry: EQU 0x0000184C -ffr_free_cont: EQU 0x00001815 -ffr_advance_index: EQU 0x00001836 -ffr_free: EQU 0x000017FF -ffr_entloop: EQU 0x000017F0 -ffr_fail: EQU 0x00001859 -ffr_grow_dir: EQU 0x0000185C -ffr_have_sector: EQU 0x000017D8 -ffr_sector_loop: EQU 0x000017C9 -find_free_run: EQU 0x000017B9 -wne_free_index_lo: EQU 0x000017B7 -wne_free_index_hi: EQU 0x000017B5 -cmkd_lenloop: EQU 0x00001CE8 -wne_cur_index_lo: EQU 0x000017B3 -wne_cur_index_hi: EQU 0x000017B1 -wne_run_start_lo: EQU 0x000017AF -wne_run_start_hi: EQU 0x000017AD -wne_run_len: EQU 0x000017AC -ble_char_nextk: EQU 0x00001779 -ble_char_hi0: EQU 0x00001777 -ble_char_have: EQU 0x0000175D -ble_char_isreal: EQU 0x00001753 -ble_char_isnull: EQU 0x0000175C -ble_lfn_next: EQU 0x0000177C -ble_charloop: EQU 0x00001734 -ble_mul13done: EQU 0x0000172F -ble_mul13: EQU 0x00001724 -ble_seqset: EQU 0x00001706 -ble_notlast: EQU 0x00001705 -ble_lfn_done: EQU 0x00001783 -ble_lfn_loop: EQU 0x000016CE -ble_divdone: EQU 0x000016B7 -ble_divloop: EQU 0x000016AC -ble_multi: EQU 0x000016A3 -build_lfn_entries: EQU 0x0000167E -ble_entry_ptr: EQU 0x0000167C -ble_charpos: EQU 0x0000167A -ble_j: EQU 0x00001679 -ble_checksum: EQU 0x00001678 -ble_lfn_count: EQU 0x00001677 -ble_name_ptr: EQU 0x00001675 -lfn_char_dest: EQU 0x00001668 -wne_slot_count: EQU 0x00001667 -wne_entries_buf: EQU 0x00001547 +core_fentry: EQU 0x00001C4B +core_setdir: EQU 0x00001C3E +core_setroot: EQU 0x00001C31 +active_dir_lo: EQU 0x00001C2F +active_dir_hi: EQU 0x00001C2D +fbn_tm_yes: EQU 0x00001BEF +fbn_tm_short: EQU 0x00001BDD +fbn_acc_null: EQU 0x00001BAF +fbn_acc_pad: EQU 0x00001BBA +fbn_acc_charloop: EQU 0x00001B83 +fbn_acc_mul: EQU 0x00001B73 +fbn_acc_mul_done: EQU 0x00001B79 +fbn_matched_setrun: EQU 0x00001B44 +fbn_matched_norun: EQU 0x00001B41 +fbn_accumulate_lfn: EQU 0x00001B4A +fbn_lfn_cont: EQU 0x00001B0B +fbn_next: EQU 0x00001B16 +fbn_matched: EQU 0x00001B2F +fbn_try_match: EQU 0x00001BBD +fbn_is_lfn: EQU 0x00001AFD +fbn_deleted: EQU 0x00001B10 +fbn_notfound_pop: EQU 0x00001B2C +fbn_entloop: EQU 0x00001ADC +fbn_notfound: EQU 0x00001B2D +fbn_sector_loop: EQU 0x00001ABC +find_by_name: EQU 0x00001AAC +fbn_match_run_start_lo: EQU 0x00001AAA +fbn_match_index_lo: EQU 0x00001AA8 +fbn_run_start_lo: EQU 0x00001AA6 +fbn_cur_index_lo: EQU 0x00001AA4 +fbn_render_buf: EQU 0x00001A97 +fbn_attr: EQU 0x00001A96 +fbn_size_lo: EQU 0x00001A94 +fbn_size_hi: EQU 0x00001A92 +fbn_cluster_hi: EQU 0x00001A8E +fbn_acc_dest: EQU 0x00001A8C +fbn_lfn_seen: EQU 0x00001A8B +fbn_lfn_checksum: EQU 0x00001A8A +fbn_lfn_buf: EQU 0x00001A21 +fbn_name_ptr: EQU 0x00001A1F +rsn_ext: EQU 0x00001A11 +rsn_done: EQU 0x00001A1B +rsn_ext_prep: EQU 0x00001A06 +rsn_base_skip: EQU 0x00001A03 +rsn_base: EQU 0x000019F7 +render_shortname: EQU 0x000019F4 +seci_eq: EQU 0x000019F2 +seci_ne: EQU 0x000019EF +str_eq_ci: EQU 0x000019DC +to_upper: EQU 0x000019D3 +wne_w_skip_write: EQU 0x000019CA +wne_w_notinrange: EQU 0x0000199C +wne_w_entloop: EQU 0x00001966 +wne_w_fail: EQU 0x000019D0 +wne_w_continue: EQU 0x00001931 +wne_w_done: EQU 0x0000192F +wne_w_sector_loop: EQU 0x0000191A +write_name_entries: EQU 0x00001911 +wne_w_this_sec_lo: EQU 0x0000190F +wne_w_this_sec_hi: EQU 0x0000190D +wne_w_dirty: EQU 0x0000190C +wne_w_entcount: EQU 0x0000190B +wne_w_entptr: EQU 0x00001909 +ffr_no_hi_carry: EQU 0x00001889 +ffr_free_cont: EQU 0x00001852 +ffr_advance_index: EQU 0x00001873 +ffr_free: EQU 0x0000183C +ffr_entloop: EQU 0x0000182D +ffr_grow_dir: EQU 0x00001899 +ffr_have_sector: EQU 0x00001815 +ffr_fail: EQU 0x00001896 +ffr_sector_loop: EQU 0x00001801 +find_free_run: EQU 0x000017F1 +wne_free_index_lo: EQU 0x000017EF +wne_free_index_hi: EQU 0x000017ED +cmkd_lenloop: EQU 0x00001D25 +wne_cur_index_lo: EQU 0x000017EB +wne_cur_index_hi: EQU 0x000017E9 +wne_run_start_lo: EQU 0x000017E7 +wne_run_start_hi: EQU 0x000017E5 +wne_run_len: EQU 0x000017E4 +ble_char_nextk: EQU 0x000017B1 +ble_char_hi0: EQU 0x000017AF +ble_char_have: EQU 0x00001795 +ble_char_isreal: EQU 0x0000178B +ble_char_isnull: EQU 0x00001794 +ble_lfn_next: EQU 0x000017B4 +ble_charloop: EQU 0x0000176C +ble_mul13done: EQU 0x00001767 +ble_mul13: EQU 0x0000175C +ble_seqset: EQU 0x0000173E +ble_notlast: EQU 0x0000173D +ble_lfn_done: EQU 0x000017BB +ble_lfn_loop: EQU 0x00001706 +ble_divdone: EQU 0x000016EF +ble_divloop: EQU 0x000016E4 +ble_multi: EQU 0x000016DB +build_lfn_entries: EQU 0x000016B6 +ble_entry_ptr: EQU 0x000016B4 +ble_charpos: EQU 0x000016B2 +ble_j: EQU 0x000016B1 +ble_checksum: EQU 0x000016B0 +ble_lfn_count: EQU 0x000016AF +ble_name_ptr: EQU 0x000016AD +lfn_char_dest: EQU 0x000016A0 +wne_slot_count: EQU 0x0000169F +wne_entries_buf: EQU 0x0000157F wne_max_lfn: EQU 0x00000008 -btd_done: EQU 0x0000153B -btd_loop: EQU 0x00001532 -gus_have_candidate: EQU 0x00001511 -bin_to_2dig: EQU 0x00001530 -gus_try_1digit: EQU 0x000014F9 -gus_try: EQU 0x000014D4 -gus_ok: EQU 0x0000152E -gus_needs_suffix: EQU 0x000014CF -generate_unique_shortname: EQU 0x000014B0 -gus_suffix: EQU 0x000014AF -fn83_ext_full: EQU 0x000014A1 -fn83_ext_bad: EQU 0x000014A8 -fn83_ext_loop: EQU 0x00001483 -fn83_ext_go: EQU 0x0000146C -fn83_base_full: EQU 0x00001451 -fn83_base_bad: EQU 0x00001458 -fn83_ext: EQU 0x0000145F -fn83_base_loop: EQU 0x00001432 -fn83_have_baselen: EQU 0x00001427 -fn83_scan_next: EQU 0x000013E5 -fn83_scandone: EQU 0x000013E9 -fn83_scan: EQU 0x000013D9 -filter_name_to_83: EQU 0x000013CE -fn83_charbuf: EQU 0x000013CD -sn_base_len: EQU 0x000013CB -sn_dot_pos: EQU 0x000013C9 -sn_name_len: EQU 0x000013C7 -sn_needs_lfn: EQU 0x000013C6 -sn_ext: EQU 0x000013C3 -sn_base: EQU 0x000013BB -cc_symyes: EQU 0x000013B6 -cc_no: EQU 0x000013B9 -cc_symloop: EQU 0x000013AC -cc_sym: EQU 0x000013A8 -cc_yes: EQU 0x000013B7 -cc_dig: EQU 0x000013A0 -cc_up: EQU 0x00001398 -classify_char: EQU 0x0000138E -cc_symbols: EQU 0x0000137D -lfnck_loop: EQU 0x00001377 -lfn_checksum: EQU 0x00001374 -stamp_dir_entry_datetime: EQU 0x0000115D -rtc_to_fat_datetime: EQU 0x000010E1 -rtc_cached: EQU 0x000010E0 -fat_time: EQU 0x000010DE -fat_date: EQU 0x000010DC -bcd_to_bin: EQU 0x000010C7 -rtc_field_to_bin: EQU 0x000010BB -rtc_dm: EQU 0x000010BA -rtc_year: EQU 0x00001049 -rtc_month: EQU 0x00001048 -rtc_date: EQU 0x00001047 -rtc_hour: EQU 0x00001046 -rtc_min: EQU 0x00001045 -rtc_sec: EQU 0x00001044 -sv512_off: EQU 0x00001C20 -sv512_page: EQU 0x00001C1F -strm_write_have2: EQU 0x0000102F -stream_write_sector_from: EQU 0x00000FCB -read_rtc_datetime: EQU 0x0000104A -strm_write_have: EQU 0x00000FB6 -stream_write_sector: EQU 0x00000F48 -stream_read_sector_to: EQU 0x00000D2D -stream_data_save: EQU 0x00000D48 -strm_read_eoc: EQU 0x00000D2A -stream_read_sector: EQU 0x00000CF7 -strm_adv_same: EQU 0x00000CF1 -strm_adv_cross: EQU 0x00000CCA -strm_adv_test_z: EQU 0x00000CC2 -strm_advance: EQU 0x00000CB5 -strm_calc_sector: EQU 0x00000C8D -strm_open_nz: EQU 0x00000C79 -stream_open: EQU 0x00000C5F -strm_sector_lo: EQU 0x00000C5D -strm_sector_hi: EQU 0x00000C5B -stream_eoc: EQU 0x00000C5A -stream_sec_in_cluster: EQU 0x00000C59 -stream_cluster_lo: EQU 0x00000C57 -stream_cluster_hi: EQU 0x00000C55 -wde_next: EQU 0x00000C24 -wde_slot: EQU 0x00000C35 -wde_entloop: EQU 0x00000C04 -wde_fail: EQU 0x00000C52 -wde_sec_loop: EQU 0x00000BEF -wde_cluster: EQU 0x00000BDA -write_dir_entry: EQU 0x00000BDA -wde_slotptr: EQU 0x00000BD8 -wde_sector_lo: EQU 0x00000BD6 -wde_sector_hi: EQU 0x00000BD4 -wde_remaining: EQU 0x00000BD3 -dir_entry_buf: EQU 0x00000BB3 -zfc_loop: EQU 0x00000BA1 -zfc_fail: EQU 0x00000BB0 -zero_fill_cluster: EQU 0x00000B7B -zfc_remaining: EQU 0x00000B7A -ffc_nowrap: EQU 0x00000B6B -ffc_nsv2: EQU 0x00000B45 -ffc_nsv: EQU 0x00000B3F -ffc_entnext: EQU 0x00000B4F -ffc_entloop: EQU 0x00000B08 -ffc_fail: EQU 0x00000B77 -ffc_from2: EQU 0x00000AC1 -ffc_scan: EQU 0x00000AD0 -ffc_use_hint: EQU 0x00000AA7 -find_free_cluster: EQU 0x00000A8C -next_search_valid: EQU 0x00000A87 -ffc_sectors_read: EQU 0x00000A85 -ffc_count: EQU 0x00000A84 -ffc_ptr: EQU 0x00000A82 -free_cluster_lo: EQU 0x00000A80 -free_cluster_hi: EQU 0x00000A7E -next_search_hi: EQU 0x00000A88 -next_search_lo: EQU 0x00000A8A -wfi_skip_count: EQU 0x00000A62 -write_fsinfo_hint: EQU 0x00000A27 -read_fsinfo_hint: EQU 0x000009C8 -fsinfo_valid: EQU 0x000009C7 -fsinfo_hint_lo: EQU 0x000009C5 -fsinfo_hint_hi: EQU 0x000009C3 -wfe_loop: EQU 0x0000098C -wfe_fail: EQU 0x000009BE -write_fat_entry: EQU 0x00000940 -wfe_sector_lo: EQU 0x0000093E -wfe_sector_hi: EQU 0x0000093C -wfe_remaining: EQU 0x0000093B -new_val_lo: EQU 0x00000939 -new_val_hi: EQU 0x00000937 -gnc_have_next: EQU 0x00000926 -gnc_end: EQU 0x00000920 -nxt_hi: EQU 0x000008DC -nxt_lo: EQU 0x000008DA -ctf_bad: EQU 0x000008D6 -ctf_shr: EQU 0x00000884 -cluster_to_fatpos: EQU 0x00000876 -gnc_inoff: EQU 0x00000874 -gnc_fatsec_lo: EQU 0x00000872 -gnc_fatsec_hi: EQU 0x00000870 -gnc_lo: EQU 0x0000086E -gnc_hi: EQU 0x0000086C -c2s_loop: EQU 0x00000832 -c2s_done: EQU 0x0000084B -cur_cluster_hi: EQU 0x00000868 -cur_cluster_lo: EQU 0x0000086A -c2s_prod_hi: EQU 0x00000805 -c2s_prod_lo: EQU 0x00000803 -c2s_hi: EQU 0x00000801 -c2s_lo: EQU 0x000007FF -chain_ended: EQU 0x000008D9 -get_next_cluster: EQU 0x000008DE -fza_next: EQU 0x000007E9 -fza_inc_loop: EQU 0x000007E1 -fza_done: EQU 0x000007FC -fza_stop: EQU 0x000007FA -fza_loop: EQU 0x000007CB -fza_sector_lo: EQU 0x00000807 -fza_sector_hi: EQU 0x00000809 -cluster_to_sector: EQU 0x0000080B -fza_cluster: EQU 0x000007B8 -find_zifi_all: EQU 0x000007B8 -fza_remaining: EQU 0x000007B7 -fz_found: EQU 0x00000782 -fz_cmpfail: EQU 0x00000775 -fz_cmp: EQU 0x00000769 -fz_skip: EQU 0x00000777 -fz_end: EQU 0x0000077F -fz_loop: EQU 0x0000075A -find_zifi: EQU 0x00000755 -target_name: EQU 0x0000074A -zifi_size: EQU 0x00000748 -fbn_acc_nocs: EQU 0x00001B21 -zifi_size_hi: EQU 0x00000746 -zifi_cluster: EQU 0x00000744 -zifi_cluster_hi: EQU 0x00000742 -root_dir_hi: EQU 0x00000740 -root_dir_lo: EQU 0x0000073E -mul2_loop: EQU 0x000006D2 -mul2_done: EQU 0x000006EB -prod2_hi: EQU 0x0000073C -prod2_lo: EQU 0x0000073A -rc2_hi: EQU 0x00000738 -rc2_lo: EQU 0x00000736 -data_start_hi: EQU 0x00000734 -data_start_lo: EQU 0x00000732 -mul1_loop: EQU 0x0000067B -mul1_done: EQU 0x00000694 -prod_hi: EQU 0x00000730 -prod_lo: EQU 0x0000072E -root_cluster_lo: EQU 0x0000072A -root_cluster_hi: EQU 0x0000072C -fatsz32_lo: EQU 0x00000726 -fatsz32_hi: EQU 0x00000728 -sectors_per_cluster: EQU 0x00000725 -num_fats: EQU 0x00000724 -fsinfo_sector: EQU 0x000009C1 -reserved_sectors: EQU 0x00000722 -total_sectors_hi: EQU 0x0000071C -total_sectors_lo: EQU 0x0000071A -parse_bpb: EQU 0x0000062F -vf_ok: EQU 0x0000062A -vf_bad: EQU 0x0000062C -verify_fat32: EQU 0x000005EA -dp_fallback: EQU 0x000005CE -dp_found: EQU 0x0000059F -dp_entry: EQU 0x000005E8 -dp_loop: EQU 0x00000585 -dp_none: EQU 0x0000059D -detect_partition: EQU 0x0000055A -sec0_copy: EQU 0x0000035A -add_addtop: EQU 0x0000034A -addtop_lo: EQU 0x00000348 -addtop_hi: EQU 0x00000346 -cmdx: EQU 0x00000313 -do_cmd55: EQU 0x000002ED -cmdo: EQU 0x0000030D -send_frame: EQU 0x000002F5 -cmd16: EQU 0x000002C5 -cmd08: EQU 0x000002BF -cmd00: EQU 0x000002B9 -sdwrite_lba_from: EQU 0x00000234 -sdwrite_lba: EQU 0x00000231 -sdwrite_multi_stop: EQU 0x0000021C -sdwrite_multi_block_from: EQU 0x000001F7 -sdwrite_multi_block: EQU 0x000001F4 -csl: EQU 0x00000258 -wait: EQU 0x0000026D -sdwrite_multi_start: EQU 0x000001AF -wtdo: EQU 0x00000282 -sdread_fail: EQU 0x000001A6 -resp: EQU 0x00000298 -sdread_common: EQU 0x0000018D -sdread_lba_oob: EQU 0x000001AC -send_cmd_lba: EQU 0x000001C7 -cmdlba_op: EQU 0x000001C6 -sdread_lba_to: EQU 0x00000182 -sdread_lba: EQU 0x0000017F -dma_send_ext: EQU 0x0000015F -dma_wait: EQU 0x00000179 -dma_recv_ext: EQU 0x00000143 -dma_set_addr_ext: EQU 0x0000012D -sdbuf: EQU 0x00001174 -set_dma_sdbuf: EQU 0x00000121 -dma_xfer_page: EQU 0x00000120 -dma_xfer_off: EQU 0x0000011E +btd_done: EQU 0x00001573 +btd_loop: EQU 0x0000156A +gus_have_candidate: EQU 0x00001549 +bin_to_2dig: EQU 0x00001568 +gus_try_1digit: EQU 0x00001531 +gus_try: EQU 0x0000150C +gus_ok: EQU 0x00001566 +gus_needs_suffix: EQU 0x00001507 +generate_unique_shortname: EQU 0x000014E8 +gus_suffix: EQU 0x000014E7 +fn83_ext_full: EQU 0x000014D9 +fn83_ext_bad: EQU 0x000014E0 +fn83_ext_loop: EQU 0x000014BB +fn83_ext_go: EQU 0x000014A4 +fn83_base_full: EQU 0x00001489 +fn83_base_bad: EQU 0x00001490 +fn83_ext: EQU 0x00001497 +fn83_base_loop: EQU 0x0000146A +fn83_have_baselen: EQU 0x0000145F +fn83_scan_next: EQU 0x0000141D +fn83_scandone: EQU 0x00001421 +fn83_scan: EQU 0x00001411 +filter_name_to_83: EQU 0x00001406 +fn83_charbuf: EQU 0x00001405 +sn_base_len: EQU 0x00001403 +sn_dot_pos: EQU 0x00001401 +sn_name_len: EQU 0x000013FF +sn_needs_lfn: EQU 0x000013FE +sn_ext: EQU 0x000013FB +sn_base: EQU 0x000013F3 +cc_symyes: EQU 0x000013EE +cc_no: EQU 0x000013F1 +cc_symloop: EQU 0x000013E4 +cc_sym: EQU 0x000013E0 +cc_yes: EQU 0x000013EF +cc_dig: EQU 0x000013D8 +cc_up: EQU 0x000013D0 +classify_char: EQU 0x000013C6 +cc_symbols: EQU 0x000013B5 +lfnck_loop: EQU 0x000013AF +lfn_checksum: EQU 0x000013AC +stamp_dir_entry_datetime: EQU 0x00001195 +rtc_to_fat_datetime: EQU 0x00001119 +rtc_cached: EQU 0x00001118 +fat_time: EQU 0x00001116 +fat_date: EQU 0x00001114 +bcd_to_bin: EQU 0x000010FF +rtc_field_to_bin: EQU 0x000010F3 +rtc_dm: EQU 0x000010F2 +rtc_year: EQU 0x00001081 +rtc_month: EQU 0x00001080 +rtc_date: EQU 0x0000107F +rtc_hour: EQU 0x0000107E +rtc_min: EQU 0x0000107D +rtc_sec: EQU 0x0000107C +sv512_off: EQU 0x00001C5D +sv512_page: EQU 0x00001C5C +strm_write_have2: EQU 0x00001067 +stream_write_sector_from: EQU 0x00001000 +read_rtc_datetime: EQU 0x00001082 +strm_write_have: EQU 0x00000FEB +stream_write_sector: EQU 0x00000F7A +stream_read_sector_to: EQU 0x00000D5F +stream_data_save: EQU 0x00000D7A +strm_read_eoc: EQU 0x00000D5C +stream_read_sector: EQU 0x00000D29 +strm_adv_same: EQU 0x00000D23 +strm_adv_cross: EQU 0x00000CFC +strm_adv_test_z: EQU 0x00000CF4 +strm_advance: EQU 0x00000CE7 +strm_calc_sector: EQU 0x00000CBF +strm_open_nz: EQU 0x00000CAB +stream_open: EQU 0x00000C91 +strm_sector_lo: EQU 0x00000C8F +strm_sector_hi: EQU 0x00000C8D +stream_eoc: EQU 0x00000C8C +stream_sec_in_cluster: EQU 0x00000C8B +stream_cluster_lo: EQU 0x00000C89 +stream_cluster_hi: EQU 0x00000C87 +wde_next: EQU 0x00000C56 +wde_slot: EQU 0x00000C67 +wde_entloop: EQU 0x00000C36 +wde_fail: EQU 0x00000C84 +wde_sec_loop: EQU 0x00000C21 +wde_cluster: EQU 0x00000C0C +write_dir_entry: EQU 0x00000C0C +wde_slotptr: EQU 0x00000C0A +wde_sector_lo: EQU 0x00000C08 +wde_sector_hi: EQU 0x00000C06 +wde_remaining: EQU 0x00000C05 +dir_entry_buf: EQU 0x00000BE5 +zfc_loop: EQU 0x00000BD3 +zfc_fail: EQU 0x00000BE2 +zero_fill_cluster: EQU 0x00000BAD +zfc_remaining: EQU 0x00000BAC +ffc_nowrap: EQU 0x00000B9D +ffc_nsv2: EQU 0x00000B77 +ffc_nsv: EQU 0x00000B71 +ffc_entnext: EQU 0x00000B81 +ffc_entloop: EQU 0x00000B3A +ffc_fail: EQU 0x00000BA9 +ffc_from2: EQU 0x00000AF3 +ffc_scan: EQU 0x00000B02 +ffc_use_hint: EQU 0x00000AD9 +find_free_cluster: EQU 0x00000ABE +next_search_valid: EQU 0x00000AB9 +ffc_sectors_read: EQU 0x00000AB7 +ffc_count: EQU 0x00000AB6 +ffc_ptr: EQU 0x00000AB4 +free_cluster_lo: EQU 0x00000AB2 +free_cluster_hi: EQU 0x00000AB0 +next_search_hi: EQU 0x00000ABA +next_search_lo: EQU 0x00000ABC +wfi_skip_count: EQU 0x00000A94 +write_fsinfo_hint: EQU 0x00000A59 +read_fsinfo_hint: EQU 0x000009FA +fsinfo_valid: EQU 0x000009F9 +fsinfo_hint_lo: EQU 0x000009F7 +fsinfo_hint_hi: EQU 0x000009F5 +wfe_loop: EQU 0x000009BE +wfe_fail: EQU 0x000009F0 +write_fat_entry: EQU 0x00000972 +wfe_sector_lo: EQU 0x00000970 +wfe_sector_hi: EQU 0x0000096E +wfe_remaining: EQU 0x0000096D +new_val_lo: EQU 0x0000096B +new_val_hi: EQU 0x00000969 +gnc_valid: EQU 0x00000958 +gnc_stop: EQU 0x00000942 +gnc_end: EQU 0x0000093C +gnc_have_next: EQU 0x00000946 +gnc_bad: EQU 0x00000940 +nxt_hi: EQU 0x000008E8 +nxt_lo: EQU 0x000008E6 +ctf_bad: EQU 0x000008E2 +ctf_shr: EQU 0x00000890 +cluster_to_fatpos: EQU 0x00000882 +gnc_inoff: EQU 0x00000880 +gnc_fatsec_lo: EQU 0x0000087E +gnc_fatsec_hi: EQU 0x0000087C +gnc_lo: EQU 0x0000087A +gnc_hi: EQU 0x00000878 +c2s_loop: EQU 0x0000083E +c2s_done: EQU 0x00000857 +cur_cluster_hi: EQU 0x00000874 +cur_cluster_lo: EQU 0x00000876 +c2s_prod_hi: EQU 0x00000811 +c2s_prod_lo: EQU 0x0000080F +c2s_hi: EQU 0x0000080D +c2s_lo: EQU 0x0000080B +chain_ended: EQU 0x000008E5 +get_next_cluster: EQU 0x000008EA +fza_next: EQU 0x000007F5 +fza_inc_loop: EQU 0x000007ED +fza_done: EQU 0x00000808 +fza_stop: EQU 0x00000806 +fza_loop: EQU 0x000007D7 +fza_sector_lo: EQU 0x00000813 +fza_sector_hi: EQU 0x00000815 +cluster_to_sector: EQU 0x00000817 +fza_cluster: EQU 0x000007C4 +find_zifi_all: EQU 0x000007C4 +fza_remaining: EQU 0x000007C3 +fz_found: EQU 0x0000078E +fz_cmpfail: EQU 0x00000781 +fz_cmp: EQU 0x00000775 +fz_skip: EQU 0x00000783 +fz_end: EQU 0x0000078B +fz_loop: EQU 0x00000766 +find_zifi: EQU 0x00000761 +target_name: EQU 0x00000756 +zifi_size: EQU 0x00000754 +fbn_acc_nocs: EQU 0x00001B5E +zifi_size_hi: EQU 0x00000752 +zifi_cluster: EQU 0x00000750 +zifi_cluster_hi: EQU 0x0000074E +root_dir_hi: EQU 0x0000074C +root_dir_lo: EQU 0x0000074A +mul2_loop: EQU 0x000006DE +mul2_done: EQU 0x000006F7 +prod2_hi: EQU 0x00000748 +prod2_lo: EQU 0x00000746 +rc2_hi: EQU 0x00000744 +rc2_lo: EQU 0x00000742 +data_start_hi: EQU 0x00000740 +data_start_lo: EQU 0x0000073E +mul1_loop: EQU 0x00000687 +mul1_done: EQU 0x000006A0 +prod_hi: EQU 0x0000073C +prod_lo: EQU 0x0000073A +root_cluster_lo: EQU 0x00000736 +root_cluster_hi: EQU 0x00000738 +fatsz32_lo: EQU 0x00000732 +fatsz32_hi: EQU 0x00000734 +sectors_per_cluster: EQU 0x00000731 +num_fats: EQU 0x00000730 +fsinfo_sector: EQU 0x000009F3 +reserved_sectors: EQU 0x0000072E +total_sectors_hi: EQU 0x00000728 +total_sectors_lo: EQU 0x00000726 +parse_bpb: EQU 0x0000063B +vf_ok: EQU 0x00000636 +vf_bad: EQU 0x00000638 +verify_fat32: EQU 0x000005F6 +dp_fallback: EQU 0x000005DA +dp_found: EQU 0x000005AB +dp_entry: EQU 0x000005F4 +dp_loop: EQU 0x00000591 +dp_none: EQU 0x000005A9 +detect_partition: EQU 0x00000566 +sec0_copy: EQU 0x00000366 +add_addtop: EQU 0x00000356 +addtop_lo: EQU 0x00000354 +addtop_hi: EQU 0x00000352 +cmdx: EQU 0x0000031F +do_cmd55: EQU 0x000002F9 +cmdo: EQU 0x00000319 +send_frame: EQU 0x00000301 +cmd16: EQU 0x000002D1 +cmd08: EQU 0x000002CB +cmd00: EQU 0x000002C5 +sdwrite_lba_from: EQU 0x0000023E +sdwrite_lba: EQU 0x0000023B +sdwrite_multi_stop: EQU 0x00000226 +sdwrite_multi_block_from: EQU 0x00000201 +sdwrite_multi_block: EQU 0x000001FE +csl: EQU 0x00000262 +wait: EQU 0x00000277 +sdwrite_multi_start: EQU 0x000001B9 +wtdo: EQU 0x0000028E +sdread_fail: EQU 0x000001B0 +resp: EQU 0x000002A4 +sdread_common: EQU 0x00000197 +sdread_lba_oob: EQU 0x000001B6 +send_cmd_lba: EQU 0x000001D1 +cmdlba_op: EQU 0x000001D0 +sdread_lba_to: EQU 0x0000018C +sdread_lba: EQU 0x00000189 +dma_send_ext: EQU 0x00000169 +dma_wait: EQU 0x00000183 +dma_recv_ext: EQU 0x0000014D +dma_set_addr_ext: EQU 0x00000137 +sdbuf: EQU 0x000011AC +set_dma_sdbuf: EQU 0x0000012B +dma_xfer_page: EQU 0x0000012A +dma_xfer_off: EQU 0x00000128 dma_ctr: EQU 0x000027AF dma_num: EQU 0x000028AF dma_len: EQU 0x000026AF @@ -408,27 +411,27 @@ dma_daddrx: EQU 0x00001FAF dma_daddrh: EQU 0x00001EAF dma_daddrl: EQU 0x00001DAF own_page: EQU 0x0000000F -cca_shift: EQU 0x000000F4 -compute_card_arg: EQU 0x000000E2 -card_arg: EQU 0x000000DE -sd_lba_max_hi: EQU 0x00000720 -sd_lba_max_lo: EQU 0x0000071E -lba_guard_check: EQU 0x000000BF -lba_arg: EQU 0x000000BB -set_lba32: EQU 0x000000AA -sd_block_addressed: EQU 0x000000DD -sd_v2_ocr_rest: EQU 0x00000099 -do_cmd58: EQU 0x000002DD -sdinit_ok: EQU 0x0000009F -do_cmd16: EQU 0x000002D7 -do_cmd1: EQU 0x000002E5 -sd_fbs: EQU 0x0000005D -do_acmd41: EQU 0x00000326 -sd_v2: EQU 0x00000067 -do_cmd8: EQU 0x000002D1 -do_cmd0: EQU 0x000002CB -sdinit_fail: EQU 0x000000A4 -cycl: EQU 0x000002AC -csh: EQU 0x00000245 +cca_shift: EQU 0x000000FE +compute_card_arg: EQU 0x000000EC +card_arg: EQU 0x000000E8 +sd_lba_max_hi: EQU 0x0000072C +sd_lba_max_lo: EQU 0x0000072A +lba_guard_check: EQU 0x000000C9 +lba_arg: EQU 0x000000C5 +set_lba32: EQU 0x000000B4 +sd_v2_ocr_rest: EQU 0x0000009D +do_cmd58: EQU 0x000002E9 +sdinit_ok: EQU 0x000000A9 +do_cmd16: EQU 0x000002E3 +do_cmd1: EQU 0x000002F1 +sd_fbs: EQU 0x00000061 +do_acmd41: EQU 0x00000332 +sd_v2: EQU 0x0000006B +do_cmd8: EQU 0x000002DD +do_cmd0: EQU 0x000002D7 +sdinit_fail: EQU 0x000000AE +cycl: EQU 0x000002B8 +csh: EQU 0x0000024F +sd_block_addressed: EQU 0x000000E7 sdinit: EQU 0x00000000 -fbn_cluster_lo: EQU 0x00001A53 +fbn_cluster_lo: EQU 0x00001A90 diff --git a/_sd/tests/README.md b/_sd/tests/README.md new file mode 100644 index 0000000..653494e --- /dev/null +++ b/_sd/tests/README.md @@ -0,0 +1,49 @@ +# Проверки драйвера + +Тесты исполняют собранный машинный код в Python-пакете `z80`. +Данные диска синтетические; физические SD-карты и образы эмулятора +не открываются и не изменяются. + +Из `_sd` после сборки тела, обновления `body_syms.inc` и упаковки +PackBits по инструкции в основном README драйвера: + +```text +sjasmplus.exe zc_sd_driver_body.asm --sym=body_syms_new.txt +# При изменении тела обновить body_syms.inc и .pb до следующей команды. +sjasmplus.exe zc_sd_driver.asm --sym=resident_syms_new.txt +python tests/test_driver.py +python tests/test_resident.py +``` + +Нужны Python и пакет `z80`. Бинарники, символьные таблицы и PackBits +должны относиться к одной сборке. + +Покрытие (17 групп): + +- все 128 смещений FAT32-записей, зеркалирование FAT и сохранение + старших зарезервированных битов; +- полное сравнение EOC, маскирование верхних четырёх битов, + свободные/зарезервированные/повреждённые ссылки и ошибки чтения; +- чтение и запись фрагментированных цепочек, размеры кластера + 1–128 секторов, несколько вызовов LOAD512/SAVE512, переходы страниц + памяти с ненулевым начальным смещением; +- выделение кластеров, расширение файла и каталога, отказ от продления + повреждённой цепочки; +- 32-битная арифметика LBA, границы тома, SDSC/SDHC-адресация; +- MBR с разделом в каждом из четырёх слотов, BPB без MBR, + геометрия FAT32 и базовые проверки BPB; +- MKDIR/MKFILE, короткие и длинные имена, коллизии коротких имён, + поиск без учёта регистра и отказ от создания дубликата; +- FSInfo, упаковка даты/времени RTC в BCD и двоичном режиме; +- значения регистров DMA и последовательности команд инициализации + SDHC, SDv2 SDSC, legacy SD и MMC, включая повторную инициализацию + и исчерпание попыток при постоянно занятой карте; +- распаковка тела Z80-кодом, FENTRY, переименование, удаление, + освобождение цепочки и сохранение IX в обёртке MKFILE. + +Границы проверки: секторный SPI/DMA-обмен подменяется блоковым +устройством в памяти. В отдельных тестах инициализации, DMA-регистров +и RTC используются модели портов. Электрические характеристики, +аппаратный тайминг, прерывания ZiFi и поведение при отключении питания +этими тестами не проверяются. Тесты не доказывают транзакционность +операций FAT или защиту от всех вариантов повреждённого тома. diff --git a/_sd/tests/test_driver.py b/_sd/tests/test_driver.py new file mode 100644 index 0000000..e12a3be --- /dev/null +++ b/_sd/tests/test_driver.py @@ -0,0 +1,362 @@ +"""Broader machine-code regression tests; run after assembling the body. + +Run from _sd: python tests/test_driver.py +Uses synthetic FAT32 sectors only; no physical card or emulator image is changed. +""" +import random +from test_fat_offsets import Driver, SYMS, test_offsets_and_links, test_write_and_allocation, test_fragmented_load + + +def u32(d, prefix): + return d.get(prefix+'_lo') | d.get(prefix+'_hi') << 16 + + +def put32(d, prefix, value): + d.put(prefix+'_lo', value & 65535) + d.put(prefix+'_hi', value >> 16) + + +def test_eoc_boundaries(): + valid = [2, 63, 64, 127, 128, 65535, 65536, + 0x0ff7ffff, 0x0ff80000, 0x0fffffef] + for top in (0, 0xa0000000, 0xf0000000): + for value in valid + list(range(0x0ffffff8, 0x10000000)) + [0, 1] + list(range(0x0ffffff0, 0x0ffffff8)): + d = Driver() + d.fat(100, value | top) + d.cluster(100) + d.call('get_next_cluster') + expected = 0 if value in valid else (1 if value >= 0x0ffffff8 else 2) + assert d.get('chain_ended', 1) == expected, hex(value | top) + assert u32(d, 'cur_cluster') == (value if expected == 0 else 100) + + +def test_errors_do_not_extend_chain(): + for broken in (0, 1, 0x0ffffff0, 0x0ffffff7, 'io'): + d = Driver() + d.fat(100, 101 if broken == 'io' else broken) + d.cluster(100) + d.call('stream_open') + d.put('stream_sec_in_cluster', 1, 1) + if broken == 'io': + d.fail_reads.add(49) + d.call('strm_advance') + assert d.get('stream_eoc', 1) == 2 + before = len(d.io) + for routine in ('stream_write_sector', 'stream_write_sector_from', 'stream_read_sector_to'): + assert d.call(routine) != 0 + assert len(d.io) == before, 'I/O after broken chain' + d = Driver() + d.cluster(8192) # first cluster outside a 64-sector FAT + d.call('get_next_cluster') + assert d.get('chain_ended', 1) == 2 and not d.io + for routine in ('core_load512', 'core_save512'): + d = Driver() + d.cluster(100) + d.call('stream_open') + d.fail_reads.add(1213) + d.fail_writes.add(1213) + d.m.c, d.m.hl, d.m.b = 32, 0x4000, 1 + assert d.call(routine) == 15 + assert (d.m.c, d.m.hl) == (32, 0x4000) + assert d.get('stream_sec_in_cluster', 1) == 0 + + +def test_address_arithmetic(): + rng = random.Random(589) + for _ in range(300): + d = Driver() + cluster = rng.randrange(2, 0x400000) + spc = rng.choice((1, 2, 4, 8, 16, 32, 64, 128)) + data, base = rng.randrange(32, 200000), rng.randrange(0, 200000) + d.cluster(cluster) + d.put('sectors_per_cluster', spc, 1) + put32(d, 'data_start', data) + put32(d, 'addtop', base) + d.call('cluster_to_sector') + assert u32(d, 'fza_sector') == base+data+(cluster-2)*spc + maximum = rng.randrange(1, 0xffffffff) + put32(d, 'sd_lba_max', maximum) + for lba in (0, maximum-1, maximum, 0xffffffff): + d.m.hl, d.m.de = lba >> 16, lba & 65535 + d.call('set_lba32') + d.call('lba_guard_check') + assert bool(d.m.f & 1) == (lba < maximum) + for block, lba in ((0, rng.randrange(0x400000)), (1, rng.randrange(0xffffffff))): + d.put('sd_block_addressed', block, 1) + d.m.hl, d.m.de = lba >> 16, lba & 65535 + d.call('set_lba32') + d.call('compute_card_arg') + p = SYMS['card_arg'] + assert int.from_bytes(d.m.memory[p:p+4], 'big') == lba*(1 if block else 512) + + +def boot_sector(spc=64): + b = bytearray(512) + for offset, size, value in ((11, 2, 512), (13, 1, spc), (14, 2, 32), + (16, 1, 2), (32, 4, 10000000), (36, 4, 59430), + (44, 4, 12345), (48, 2, 1)): + b[offset:offset+size] = value.to_bytes(size, 'little') + b[510:512] = b'\x55\xaa' + return b + + +def test_bpb_and_partition(): + for spc in (1, 2, 4, 8, 16, 32, 64, 128): + d = Driver() + d.m.set_memory_block(SYMS['sdbuf'], boot_sector(spc)) + assert d.call('verify_fat32') == 0 + d.call('parse_bpb') + assert u32(d, 'data_start') == 32+2*59430 + assert u32(d, 'root_dir') == 32+2*59430+(12345-2)*spc + assert u32(d, 'sd_lba_max') == 17+10000000 + for offset, value in ((510, 0), (511, 0), (11, 1), (22, 1), (17, 1), (13, 0), (13, 3)): + d = Driver() + b = boot_sector() + b[offset] = value + d.m.set_memory_block(SYMS['sdbuf'], b) + assert d.call('verify_fat32') != 0, offset + for slot in range(4): + d = Driver() + mbr = bytearray(512) + mbr[510:512] = b'\x55\xaa' + mbr[446+slot*16+4] = 0x0c + mbr[446+slot*16+8:446+slot*16+12] = (70000).to_bytes(4, 'little') + d.disk[70000] = boot_sector() + d.m.set_memory_block(SYMS['sdbuf'], mbr) + assert d.call('detect_partition') == 0 + assert u32(d, 'addtop') == 70000 + d = Driver() + original = boot_sector() + d.m.set_memory_block(SYMS['sdbuf'], original) + assert d.call('detect_partition') == 0 + assert u32(d, 'addtop') == 0 + + +def test_stream_read_write_all_cluster_sizes(): + rng = random.Random(731) + for spc in (1, 2, 4, 8, 16, 32, 64, 128): + d = Driver() + d.put('sectors_per_cluster', spc, 1) + chain = [319, 500, 320, 1023] + for i, n in enumerate(chain): + d.fat(n, chain[i+1] if i+1 < len(chain) else 0x0ffffff8) + for s in range(spc): + d.disk[1017+(n-2)*spc+s] = bytearray(512) + payload = rng.randbytes(len(chain)*spc*512) + start = 32*16384+4 # nonzero, even offset; exercise page overflow remainder + d.ram[start:start+len(payload)] = payload + d.cluster(chain[0]) + d.call('stream_open') + d.m.c, d.m.hl = 32, 0x4004 + remaining = len(payload)//512 + while remaining: + count = min(remaining, 127) + d.m.b = count + assert d.call('core_save512') == 0 + remaining -= count + on_disk = b''.join(d.disk[1017+(n-2)*spc+s] for n in chain for s in range(spc)) + assert on_disk == payload, spc + d.ram[start:start+len(payload)] = bytes(len(payload)) + d.cluster(chain[0]) + d.call('stream_open') + d.m.c, d.m.hl = 32, 0x4004 + remaining = len(payload)//512 + while remaining: + count = min(remaining, 113) + d.m.b = count + assert d.call('core_load512') == 0 + remaining -= count + assert d.ram[start:start+len(payload)] == payload, spc + d.cluster(chain[0]) + d.call('stream_open') + for s in range(spc): + assert d.call('stream_read_sector') == 0 + p = SYMS['sdbuf'] + assert bytes(d.m.memory[p:p+512]) == payload[s*512:(s+1)*512] + + +def test_grow_fragmented_file(): + d = Driver() + for n in range(256, 512): + d.fat(n, 0x0fffffff) + free = [320, 356, 383, 400] + for n in free: + d.fat(n, 0) + d.put('next_search_valid', 1, 1) + d.put('next_search_lo', 320) + d.cluster(319) + d.call('stream_open') + payload = random.Random(52).randbytes(10*512) + d.ram[32*16384:32*16384+len(payload)] = payload + d.m.c, d.m.hl, d.m.b = 32, 0x4000, 10 + assert d.call('core_save512') == 0 + chain = [319]+free + actual = b''.join(d.disk[1017+(n-2)*2+s] for n in chain for s in range(2)) + assert actual == payload + for index, n in enumerate(chain): + sector, offset = divmod(n*4, 512) + value = int.from_bytes(d.disk[49+sector][offset:offset+4], 'little') & 0x0fffffff + assert value == (chain[index+1] if index+1 < len(chain) else 0x0fffffff) + assert d.disk[49+sector] == d.disk[113+sector] + + +def test_directories_and_long_names(): + d = Driver() + d.put('rtc_cached', 1, 1) + d.put('fat_date', 0x5d47) + d.put('fat_time', 0x6d5d) + for sector in range(49, 113): + d.disk[sector] = bytearray(512) + d.fat(2, 0x0fffffff) + d.put('root_cluster_lo', 2) + d.call('core_setroot') + d.disk[1017] = bytearray(512) + d.disk[1018] = bytearray(512) + d.put('next_search_valid', 1, 1) + d.put('next_search_lo', 320) + def arg(data): + d.m.set_memory_block(0x9000, data + b'\0') + d.m.hl = 0x9000 + arg(b'Long directory name') + assert d.call('core_mkdir') == 0 + arg(b'long DIRECTORY name') + assert d.call('core_fentry') == 1 + directory = u32(d, 'fbn_cluster') + assert directory == 320 + first = d.disk[1017+(directory-2)*2] + assert first[:11] == b'. ' + assert first[32:43] == b'.. ' + assert int.from_bytes(first[58:60], 'little') == 2 + d.call('core_setdir') + # Multiple colliding short names, LFN runs crossing sectors, directory growth. + names = [f'Long music filename number {i:02d}.pt3'.encode() for i in range(18)] + for name in names: + arg(b'\0'+(12345).to_bytes(4, 'little')+name) + assert d.call('core_mkfile') == 0, name + clusters = set() + for name in names: + arg(name.upper()) + assert d.call('core_fentry') == 1, name + assert u32(d, 'fbn_size') == 12345 + clusters.add(u32(d, 'fbn_cluster')) + assert len(clusters) == len(names) + before = {n: bytes(b) for n, b in d.disk.items()} + arg(b'\0'+(12345).to_bytes(4, 'little')+names[0]) + assert d.call('core_mkfile') == 3 + assert before == d.disk + + +def test_fsinfo(): + for free_count in (0xffffffff, 100000, 65536): + d = Driver() + addr = SYMS['write_fsinfo_hint'] + d.m.clear_breakpoint(addr) + del d.hooks[addr] + b = bytearray(512) + for offset, value in ((0, 0x41615252), (484, 0x61417272), + (488, free_count), (492, 65535)): + b[offset:offset+4] = value.to_bytes(4, 'little') + d.disk[18] = b + d.put('fsinfo_sector', 1) + d.call('read_fsinfo_hint') + assert d.get('fsinfo_valid', 1) == 1 + assert u32(d, 'fsinfo_hint') == 65535 + put32(d, 'next_search', 65536) + assert d.call('write_fsinfo_hint') == 0 + assert int.from_bytes(d.disk[18][492:496], 'little') == 65536 + assert int.from_bytes(d.disk[18][488:492], 'little') == (free_count if free_count == 0xffffffff else free_count-1) + + +def test_rtc_encoding(): + for binary in (False, True): + d = Driver() + rtc = {0: 59, 2: 42, 4: 23, 7: 7, 8: 10, 9: 26} + selected = [0] + def output(port, value): + if port == 0xdff7: + selected[0] = value + def input_(port): + if port != 0xbff7: + return 0 + if selected[0] == 11: + return 4 if binary else 0 + value = rtc.get(selected[0], 0) + return value if binary else (value//10)*16+value%10 + d.m.set_output_callback(output) + d.m.set_input_callback(input_) + d.call('rtc_to_fat_datetime') + assert d.get('fat_date') == ((2026-1980)<<9 | 10<<5 | 7) + assert d.get('fat_time') == (23<<11 | 42<<5 | 59//2) + + +def test_dma_registers(): + for routine, base, control in (('dma_recv_ext', 0x1daf, 0x42), ('dma_send_ext', 0x1aaf, 0xc2)): + for offset in (0x4000, 0x4004, 0x7ffe): + d = Driver() + ports = {} + d.m.set_output_callback(lambda port, value: ports.__setitem__(port, value)) + d.m.set_input_callback(lambda port: 0) + d.put('dma_xfer_off', offset) + d.put('dma_xfer_page', 32, 1) + d.call(routine) + assert ports == {base: offset & 255, base+256: (offset>>8)&63, + base+512: 32, 0x26af: 255, 0x28af: 0, 0x27af: control} + + +def test_sd_initialization(): + from collections import deque + d = Driver() + for kind in ('sdhc', 'sdsc', 'legacy', 'mmc', 'busy'): + commands, frame, response = [], [], deque() + selected = [False] + def output(port, value): + if port & 255 == 0x77: + selected[0] = value == 1 + if not selected[0]: + frame.clear() + response.clear() + if port & 255 != 0x57 or not selected[0]: + return + if not frame and not 0x40 <= value <= 0x7f: + return + frame.append(value) + if len(frame) != 6: + return + command = frame[0] & 63 + commands.append((command, int.from_bytes(bytes(frame[1:5]), 'big'))) + assert len(commands) < 16010, 'Initialization retry counter never expires' + answers = {0: [1], 8: [1, 0, 0, 1, 0xaa] if kind in ('sdhc', 'sdsc', 'busy') else [5], + 55: [1], 41: [1 if kind == 'busy' else (5 if kind == 'mmc' else 0)], 1: [0], 16: [0], + 58: [0, 0xc0 if kind == 'sdhc' else 0x80, 0xff, 0x80, 0]} + response.extend(answers[command]) + frame.clear() + def input_(port): + return response.popleft() if response else 255 + d.m.set_output_callback(output) + d.m.set_input_callback(input_) + result = d.call('sdinit') + if kind == 'busy': + assert result != 0 + continue + assert result == 0, kind + assert d.get('sd_block_addressed', 1) == int(kind == 'sdhc'), kind + ids = [c for c, _ in commands] + if kind in ('sdhc', 'sdsc'): + assert ids.index(41) < ids.index(58) + if kind != 'sdhc': + assert (16, 512) in commands, (kind, commands) + + +if __name__ == '__main__': + tests = [value for name, value in list(globals().items()) if name.startswith('test_')] + failures = [] + for test in tests: + try: + test() + print('PASS:', test.__name__, flush=True) + except Exception as exc: + import traceback + traceback.print_exc() + failures.append(test.__name__) + assert not failures, failures + print(f'{len(tests)} test groups passed') diff --git a/_sd/tests/test_fat_offsets.py b/_sd/tests/test_fat_offsets.py index 6bf9bf3..9380cd4 100644 --- a/_sd/tests/test_fat_offsets.py +++ b/_sd/tests/test_fat_offsets.py @@ -20,9 +20,16 @@ class Driver: self.m = z80.Z80Machine() self.m.set_memory_block(0, BODY) self.disk = {} + self.fail_reads = set() + self.fail_writes = set() + self.io = [] self.ram = bytearray(256 * 16384) self.hooks = {SYMS[n]: n for n in - ('sdread_lba', 'sdread_lba_to', 'sdwrite_lba', 'write_fsinfo_hint')} + ('sdread_lba', 'sdread_lba_to', 'sdwrite_lba', + 'sdwrite_lba_from', 'write_fsinfo_hint')} + self.hooks.update({SYMS[n]: n for n in + ('sdwrite_multi_start', 'sdwrite_multi_block', 'sdwrite_multi_stop')}) + self.multi_lba = None for address in [0xff00, *self.hooks]: self.m.set_breakpoint(address) self.put('reserved_sectors', 32) @@ -59,9 +66,28 @@ class Driver: hook = self.hooks.get(self.m.pc) if not hook: continue - if hook != 'write_fsinfo_hint': + failed = False + if hook == 'sdwrite_multi_start': + p = SYMS['lba_arg'] + self.multi_lba = int.from_bytes(self.m.memory[p:p+4], 'big') + elif hook == 'sdwrite_multi_stop': + self.multi_lba = None + elif hook == 'sdwrite_multi_block': + assert self.multi_lba is not None + p = SYMS['sdbuf'] + self.disk[self.multi_lba] = bytearray(self.m.memory[p:p+512]) + self.io.append((hook, self.multi_lba)) + self.multi_lba += 1 + elif hook != 'write_fsinfo_hint': lba = int.from_bytes(self.m.memory[SYMS['lba_arg']:SYMS['lba_arg']+4], 'big') - if hook == 'sdwrite_lba': + self.io.append((hook, lba)) + failed = lba in (self.fail_reads if hook.startswith('sdread') else self.fail_writes) + if failed: + pass + elif hook == 'sdwrite_lba_from': + source = self.get('dma_xfer_page', 1)*16384 + (self.get('dma_xfer_off') & 16383) + self.disk[lba] = self.ram[source:source+512] + elif hook == 'sdwrite_lba': p = SYMS['sdbuf'] self.disk[lba] = bytearray(self.m.memory[p:p+512]) elif hook == 'sdread_lba': @@ -72,9 +98,11 @@ class Driver: else: dest = self.get('dma_xfer_page', 1)*16384 + (self.get('dma_xfer_off') & 16383) self.ram[dest:dest+512] = self.disk[lba] - self.m.af = 0x0040 # A=0, Z=1 (successful sector I/O) + self.m.af = 0x0100 if failed else 0x0040 self.m.pc = int.from_bytes(self.m.memory[self.m.sp:self.m.sp+2], 'little') self.m.sp += 2 + if self.m.pc == 0xff00: + return self.m.a raise AssertionError(f'{name}: execution did not return') diff --git a/_sd/tests/test_resident.py b/_sd/tests/test_resident.py new file mode 100644 index 0000000..98d3cf7 --- /dev/null +++ b/_sd/tests/test_resident.py @@ -0,0 +1,80 @@ +"""Resident routines; build resident with --sym=resident_syms_new.txt first.""" +import re +from test_fat_offsets import Driver, ROOT, SYMS, BODY + +SYMS.update((name, int(value, 16)) for name, value in re.findall( + r'^(\w+): EQU 0x([0-9A-Fa-f]+)$', (ROOT/'resident_syms_new.txt').read_text(), re.M)) + + +def driver(): + d = Driver() + d.m.set_memory_block(0x2000, (ROOT/'zc_sd_driver.bin').read_bytes()) + return d + + +def test_depacker(): + d = driver() + d.m.set_memory_block(0, bytes(len(BODY))) + assert d.call('drv_dos_swp') == 0 + assert bytes(d.m.memory[:len(BODY)]) == BODY + assert bytes(d.m.memory[0x2000:0x2000+(ROOT/'zc_sd_driver.bin').stat().st_size]) == (ROOT/'zc_sd_driver.bin').read_bytes() + + +def test_free_chain(): + for broken in (False, True): + d = driver() + chain = [319, 500, 320, 1023] + for i, n in enumerate(chain): + d.fat(n, chain[i+1] if i+1 < len(chain) else 0x0ffffff8) + d.cluster(chain[0]) + if broken: + d.fail_reads.add(49+chain[0]//128) + assert d.call('core_free_chain') != 0 + assert not [x for x in d.io if x[0].startswith('sdwrite')] + else: + assert d.call('core_free_chain') == 0 + for n in chain: + sector, offset = divmod(n*4, 512) + assert d.disk[49+sector][offset:offset+4] == bytes(4) + + +def test_file_abi(): + d = driver() + d.put('rtc_cached', 1, 1) + for s in range(49, 113): + d.disk[s] = bytearray(512) + d.fat(2, 0x0fffffff) + d.disk[1017] = bytearray(512) + d.disk[1018] = bytearray(512) + d.put('root_cluster_lo', 2) + d.put('next_search_valid', 1, 1) + d.put('next_search_lo', 319) + d.call('core_setroot') + def arg(data): + d.m.set_memory_block(0x9000, data+b'\0') + d.m.hl = 0x9000 + arg(b'\0'+(1024).to_bytes(4, 'little')+b'Example long file.bin') + d.m.ix = 0xabcd + assert d.call('wrap_mkfile') == 0 + assert d.m.ix == 0xabcd + arg(b'\0Example long file.bin') + assert d.call('drv_fentry') == 1 + assert (d.m.de, d.m.hl) == (0, 1024) + arg(b'\0Example long file.bin') + d.m.set_memory_block(0x9200, b'Renamed long file.bin\0') + d.m.de = 0x9200 + assert d.call('core_renam') == 1 + arg(b'\0Renamed long file.bin') + assert d.call('drv_fentry') == 1 + arg(b'\0Example long file.bin') + assert d.call('drv_fentry') == 0 + arg(b'\0Renamed long file.bin') + assert d.call('core_delfl') == 1 + arg(b'\0Renamed long file.bin') + assert d.call('drv_fentry') == 0 + + +if __name__ == '__main__': + for test in (test_depacker, test_free_chain, test_file_abi): + test() + print('PASS:', test.__name__) diff --git a/_sd/zc_sd_driver.asm b/_sd/zc_sd_driver.asm index bc0108a..f36fe4c 100644 --- a/_sd/zc_sd_driver.asm +++ b/_sd/zc_sd_driver.asm @@ -540,6 +540,8 @@ cfc_loop ld hl,(cfc_cur_hi) ld (cur_cluster_lo),hl call get_next_cluster ld a,(chain_ended) + cp 2 + jr nc,cfc_fail ; do not free a link whose FAT read failed or a jr nz,cfc_nonext ld hl,(cur_cluster_hi) @@ -696,6 +698,8 @@ core_delfl inc hl ; skip flag byte ld hl,(fbn_cluster_lo) ld (cur_cluster_lo),hl call core_free_chain + or a + jr nz,cdf_fail ld hl,(fbn_match_run_start_lo) ld (cmdr_start_lo),hl @@ -706,10 +710,14 @@ core_delfl inc hl ; skip flag byte ld hl,(active_dir_lo) ld (cur_cluster_lo),hl call core_mark_deleted_range + or a + jr nz,cdf_fail xor a inc a ; NZ -- FILE DELETED ret +cdf_fail xor a ; Z -- deletion failed + ret ;--- core_renam: HL -> {flag(1),oldname(1-255),0}, DE -> {newname ;--- (1-255),0} -> renames an entry in the active directory, preserving diff --git a/_sd/zc_sd_driver_body.asm b/_sd/zc_sd_driver_body.asm index 61f862a..ba2fbd3 100644 --- a/_sd/zc_sd_driver_body.asm +++ b/_sd/zc_sd_driver_body.asm @@ -37,7 +37,9 @@ CONF EQU #77 DATA EQU #57 -sdinit call csh +sdinit xor a + ld (sd_block_addressed),a + call csh ld de,512+10 call cycl @@ -134,6 +136,9 @@ sd_v2 ld de,#01aa sd_v2_ocr_rest in a,(c) ; discard the remaining 3 OCR bytes in a,(c) in a,(c) + ld a,(sd_block_addressed) + or a + jr z,sd_fbs ; SDv2 SDSC also requires a 512-byte block length sdinit_ok call csh xor a @@ -145,12 +150,8 @@ sdinit_fail call csh ret ;--- set_lba32: HL=bits31-16, DE=bits15-0 -> stores into lba_arg (MSB -;--- first). Do this BEFORE calling sdread_lba, not with the LBA sitting -;--- in registers across the call -- csl calls wait, which uses DE as a -;--- busy-loop counter and never restores it, so DE (and only DE) comes -;--- back scrambled. That's exactly what broke the register-argument -;--- version earlier: H/L survived, D/E didn't. Routing the argument -;--- through memory instead sidesteps the whole problem. --- +;--- first). Store the LBA before SD I/O: transfer routines use registers +;--- for command framing and DMA setup. --- set_lba32 ld a,h ld (lba_arg),a ld a,l @@ -611,6 +612,7 @@ csl push bc wait push bc push af + push de ; preserve sdinit's retry counter across csl ld bc,DATA ld de,60000 1 in a,(c) @@ -620,7 +622,8 @@ wait push bc ld a,d or e jr nz,1b -2 pop af +2 pop de + pop af pop bc ret @@ -1332,8 +1335,8 @@ ctf_bad xor a ;--- get_next_cluster: reads the FAT32 table entry for the cluster in ;--- (cur_cluster_hi,cur_cluster_lo) and, if it isn't an end-of-chain ;--- marker, replaces cur_cluster_hi/lo with the next cluster. Sets -;--- chain_ended=1 on end-of-chain (masked entry >=0x0FFFFFF8), an -;--- out-of-range cluster, or a read error, 0 otherwise. --- +;--- chain_ended=1 on EOC (masked entry >=0x0FFFFFF8), 2 on an +;--- invalid link or read error, 0 on a valid next cluster. --- chain_ended db 0 nxt_lo dw 0 @@ -1342,7 +1345,7 @@ nxt_hi dw 0 get_next_cluster call cluster_to_fatpos or a - jr nz,gnc_end + jr nz,gnc_bad ld hl,(gnc_fatsec_hi) ld de,(gnc_fatsec_lo) @@ -1350,7 +1353,7 @@ get_next_cluster call set_lba32 call sdread_lba or a - jr nz,gnc_end + jr nz,gnc_bad ld hl,(gnc_inoff) ld de,sdbuf @@ -1373,14 +1376,34 @@ get_next_cluster cp #0f jr nz,gnc_have_next ld a,l - cp #f8 + cp #ff jr c,gnc_have_next + ld hl,(nxt_lo) + ld de,#fff0 + or a + sbc hl,de + jr c,gnc_have_next + ld a,l ; #0FFFFFF0..7 are reserved/bad, not EOC + cp 8 + jr c,gnc_bad gnc_end ld a,1 + jr gnc_stop +gnc_bad ld a,2 +gnc_stop ld (chain_ended),a ret -gnc_have_next ld hl,(nxt_lo) +gnc_have_next ld hl,(nxt_hi) + ld a,h + or l + jr nz,gnc_valid + ld hl,(nxt_lo) + ld de,2 + or a + sbc hl,de + jr c,gnc_bad ; free/reserved links 0 and 1 +gnc_valid ld hl,(nxt_lo) ld (cur_cluster_lo),hl ld hl,(nxt_hi) ld (cur_cluster_hi),hl @@ -1598,7 +1621,7 @@ write_fsinfo_hint ld (sdbuf+488),hl jr nc,wfi_skip_count ld hl,(sdbuf+490) - ld de,1 + ld de,0 ; low-word subtraction already supplied the borrow sbc hl,de ld (sdbuf+490),hl @@ -1970,9 +1993,9 @@ strm_calc_sector ;--- strm_advance: moves the stream position forward by one sector, ;--- crossing into the next cluster (via get_next_cluster) if that was ;--- the last sector of the current one. Sets stream_eoc=1 if the chain -;--- ends there. Always returns A=0 (advancing itself can't fail -- -;--- get_next_cluster's own failure just means stream_eoc becomes 1, -;--- which the NEXT read/write call reports). --- +;--- ends there, or stream_eoc=2 on a broken link / FAT read error. +;--- Returns A=0: the sector just transferred remains valid; the NEXT +;--- read/write reports the stopped stream. Only EOC=1 permits growth. --- strm_advance ld a,(stream_sec_in_cluster) inc a ld b,a ; b = candidate new sec_in_cluster @@ -2086,6 +2109,8 @@ stream_data_save ds 512 ; scratch: the caller's pending sector data, stream_write_sector ld a,(stream_eoc) + cp 2 + ret nc ; a broken chain must never be extended or a jr z,strm_write_have @@ -2172,6 +2197,8 @@ strm_write_have call strm_calc_sector ;--- see dma_xfer_off's own comment above. --- stream_write_sector_from ld a,(stream_eoc) + cp 2 + ret nc or a jr z,strm_write_have2 @@ -3081,6 +3108,8 @@ find_free_run call stream_open ld (wne_cur_index_lo),hl ffr_sector_loop ld a,(stream_eoc) + cp 2 + jp nc,ffr_fail or a jr z,ffr_have_sector call ffr_grow_dir diff --git a/_spg/ZIFI/zifi.spg b/_spg/ZIFI/zifi.spg index 2f6d1a934022464ac180840888341ee0b490e6ed..d73bca02e41df14d4e47cf09b4c66345d0e7ce7c 100644 GIT binary patch delta 4382 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 delta 4332 zcmWO6jU$tZ;{fp4Y;4GeWY^>SdA+G^pPkj$Gz$K#hyB z>{imdy!+I(;0ilVBUOFShJ4C6I9L;h_D!9M2(h7tS~CpBIfA57zDQGP%Ed03v0!JQ zC)BUxXcF!#CT6cjmDQkXY#6Sh_eF64OX0WUZQo*VSJ6&75DeMtHUD-Zy}fRtH(QJR zjDNJ?=_9U`71T&!nX|Br@UavFi^W058XJ2-kREQz;mxO$MZaI9MrQK;dgOrpMq~rG4QRi(NZ5DRf;AGRH^ETg_!N;eb zoXu5TF8NjSZ_VR&k(hMf?9#iL;LI=lhpz|hu&-h7=wanFL4SywqUj#bKco4_;})a! zkK4Gs@_1go!RUqbe@5Zy^Zy(!aEOe;(U;!PYE7t*P+xXy9(`G@DRXYs0FQX>CpW%q zv1Glo;s9&M6fC^(#OrG733K7;S+J8}4H^Z&`8W*bHK_4(uuUm%f=XZ_AskR{ZQ+@! z+TkdJ&=oU>bM;5-PRW38!q9$-C#<&->-4=20Vwnat(5uaaDr9b#8|LM!d=XhE=ZZp zq8`O|z1o-HjeBWO7XnmnI@<5zv?-ED=WfCeY1b1A^OAyy<5Sv;h?u0Q%;PO$b+ z8f~NomhS)IO3Fugs$s3EKxem#{z-Y#S9yLW!ro|KA-%>`_Ti)qFj^2SkHfamIUGuh zDQN;X>}p_m`PDl?dug;Fl+hcwZBMX0Ug+pyu?rW8O`?4oN-q6Y^xdgQT%B0_!fl$3bqd%i# zguVaDdwzQ9>~|QZZhhOX5lXMs;Se2%{@Lj`X#?vFsBz3&&QJEraX(`;$S7Eri=x|= zpwe#Q%oROFtQm8NK*ImtEzylPqOg*R<=Ny*u)W5i>`YbKQiC0wuXIlADn^i);p^ubhtUG+PpytTUXbk;WIRh6pB$&&BWdKzCCHpVvv(I_3QgcH?X=jFz_
PD<~Zy;TUaIIt>#mzr( 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 F{{Xich(rJY