├── .github └── workflows │ ├── ci-build.yml │ └── crates-io-publish.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── clippy.toml ├── resources ├── TEST.DAT ├── TEST_BLOCK.DAT ├── TEST_EMPTY.DAT ├── TEST_MACRO.DAT ├── TEST_SECTION.DAT ├── TEST_XOR.DAT └── default_dats │ ├── ACQ.DAT │ ├── ADDON.DAT │ ├── COMMON.DAT │ ├── CONTROL0.DAT │ ├── CONTROL1.DAT │ ├── GEARSET.DAT │ ├── GS.DAT │ ├── ITEMFDR.DAT │ ├── ITEMODR.DAT │ ├── KEYBIND.DAT │ ├── LOGFLTR.DAT │ ├── MACRO.DAT │ └── UISAVE.DAT ├── rustfmt.toml └── src ├── dat_error.rs ├── dat_file.rs ├── dat_type.rs ├── high_level.rs ├── high_level_modules ├── macro.rs ├── macro │ └── icon.rs └── mod.rs ├── lib.rs └── section.rs /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: CIBuild 2 | 3 | on: 4 | push: 5 | branches: ['**'] 6 | tags-ignore: ['**'] 7 | pull_request: 8 | branches: ['**'] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | toolchain: [stable, nightly] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Install toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: ${{ matrix.toolchain }} 25 | override: true 26 | components: rustfmt, clippy 27 | - name: Build 28 | run: cargo build --verbose 29 | - name: Test 30 | run: cargo test --verbose --all-features 31 | - name: Rustfmt 32 | run: rustfmt -v --check **/*.rs 33 | - name: Clippy 34 | run: cargo clippy --all-features 35 | -------------------------------------------------------------------------------- /.github/workflows/crates-io-publish.yml: -------------------------------------------------------------------------------- 1 | name: CratesIOPublish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v?[0-9]+.[0-9]+.[0-9]+' 7 | branches-ignore: 8 | - '**' 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | override: true 23 | components: rustfmt, clippy 24 | - name: Build 25 | run: cargo build --verbose 26 | - name: Test 27 | run: cargo test --verbose --all-features 28 | - name: Rustfmt 29 | run: rustfmt -v --check **/*.rs 30 | - name: Clippy 31 | run: cargo clippy --all-features 32 | - name: Cargo Login 33 | run: cargo login ${{ secrets.CRATES_IO_TOKEN }} 34 | - name: Cargo Publish 35 | if: success() 36 | run: cargo publish -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libxivdat" 3 | version = "0.2.0" 4 | edition = "2018" 5 | license = "Apache-2.0" 6 | description = "Read & write Final Fantasy XIV .DAT files." 7 | keywords = ["xiv", "io", "file", "dat", "config"] 8 | categories = ["encoding", "filesystem"] 9 | authors = ["Carrie J V =1.52.0. Earlier versions may work, but use at your own risk. 15 | 16 | CI builds are run against current stable and nightly at time of build. 17 | 18 | ## DATFile 19 | 20 | `DATFile` provides a common, low-level method of working with binary DAT files. `DATFile` emulates Rust's std lib `File` but reads and writes only from the inner content block of .DAT files, automatically handling header updates, padding, and masking as necessary. 21 | 22 | ## High Level Modules 23 | 24 | Higher-level support for specific file types is implemented on a type-by-type basis as optional features. See the [chart below](#dat-type-support) for more information and feature names. 25 | 26 | High level modules allow working with DAT files at a resource level (ie, Macros or Gearsets) as opposed to working with raw byte streams from `DATFile`. 27 | 28 | ## DAT Data Content 29 | 30 | Most DAT files (excluding those marked as "Unique" in the support table), share a common file structure consisting of a header, content block, and footer. 31 | 32 | Internally, some DAT file content blocks use a variable-length data structure referred to as a `section` in this library. A section consists of a single UTF-8 char type tag, u16le size, and a null-terminated UTF-8 string. A single resource (ie, a macro) is then comprised of a repeating pattern of sections. A toolkit for working with sections is provided in the `section` submodule. 33 | 34 | Other DAT files use fixed-size resource blocks, with each resource immediately following the last. These are referred to as "Block DATs" below. 35 | 36 | ## Plaintext DAT Files 37 | 38 | Some DAT files (namely `COMMON.DAT`, `CONTROL0.DAT`, and `CONTROL1.DAT`) are actually just UTF-8 plaintext and do not share a common format with the binary DAT files. 39 | 40 | Support for working with plaintext DATs may happen at some future point, but isn't currently a priority. 41 | 42 | ## Unique Binary DAT Files 43 | 44 | Two binary file types (`ADDON.DAT` and `FFXIV_CHARA_XX.DAT` files) do not use the common shared structure of other DAT files. Support for these files is not currently planned. 45 | 46 | ## Future Plans 47 | 48 | The goal of this library is to fully abstract the data structures of DAT files and create high-level, resource-based interfaces for each file type that operate on resources (ie, macros or gearsets) rather than raw byte streams. Each file has its own internal data types, so these layers will be implemented one at a time as optional features. 49 | 50 | My focus with this libary is mainly on macros, gearsets, and ui config. High level support for other DAT types will liklely be a long time coming unless you build it yourself. 51 | 52 | ## DAT Type Support 53 | 54 | | Symbol | Description | 55 | |--------|-----------------| 56 | | ✅ | Full support | 57 | | 🌀 | Partial support | 58 | | ❌ | No support | 59 | 60 | | File | Contains | Type | DATFile Read/Write | High Level Module | 61 | |--------------------|----------------------------------|------------|--------------------|-------------------| 62 | | ACQ.DAT | Recent /tell history | Section | ✅ | 🌀 - `section` | 63 | | ADDON.DAT | ? | Unique | ❌ | ❌ | 64 | | COMMON.DAT | Character configuration | Plaintext | ❌ | ❌ | 65 | | CONTROL0.DAT | Gamepad control config | Plaintext | ❌ | ❌ | 66 | | CONTROL1.DAT | Keyboard/mouse control config | Plaintext | ❌ | ❌ | 67 | | FFXIV_CHARA_XX.DAT | Character appearance presets | Unique | ❌ | ❌ | 68 | | GEARSET.DAT | Gearsets | Block | ✅ | ❌ | 69 | | GS.DAT | Gold Saucer config (Triad decks) | Block | ✅ | ❌ | 70 | | HOTBAR.DAT | Hotbar layouts | Block | ✅ | ❌ | 71 | | ITEMFDR.DAT | "Search for item" indexing? | Block | ✅ | ❌ | 72 | | ITEMODR.DAT | Item order in bags | Block | ✅ | ❌ | 73 | | KEYBIND.DAT | Keybinds | Section | ✅ | 🌀 - `section` | 74 | | LOGFLTR.DAT | Chat log filters? | Block | ✅ | ❌ | 75 | | MACRO.DAT | Character-specific macros | Section | ✅ | ✅ - `macro` | 76 | | MACROSYS.DAT | System-wide macros | Section | ✅ | ✅ - `macro` | 77 | | UISAVE.DAT | UI config | Block | ✅ | ❌ | 78 | 79 | ## Special Thanks 80 | 81 | [EmperorArthur/FFXIV_Settings](https://github.com/EmperorArthur/FFXIV_Settings) was my jumping off point for research when developing this library. 82 | 83 | ## Contributing 84 | 85 | Contributions are always welcomed. Please ensure code passes `cargo test --all-features`, `cargo clippy --all-features`, and `rustfmt -v --check **/*.rs` before making pull requests. 86 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.52.0" -------------------------------------------------------------------------------- /resources/TEST.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/TEST.DAT -------------------------------------------------------------------------------- /resources/TEST_BLOCK.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/TEST_BLOCK.DAT -------------------------------------------------------------------------------- /resources/TEST_EMPTY.DAT: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /resources/TEST_MACRO.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/TEST_MACRO.DAT -------------------------------------------------------------------------------- /resources/TEST_SECTION.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/TEST_SECTION.DAT -------------------------------------------------------------------------------- /resources/TEST_XOR.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/TEST_XOR.DAT -------------------------------------------------------------------------------- /resources/default_dats/ACQ.DAT: -------------------------------------------------------------------------------- 1 | d -------------------------------------------------------------------------------- /resources/default_dats/ADDON.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/ADDON.DAT -------------------------------------------------------------------------------- /resources/default_dats/COMMON.DAT: -------------------------------------------------------------------------------- 1 | BattleEffectSelf 0 2 | BattleEffectParty 0 3 | BattleEffectOther 0 4 | 5 | 6 | PadMode 0 7 | 8 | 9 | 10 | 11 | WeaponAutoPutAway 1 12 | WeaponAutoPutAwayTime 3 13 | LipMotionType 1 14 | 15 | 16 | FirstPersonDefaultYAngle 0.000000 17 | FirstPersonDefaultZoom 0.780000 18 | FirstPersonDefaultDistance 1.500000 19 | ThirdPersonDefaultYAngle -0.349066 20 | ThirdPersonDefaultZoom 0.780000 21 | ThirdPersonDefaultDistance 6.000000 22 | LockonDefaultYAngle -0.349066 23 | LockonDefaultZoom 0.610000 24 | LockonDefaultZoom 6.000000 25 | CameraProductionOfAction 1 26 | FPSCameraInterpolationType 1 27 | FPSCameraVerticalInterpolation 0 28 | LegacyCameraType 1 29 | EventCameraAutoControl 1 30 | CameraLookBlinkType 0 31 | IdleEmoteTime 5 32 | IdleEmoteRandomType 0 33 | CutsceneSkipIsShip 0 34 | CutsceneSkipIsContents 0 35 | CutsceneSkipIsHousing 0 36 | PetTargetOffInCombat 1 37 | GroundTargetFPSPosX 50000 38 | GroundTargetFPSPosY 50000 39 | GroundTargetTPSPosX 50000 40 | GroundTargetTPSPosY 45000 41 | TargetDisableAnchor 0 42 | TargetCircleClickFilterEnableNearestCursor 0 43 | TargetEnableMouseOverSelect 0 44 | TurnSpeed 50 45 | FootEffect 0 46 | LegacySeal 1 47 | GBarrelDisp 1 48 | EgiMirageTypeGaruda 0 49 | EgiMirageTypeTitan 0 50 | EgiMirageTypeIfrit 0 51 | BahamutSize 2 52 | TimeMode 2 53 | Time12 0 54 | TimeEorzea 0 55 | TimeLocal 1 56 | TimeServer 0 57 | ActiveLS_H 0 58 | ActiveLS_L 0 59 | HotbarLock 0 60 | HotbarDispRecastTime 1 61 | HotbarCrossContentsActionEnableInput 1 62 | ExHotbarChangeHotbar1 1 63 | HotbarCommon01 0 64 | HotbarCommon02 0 65 | HotbarCommon03 0 66 | HotbarCommon04 1 67 | HotbarCommon05 1 68 | HotbarCommon06 1 69 | HotbarCommon07 1 70 | HotbarCommon08 1 71 | HotbarCommon09 1 72 | HotbarCommon10 1 73 | HotbarCrossCommon01 0 74 | HotbarCrossCommon02 0 75 | HotbarCrossCommon03 0 76 | HotbarCrossCommon04 1 77 | HotbarCrossCommon05 1 78 | HotbarCrossCommon06 1 79 | HotbarCrossCommon07 1 80 | HotbarCrossCommon08 1 81 | HotbarCrossHelpDisp 1 82 | HotbarCrossOperation 0 83 | HotbarCrossDisp 0 84 | HotbarCrossLock 0 85 | HotbarCrossUsePadGuide 1 86 | HotbarCrossActiveSet 0 87 | HotbarCrossActiveSetPvP 0 88 | HotbarCrossSetChangeCustomIsAuto 1 89 | HotbarCrossSetChangeCustom 0 90 | HotbarCrossSetChangeCustomSet1 1 91 | HotbarCrossSetChangeCustomSet2 1 92 | HotbarCrossSetChangeCustomSet3 0 93 | HotbarCrossSetChangeCustomSet4 0 94 | HotbarCrossSetChangeCustomSet5 0 95 | HotbarCrossSetChangeCustomSet6 0 96 | HotbarCrossSetChangeCustomSet7 0 97 | HotbarCrossSetChangeCustomSet8 1 98 | HotbarCrossSetChangeCustomIsSword 0 99 | HotbarCrossSetChangeCustomIsSwordSet1 1 100 | HotbarCrossSetChangeCustomIsSwordSet2 1 101 | HotbarCrossSetChangeCustomIsSwordSet3 0 102 | HotbarCrossSetChangeCustomIsSwordSet4 0 103 | HotbarCrossSetChangeCustomIsSwordSet5 0 104 | HotbarCrossSetChangeCustomIsSwordSet6 0 105 | HotbarCrossSetChangeCustomIsSwordSet7 0 106 | HotbarCrossSetChangeCustomIsSwordSet8 0 107 | HotbarCrossAdvancedSetting 0 108 | HotbarCrossAdvancedSettingLeft 15 109 | HotbarCrossAdvancedSettingRight 14 110 | HotbarCrossSetPvpModeActive 0 111 | HotbarCrossSetChangeCustomPvp 0 112 | HotbarCrossSetChangeCustomIsAutoPvp 1 113 | HotbarCrossSetChangeCustomSet1Pvp 1 114 | HotbarCrossSetChangeCustomSet2Pvp 1 115 | HotbarCrossSetChangeCustomSet3Pvp 0 116 | HotbarCrossSetChangeCustomSet4Pvp 0 117 | HotbarCrossSetChangeCustomSet5Pvp 0 118 | HotbarCrossSetChangeCustomSet6Pvp 0 119 | HotbarCrossSetChangeCustomSet7Pvp 0 120 | HotbarCrossSetChangeCustomSet8Pvp 1 121 | HotbarCrossSetChangeCustomIsSwordPvp 0 122 | HotbarCrossSetChangeCustomIsSwordSet1Pvp 1 123 | HotbarCrossSetChangeCustomIsSwordSet2Pvp 1 124 | HotbarCrossSetChangeCustomIsSwordSet3Pvp 0 125 | HotbarCrossSetChangeCustomIsSwordSet4Pvp 0 126 | HotbarCrossSetChangeCustomIsSwordSet5Pvp 0 127 | HotbarCrossSetChangeCustomIsSwordSet6Pvp 0 128 | HotbarCrossSetChangeCustomIsSwordSet7Pvp 0 129 | HotbarCrossSetChangeCustomIsSwordSet8Pvp 0 130 | HotbarCrossAdvancedSettingPvp 0 131 | HotbarCrossAdvancedSettingLeftPvp 15 132 | HotbarCrossAdvancedSettingRightPvp 14 133 | HotbarWXHBEnable 0 134 | HotbarWXHBSetLeft 14 135 | HotbarWXHBSetRight 15 136 | HotbarWXHBEnablePvP 0 137 | HotbarWXHBSetLeftPvP 14 138 | HotbarWXHBSetRightPvP 15 139 | HotbarWXHB8Button 0 140 | HotbarWXHB8ButtonPvP 0 141 | HotbarWXHBSetInputTime 25 142 | HotbarWXHBDisplay 0 143 | HotbarWXHBFreeLayout 0 144 | HotbarXHBActiveTransmissionAlpha 0 145 | HotbarXHBAlphaDefault 0 146 | HotbarXHBAlphaActiveSet 0 147 | HotbarXHBAlphaInactiveSet 0 148 | HotbarWXHBInputOnce 0 149 | IdlingCameraSwitchType 1 150 | PlateType 1 151 | PlateDispHPBar 1 152 | PlateDisableMaxHPBar 1 153 | NamePlateHpSizeType 1 154 | NamePlateColorSelf 4294901736 155 | NamePlateEdgeSelf 4288172040 156 | NamePlateDispTypeSelf 0 157 | NamePlateNameTypeSelf 0 158 | NamePlateHpTypeSelf 2 159 | NamePlateColorSelfBuddy 4293066729 160 | NamePlateEdgeSelfBuddy 4278213930 161 | NamePlateDispTypeSelfBuddy 0 162 | NamePlateHpTypeSelfBuddy 2 163 | NamePlateColorSelfPet 4293066729 164 | NamePlateEdgeSelfPet 4278213930 165 | NamePlateDispTypeSelfPet 0 166 | NamePlateHpTypeSelfPet 2 167 | NamePlateColorParty 4294901736 168 | NamePlateEdgeParty 4288172040 169 | NamePlateDispTypeParty 0 170 | NamePlateNameTypeParty 0 171 | NamePlateHpTypeParty 2 172 | NamePlateDispTypePartyPet 0 173 | NamePlateHpTypePartyPet 2 174 | NamePlateDispTypePartyBuddy 0 175 | NamePlateHpTypePartyBuddy 2 176 | NamePlateColorAlliance 4282964140 177 | NamePlateEdgeAlliance 4279781177 178 | NamePlateDispTypeAlliance 1 179 | NamePlateNameTypeAlliance 0 180 | NamePlateHpTypeAlliance 2 181 | NamePlateDispTypeAlliancePet 1 182 | NamePlateHpTypeAlliancePet 2 183 | NamePlateColorOther 4294234268 184 | NamePlateEdgeOther 4283251221 185 | NamePlateDispTypeOther 0 186 | NamePlateNameTypeOther 0 187 | NamePlateHpTypeOther 2 188 | NamePlateDispTypeOtherPet 2 189 | NamePlateHpTypeOtherPet 2 190 | NamePlateDispTypeOtherBuddy 2 191 | NamePlateHpTypeOtherBuddy 2 192 | NamePlateColorUnengagedEnemy 4289788159 193 | NamePlateEdgeUnengagedEnemy 4278210403 194 | NamePlateDispTypeUnengagedEnemy 0 195 | NamePlateHpTypeUnengagedEmemy 1 196 | NamePlateColorEngagedEnemy 4290756095 197 | NamePlateEdgeEngagedEnemy 4278190244 198 | NamePlateDispTypeEngagedEnemy 0 199 | NamePlateHpTypeEngagedEmemy 1 200 | NamePlateColorClaimedEnemy 4294958335 201 | NamePlateEdgeClaimedEnemy 4287768974 202 | NamePlateDispTypeClaimedEnemy 0 203 | NamePlateHpTypeClaimedEmemy 1 204 | NamePlateColorUnclaimedEnemy 4291354879 205 | NamePlateEdgeUnclaimedEnemy 4278206592 206 | NamePlateDispTypeUnclaimedEnemy 0 207 | NamePlateHpTypeUnclaimedEmemy 1 208 | NamePlateColorNpc 4293066729 209 | NamePlateEdgeNpc 4278213930 210 | NamePlateDispTypeNpc 0 211 | NamePlateHpTypeNpc 1 212 | NamePlateColorObject 4294967295 213 | NamePlateEdgeObject 4278190080 214 | NamePlateDispTypeObject 0 215 | NamePlateHpTypeObject 1 216 | NamePlateColorMinion 4294967295 217 | NamePlateEdgeMinion 4278190080 218 | NamePlateDispTypeMinion 0 219 | NamePlateColorOtherBuddy 4293066729 220 | NamePlateEdgeOtherBuddy 4278213930 221 | NamePlateColorOtherPet 4293066729 222 | NamePlateEdgeOtherPet 4278213930 223 | NamePlateNameTitleTypeSelf 1 224 | NamePlateNameTitleTypeParty 1 225 | NamePlateNameTitleTypeAlliance 1 226 | NamePlateNameTitleTypeOther 1 227 | NamePlateNameTitleTypeFriend 1 228 | NamePlateColorFriend 4278223103 229 | NamePlateColorFriendEdge 4278201434 230 | NamePlateDispTypeFriend 0 231 | NamePlateNameTypeFriend 0 232 | NamePlateHpTypeFriend 2 233 | NamePlateDispTypeFriendPet 2 234 | NamePlateHpTypeFriendPet 2 235 | NamePlateDispTypeFriendBuddy 2 236 | NamePlateHpTypeFriendBuddy 2 237 | NamePlateColorLim 4282401023 238 | NamePlateColorLimEdge 4278584896 239 | NamePlateColorGri 4278255615 240 | NamePlateColorGriEdge 4278210403 241 | NamePlateColorUld 4294942528 242 | NamePlateColorUldEdge 4282392576 243 | NamePlateColorHousingFurniture 4294967295 244 | NamePlateColorHousingFurnitureEdge 4278190080 245 | NamePlateDispTypeHousingFurniture 0 246 | NamePlateColorHousingField 4294967295 247 | NamePlateColorHousingFieldEdge 4278190080 248 | NamePlateDispTypeHousingField 1 249 | NamePlateNameTypePvPEnemy 0 250 | NamePlateDispTypeFeast 0 251 | NamePlateNameTypeFeast 0 252 | NamePlateHpTypeFeast 2 253 | NamePlateDispTypeFeastPet 0 254 | NamePlateHpTypeFeastPet 2 255 | NamePlateNameTitleTypeFeast 1 256 | NamePlateDispSize 0 257 | ActiveInfo 0 258 | PartyList 0 259 | PartyListStatus 5 260 | EnemyList 0 261 | TargetInfo 0 262 | Gil 0 263 | DTR 0 264 | PlayerInfo 0 265 | NaviMap 0 266 | Help 0 267 | CrossMainHelp 1 268 | HousingLocatePreview 1 269 | Log 0 270 | LogTell 0 271 | LogFontSize 12 272 | LogTabName2 Event 273 | LogTabName3 274 | LogTabFilter0 0 275 | LogTabFilter1 1 276 | LogTabFilter2 2 277 | LogTabFilter3 3 278 | LogChatFilter 1 279 | LogEnableErrMsgLv1 1 280 | LogNameType 0 281 | LogTimeDisp 0 282 | LogTimeSettingType 0 283 | LogTimeDispType 0 284 | IsLogTell 1 285 | IsLogParty 0 286 | LogParty 0 287 | IsLogAlliance 0 288 | LogAlliance 0 289 | IsLogFc 0 290 | LogFc 0 291 | IsLogPvpTeam 0 292 | LogPvpTeam 0 293 | IsLogLs1 0 294 | LogLs1 0 295 | IsLogLs2 0 296 | LogLs2 0 297 | IsLogLs3 0 298 | LogLs3 0 299 | IsLogLs4 0 300 | LogLs4 0 301 | IsLogLs5 0 302 | LogLs5 0 303 | IsLogLs6 0 304 | LogLs6 0 305 | IsLogLs7 0 306 | LogLs7 0 307 | IsLogLs8 0 308 | LogLs8 0 309 | IsLogBeginner 0 310 | LogBeginner 0 311 | IsLogCwls 0 312 | IsLogCwls2 0 313 | IsLogCwls3 0 314 | IsLogCwls4 0 315 | IsLogCwls5 0 316 | IsLogCwls6 0 317 | IsLogCwls7 0 318 | IsLogCwls8 0 319 | LogCwls 0 320 | LogCwls2 0 321 | LogCwls3 0 322 | LogCwls4 0 323 | LogCwls5 0 324 | LogCwls6 0 325 | LogCwls7 0 326 | LogCwls8 0 327 | LogRecastActionErrDisp 1 328 | LogPermeationRate 50 329 | LogFontSizeForm 12 330 | LogItemLinkEnableType 1 331 | LogFontSizeLog2 12 332 | LogTimeDispLog2 0 333 | LogPermeationRateLog2 50 334 | LogFontSizeLog3 12 335 | LogTimeDispLog3 0 336 | LogPermeationRateLog3 50 337 | LogFontSizeLog4 12 338 | LogTimeDispLog4 0 339 | LogPermeationRateLog4 50 340 | LogFlyingHeightMaxErrDisp 1 341 | LogCrossWorldName 1 342 | ChatType 1 343 | ShopSell 0 344 | ColorSay 4294506744 345 | ColorShout 4294944358 346 | ColorTell 4294949088 347 | ColorParty 4284933887 348 | ColorAlliance 4294934528 349 | ColorLS1 4292149119 350 | ColorLS2 4292149119 351 | ColorLS3 4292149119 352 | ColorLS4 4292149119 353 | ColorLS5 4292149119 354 | ColorLS6 4292149119 355 | ColorLS7 4292149119 356 | ColorLS8 4292149119 357 | ColorFCompany 4289518822 358 | ColorPvPGroup 4289518822 359 | ColorPvPGroupAnnounce 4289518822 360 | ColorBeginner 4292149119 361 | ColorEmoteUser 4290576368 362 | ColorEmote 4290576368 363 | ColorYell 4294967040 364 | ColorBeginnerAnnounce 4292149119 365 | ColorCWLS 4292149119 366 | ColorCWLS2 4292149119 367 | ColorCWLS3 4292149119 368 | ColorCWLS4 4292149119 369 | ColorCWLS5 4292149119 370 | ColorCWLS6 4292149119 371 | ColorCWLS7 4292149119 372 | ColorCWLS8 4292149119 373 | ColorAttackSuccess 4294934399 374 | ColorAttackFailure 4291611852 375 | ColorAction 4294967218 376 | ColorItem 4294967218 377 | ColorCureGive 4292149119 378 | ColorBuffGive 4287938815 379 | ColorDebuffGive 4294937798 380 | ColorEcho 4291611852 381 | ColorSysMsg 4291611852 382 | ColorFCAnnounce 4289518822 383 | ColorSysBattle 4291611852 384 | ColorSysGathering 4291611852 385 | ColorSysErr 4294921292 386 | ColorNpcSay 4289517640 387 | ColorItemNotice 4294967218 388 | ColorGrowup 4294958707 389 | ColorLoot 4291346592 390 | ColorCraft 4292919544 391 | ColorGathering 4292919544 392 | ShopConfirm 1 393 | ShopConfirmMateria 1 394 | ShopConfirmExRare 1 395 | ShopConfirmSpiritBondMax 1 396 | ItemSortItemCategory 0 397 | ItemSortEquipLevel 1 398 | ItemSortItemLevel 1 399 | ItemSortItemStack 1 400 | ItemSortTidyingType 0 401 | ItemNoArmoryMaskOff 0 402 | InfoSettingDispWorldNameType 1 403 | TargetNamePlateNameType 0 404 | FocusTargetNamePlateNameType 0 405 | ItemDetailTemporarilySwitch 1 406 | ItemDetailTemporarilySwitchKey 0 407 | ItemDetailTemporarilyHide 1 408 | ItemDetailTemporarilyHideKey 1 409 | ToolTipDispSize 0 410 | RecommendLoginDisp 1 411 | RecommendAreaChangeDisp 0 412 | PlayGuideLoginDisp 1 413 | PlayGuideAreaChangeDisp 0 414 | MapPadOperationYReverse 0 415 | MapPadOperationXReverse 0 416 | MapDispSize 0 417 | FlyTextDispSize 0 418 | PopUpTextDispSize 0 419 | DetailDispDelayType 0 420 | PartyListSortTypeTank 0 421 | PartyListSortTypeHealer 0 422 | PartyListSortTypeDps 0 423 | PartyListSortTypeOther 0 424 | RatioHpDisp 0 425 | BuffDispType 0 426 | ContentsFinderListSortType 0 427 | ContentsFinderSupplyEnable 0 428 | EnemyListCastbarEnable 1 429 | AchievementAppealLoginDisp 0 430 | ContentsFinderUseLangTypeJA 0 431 | ContentsFinderUseLangTypeEN 0 432 | ContentsFinderUseLangTypeDE 0 433 | ContentsFinderUseLangTypeFR 0 434 | ItemInventryWindowSizeType 0 435 | ItemInventryRetainerWindowSizeType 0 436 | EmoteTextType 1 437 | IsEmoteSe 0 438 | EmoteSeType 0 439 | PartyFinderNewArrivalDisp 1 440 | GPoseTargetFilterNPCLookAt 1 441 | GPoseMotionFilterAction 1 442 | LsListSortPriority 0 443 | FriendListSortPriority 0 444 | FriendListFilterType 0 445 | FriendListSortType 0 446 | LetterListFilterType 0 447 | LetterListSortType 0 448 | ContentsReplayEnable 0 449 | MouseWheelOperationUp 1 450 | MouseWheelOperationDown 2 451 | MouseWheelOperationCtrlUp 0 452 | MouseWheelOperationCtrlDown 0 453 | MouseWheelOperationAltUp 0 454 | MouseWheelOperationAltDown 0 455 | MouseWheelOperationShiftUp 0 456 | MouseWheelOperationShiftDown 0 457 | PvPFrontlinesGCFree 1 458 | -------------------------------------------------------------------------------- /resources/default_dats/CONTROL0.DAT: -------------------------------------------------------------------------------- 1 | 2 | 3 | AutoChangePointOfView 0 4 | KeyboardCameraInterpolationType 0 5 | KeyboardCameraVerticalInterpolation 0 6 | TiltOffset 0.000000 7 | KeyboardSpeed 0.500000 8 | PadSpeed 0.500000 9 | PadFpsXReverse 0 10 | PadFpsYReverse 0 11 | PadTpsXReverse 0 12 | PadTpsYReverse 0 13 | MouseFpsXReverse 0 14 | MouseFpsYReverse 0 15 | MouseTpsXReverse 0 16 | MouseTpsYReverse 0 17 | FlyingControlType 0 18 | FlyingLegacyAutorun 1 19 | 20 | 21 | AutoFaceTargetOnAction 1 22 | SelfClick 0 23 | NoTargetClickCancel 1 24 | AutoTarget 1 25 | TargetTypeSelect 0 26 | AutoLockOn 0 27 | CircleBattleModeAutoChange 1 28 | CircleIsCustom 0 29 | CircleSwordDrawnIsActive 1 30 | CircleSwordDrawnNonPartyPc 0 31 | CircleSwordDrawnParty 0 32 | CircleSwordDrawnEnemy 1 33 | CircleSwordDrawnAggro 1 34 | CircleSwordDrawnNpcOrObject 1 35 | CircleSwordDrawnMinion 0 36 | CircleSwordDrawnDutyEnemy 1 37 | CircleSwordDrawnPet 0 38 | CircleSwordDrawnAlliance 0 39 | CircleSwordDrawnMark 0 40 | CircleSheathedIsActive 1 41 | CircleSheathedNonPartyPc 1 42 | CircleSheathedParty 1 43 | CircleSheathedEnemy 1 44 | CircleSheathedAggro 1 45 | CircleSheathedNpcOrObject 1 46 | CircleSheathedMinion 1 47 | CircleSheathedDutyEnemy 1 48 | CircleSheathedPet 1 49 | CircleSheathedAlliance 1 50 | CircleSheathedMark 1 51 | CircleClickIsActive 1 52 | CircleClickNonPartyPc 0 53 | CircleClickParty 0 54 | CircleClickEnemy 1 55 | CircleClickAggro 1 56 | CircleClickNpcOrObject 1 57 | CircleClickMinion 0 58 | CircleClickDutyEnemy 1 59 | CircleClickPet 0 60 | CircleClickAlliance 0 61 | CircleClickMark 0 62 | CircleXButtonIsActive 1 63 | CircleXButtonNonPartyPc 1 64 | CircleXButtonParty 1 65 | CircleXButtonEnemy 0 66 | CircleXButtonAggro 0 67 | CircleXButtonNpcOrObject 1 68 | CircleXButtonMinion 0 69 | CircleXButtonDutyEnemy 0 70 | CircleXButtonPet 0 71 | CircleXButtonAlliance 1 72 | CircleXButtonMark 0 73 | CircleYButtonIsActive 1 74 | CircleYButtonNonPartyPc 1 75 | CircleYButtonParty 1 76 | CircleYButtonEnemy 1 77 | CircleYButtonAggro 1 78 | CircleYButtonNpcOrObject 1 79 | CircleYButtonMinion 1 80 | CircleYButtonDutyEnemy 1 81 | CircleYButtonPet 1 82 | CircleYButtonAlliance 1 83 | CircleYButtonMark 1 84 | CircleBButtonIsActive 1 85 | CircleBButtonNonPartyPc 0 86 | CircleBButtonParty 0 87 | CircleBButtonEnemy 1 88 | CircleBButtonAggro 1 89 | CircleBButtonNpcOrObject 0 90 | CircleBButtonMinion 0 91 | CircleBButtonDutyEnemy 1 92 | CircleBButtonPet 0 93 | CircleBButtonAlliance 0 94 | CircleBButtonMark 0 95 | CircleAButtonIsActive 1 96 | CircleAButtonNonPartyPc 0 97 | CircleAButtonParty 1 98 | CircleAButtonEnemy 0 99 | CircleAButtonAggro 0 100 | CircleAButtonNpcOrObject 0 101 | CircleAButtonMinion 0 102 | CircleAButtonDutyEnemy 0 103 | CircleAButtonPet 0 104 | CircleAButtonAlliance 0 105 | CircleAButtonMark 0 106 | GroundTargetType 0 107 | GroundTargetCursorSpeed 50 108 | TargetCircleType 1 109 | TargetLineType 1 110 | LinkLineType 1 111 | ObjectBorderingType 0 112 | MoveMode 0 113 | HotbarDisp 3 114 | HotbarEmptyVisible 1 115 | HotbarNoneSlotDisp01 1 116 | HotbarNoneSlotDisp02 1 117 | HotbarNoneSlotDisp03 1 118 | HotbarNoneSlotDisp04 1 119 | HotbarNoneSlotDisp05 1 120 | HotbarNoneSlotDisp06 1 121 | HotbarNoneSlotDisp07 1 122 | HotbarNoneSlotDisp08 1 123 | HotbarNoneSlotDisp09 1 124 | HotbarNoneSlotDisp10 1 125 | HotbarNoneSlotDispEX 1 126 | ExHotbarSetting 2 127 | HotbarExHotbarUseSetting 0 128 | HotbarCrossUseEx 1 129 | HotbarCrossUseExDirection 1 130 | HotbarCrossDispType 0 131 | PartyListSoloOff 1 132 | HowTo 1 133 | HousingFurnitureBindConfirm 1 134 | DirectChat 0 135 | CharaParamDisp 1 136 | LimitBreakGaugeDisp 1 137 | ScenarioTreeDisp 1 138 | ScenarioTreeCompleteDisp 0 139 | HotbarCrossDispAlways 1 140 | ExpDisp 1 141 | InventryStatusDisp 1 142 | DutyListDisp 1 143 | NaviMapDisp 1 144 | GilStatusDisp 1 145 | InfoSettingDisp 1 146 | InfoSettingDispType 0 147 | TargetInfoDisp 1 148 | EnemyListDisp 1 149 | FocusTargetDisp 1 150 | ItemDetailDisp 1 151 | ActionDetailDisp 1 152 | DetailTrackingType 0 153 | ToolTipDisp 1 154 | MapPermeationRate 25 155 | MapOperationType 0 156 | PartyListDisp 1 157 | PartyListNameType 0 158 | FlyTextDisp 1 159 | MapPermeationMode 0 160 | AllianceList1Disp 1 161 | AllianceList2Disp 1 162 | TargetInfoSelfBuff 0 163 | PopUpTextDisp 1 164 | ContentsInfoDisp 1 165 | DutyListHideWhenCntInfoDisp 1 166 | DutyListNumDisp 5 167 | InInstanceContentDutyListDisp 0 168 | InPublicContentDutyListDisp 0 169 | ContentsInfoJoiningRequestDisp 1 170 | ContentsInfoJoiningRequestSituationDisp 1 171 | HotbarDispSetNum 1 172 | HotbarDispSetChangeType 1 173 | HotbarDispSetDragType 1 174 | MainCommandType 0 175 | MainCommandDisp 1 176 | MainCommandDragShortcut 1 177 | HotbarDispLookNum 1 178 | -------------------------------------------------------------------------------- /resources/default_dats/CONTROL1.DAT: -------------------------------------------------------------------------------- 1 | 2 | 3 | AutoChangePointOfView 0 4 | KeyboardCameraInterpolationType 0 5 | KeyboardCameraVerticalInterpolation 0 6 | TiltOffset 0.000000 7 | KeyboardSpeed 0.500000 8 | PadSpeed 0.500000 9 | PadFpsXReverse 0 10 | PadFpsYReverse 0 11 | PadTpsXReverse 0 12 | PadTpsYReverse 0 13 | MouseFpsXReverse 0 14 | MouseFpsYReverse 0 15 | MouseTpsXReverse 0 16 | MouseTpsYReverse 0 17 | FlyingControlType 0 18 | FlyingLegacyAutorun 1 19 | 20 | 21 | AutoFaceTargetOnAction 1 22 | SelfClick 0 23 | NoTargetClickCancel 1 24 | AutoTarget 1 25 | TargetTypeSelect 0 26 | AutoLockOn 0 27 | CircleBattleModeAutoChange 1 28 | CircleIsCustom 0 29 | CircleSwordDrawnIsActive 1 30 | CircleSwordDrawnNonPartyPc 0 31 | CircleSwordDrawnParty 0 32 | CircleSwordDrawnEnemy 1 33 | CircleSwordDrawnAggro 1 34 | CircleSwordDrawnNpcOrObject 1 35 | CircleSwordDrawnMinion 0 36 | CircleSwordDrawnDutyEnemy 1 37 | CircleSwordDrawnPet 0 38 | CircleSwordDrawnAlliance 0 39 | CircleSwordDrawnMark 0 40 | CircleSheathedIsActive 1 41 | CircleSheathedNonPartyPc 1 42 | CircleSheathedParty 1 43 | CircleSheathedEnemy 1 44 | CircleSheathedAggro 1 45 | CircleSheathedNpcOrObject 1 46 | CircleSheathedMinion 1 47 | CircleSheathedDutyEnemy 1 48 | CircleSheathedPet 1 49 | CircleSheathedAlliance 1 50 | CircleSheathedMark 1 51 | CircleClickIsActive 1 52 | CircleClickNonPartyPc 0 53 | CircleClickParty 0 54 | CircleClickEnemy 1 55 | CircleClickAggro 1 56 | CircleClickNpcOrObject 1 57 | CircleClickMinion 0 58 | CircleClickDutyEnemy 1 59 | CircleClickPet 0 60 | CircleClickAlliance 0 61 | CircleClickMark 0 62 | CircleXButtonIsActive 1 63 | CircleXButtonNonPartyPc 1 64 | CircleXButtonParty 1 65 | CircleXButtonEnemy 0 66 | CircleXButtonAggro 0 67 | CircleXButtonNpcOrObject 1 68 | CircleXButtonMinion 0 69 | CircleXButtonDutyEnemy 0 70 | CircleXButtonPet 0 71 | CircleXButtonAlliance 1 72 | CircleXButtonMark 0 73 | CircleYButtonIsActive 1 74 | CircleYButtonNonPartyPc 1 75 | CircleYButtonParty 1 76 | CircleYButtonEnemy 1 77 | CircleYButtonAggro 1 78 | CircleYButtonNpcOrObject 1 79 | CircleYButtonMinion 1 80 | CircleYButtonDutyEnemy 1 81 | CircleYButtonPet 1 82 | CircleYButtonAlliance 1 83 | CircleYButtonMark 1 84 | CircleBButtonIsActive 1 85 | CircleBButtonNonPartyPc 0 86 | CircleBButtonParty 0 87 | CircleBButtonEnemy 1 88 | CircleBButtonAggro 1 89 | CircleBButtonNpcOrObject 0 90 | CircleBButtonMinion 0 91 | CircleBButtonDutyEnemy 1 92 | CircleBButtonPet 0 93 | CircleBButtonAlliance 0 94 | CircleBButtonMark 0 95 | CircleAButtonIsActive 1 96 | CircleAButtonNonPartyPc 0 97 | CircleAButtonParty 1 98 | CircleAButtonEnemy 0 99 | CircleAButtonAggro 0 100 | CircleAButtonNpcOrObject 0 101 | CircleAButtonMinion 0 102 | CircleAButtonDutyEnemy 0 103 | CircleAButtonPet 0 104 | CircleAButtonAlliance 0 105 | CircleAButtonMark 0 106 | GroundTargetType 1 107 | GroundTargetCursorSpeed 50 108 | TargetCircleType 1 109 | TargetLineType 1 110 | LinkLineType 1 111 | ObjectBorderingType 0 112 | MoveMode 1 113 | HotbarDisp 0 114 | HotbarEmptyVisible 1 115 | HotbarNoneSlotDisp01 0 116 | HotbarNoneSlotDisp02 0 117 | HotbarNoneSlotDisp03 0 118 | HotbarNoneSlotDisp04 0 119 | HotbarNoneSlotDisp05 0 120 | HotbarNoneSlotDisp06 0 121 | HotbarNoneSlotDisp07 0 122 | HotbarNoneSlotDisp08 0 123 | HotbarNoneSlotDisp09 0 124 | HotbarNoneSlotDisp10 0 125 | HotbarNoneSlotDispEX 0 126 | ExHotbarSetting 1 127 | HotbarExHotbarUseSetting 0 128 | HotbarCrossUseEx 1 129 | HotbarCrossUseExDirection 1 130 | HotbarCrossDispType 1 131 | PartyListSoloOff 1 132 | HowTo 1 133 | HousingFurnitureBindConfirm 1 134 | DirectChat 1 135 | CharaParamDisp 1 136 | LimitBreakGaugeDisp 1 137 | ScenarioTreeDisp 1 138 | ScenarioTreeCompleteDisp 0 139 | HotbarCrossDispAlways 1 140 | ExpDisp 1 141 | InventryStatusDisp 1 142 | DutyListDisp 1 143 | NaviMapDisp 1 144 | GilStatusDisp 1 145 | InfoSettingDisp 1 146 | InfoSettingDispType 0 147 | TargetInfoDisp 1 148 | EnemyListDisp 1 149 | FocusTargetDisp 1 150 | ItemDetailDisp 1 151 | ActionDetailDisp 1 152 | DetailTrackingType 1 153 | ToolTipDisp 1 154 | MapPermeationRate 25 155 | MapOperationType 1 156 | PartyListDisp 1 157 | PartyListNameType 0 158 | FlyTextDisp 1 159 | MapPermeationMode 0 160 | AllianceList1Disp 1 161 | AllianceList2Disp 1 162 | TargetInfoSelfBuff 0 163 | PopUpTextDisp 1 164 | ContentsInfoDisp 1 165 | DutyListHideWhenCntInfoDisp 1 166 | DutyListNumDisp 5 167 | InInstanceContentDutyListDisp 0 168 | InPublicContentDutyListDisp 0 169 | ContentsInfoJoiningRequestDisp 1 170 | ContentsInfoJoiningRequestSituationDisp 1 171 | HotbarDispSetNum 1 172 | HotbarDispSetChangeType 1 173 | HotbarDispSetDragType 1 174 | MainCommandType 1 175 | MainCommandDisp 1 176 | MainCommandDragShortcut 1 177 | HotbarDispLookNum 1 178 | -------------------------------------------------------------------------------- /resources/default_dats/GEARSET.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/GEARSET.DAT -------------------------------------------------------------------------------- /resources/default_dats/GS.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/GS.DAT -------------------------------------------------------------------------------- /resources/default_dats/ITEMFDR.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/ITEMFDR.DAT -------------------------------------------------------------------------------- /resources/default_dats/ITEMODR.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/ITEMODR.DAT -------------------------------------------------------------------------------- /resources/default_dats/KEYBIND.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/KEYBIND.DAT -------------------------------------------------------------------------------- /resources/default_dats/LOGFLTR.DAT: -------------------------------------------------------------------------------- 1 | @@@@@ -------------------------------------------------------------------------------- /resources/default_dats/MACRO.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/MACRO.DAT -------------------------------------------------------------------------------- /resources/default_dats/UISAVE.DAT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carriejv/libxivdat/07aedfc94c7447ca44cb5e81983759b402e69bb1/resources/default_dats/UISAVE.DAT -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | comment_width = 120 2 | fn_args_layout = "Compressed" 3 | max_width = 120 -------------------------------------------------------------------------------- /src/dat_error.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | use std::convert::From; 3 | use std::error::Error; 4 | use std::fmt; 5 | use std::io; 6 | use std::num; 7 | 8 | /// Wrapper error for any error related to processing a binary DAT file. 9 | #[derive(Debug)] 10 | pub enum DATError { 11 | /// Attempted to read a byte stream as UTF-8 text when it didn't contain 12 | /// valid UTF-8. 13 | BadEncoding(&'static str), 14 | /// The header data is incorrect. The file is probably not a binary DAT file, 15 | /// but may be a plaintext DAT. 16 | BadHeader(&'static str), 17 | /// Data provided exceeds the maximum length specified in the header or the 18 | /// maximum possible length. 19 | Overflow(&'static str), 20 | /// Data provided is shorter than the content_size specified in the header or 21 | /// the minimum possible length. 22 | Underflow(&'static str), 23 | /// Unexpectedly hit the EOF when attempting to read a block of data. 24 | EndOfFile(&'static str), 25 | /// Wrapper for various `std::io::Error` errors. Represents an error reading or writing a 26 | /// file on disk. 27 | FileIO(io::Error), 28 | /// Attempted to use a type-specific function on the incorrect [`DATType`](crate::dat_type::DATType) 29 | IncorrectType(&'static str), 30 | /// Invalid input for a function 31 | InvalidInput(&'static str), 32 | } 33 | 34 | impl fmt::Display for DATError { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | match self { 37 | DATError::BadEncoding(desc) => write!(f, "Invalid text encoding: {}", desc), 38 | DATError::BadHeader(desc) => write!(f, "Invalid header data: {}", desc), 39 | DATError::Overflow(desc) => write!(f, "Content overflow: {}", desc), 40 | DATError::Underflow(desc) => write!(f, "Content underflow: {}", desc), 41 | DATError::EndOfFile(desc) => write!(f, "Unexpected EOF: {}", desc), 42 | DATError::FileIO(e) => write!(f, "File IO error: {:?}", e.source()), 43 | DATError::IncorrectType(desc) => write!(f, "Incorrect DAT file type: {}", desc), 44 | DATError::InvalidInput(desc) => write!(f, "Invalid input: {}", desc), 45 | } 46 | } 47 | } 48 | 49 | impl Error for DATError { 50 | fn source(&self) -> Option<&(dyn Error + 'static)> { 51 | match *self { 52 | DATError::FileIO(ref e) => Some(e), 53 | _ => None, 54 | } 55 | } 56 | } 57 | 58 | impl From for DATError { 59 | fn from(e: io::Error) -> DATError { 60 | DATError::FileIO(e) 61 | } 62 | } 63 | 64 | impl From for DATError { 65 | fn from(_: num::TryFromIntError) -> DATError { 66 | DATError::Overflow("Could not index full file content on a 16-bit platform.") 67 | } 68 | } 69 | 70 | impl From for DATError { 71 | fn from(_: array::TryFromSliceError) -> DATError { 72 | DATError::BadHeader("Header data is absent or unreadable.") 73 | } 74 | } 75 | 76 | impl From for DATError { 77 | fn from(_: std::str::Utf8Error) -> DATError { 78 | DATError::BadEncoding("Text data block did not contain valid utf-8.") 79 | } 80 | } 81 | 82 | impl From for DATError { 83 | fn from(_: std::string::FromUtf8Error) -> DATError { 84 | DATError::BadEncoding("Text data block did not contain valid utf-8.") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/dat_file.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use std::fs::{File, Metadata, OpenOptions}; 3 | use std::io::{Read, Seek, SeekFrom, Write}; 4 | use std::path::Path; 5 | 6 | use crate::dat_error::DATError; 7 | use crate::dat_type::*; 8 | 9 | /// Header size in bytes. 10 | pub const HEADER_SIZE: u32 = 0x11; 11 | /// Offset of the max_size header value from the actual file size on disk. 12 | /// This value should be added to the `max_size` in the header to produce size of the file on disk. 13 | /// Files have a null-padded "footer" of 15 bytes that cannot be omitted, as well as the 17 byte header. 14 | const MAX_SIZE_OFFSET: u32 = 32; 15 | /// Index of the `file_type` header record. 16 | const INDEX_FILE_TYPE: usize = 0x00; 17 | /// Index of the `max_size` header record. 18 | const INDEX_MAX_SIZE: usize = 0x04; 19 | /// Index of the `content_size` header record. 20 | const INDEX_CONTENT_SIZE: usize = 0x08; 21 | 22 | /// A reference to an open DAT file on the system. This emulates the standard lib 23 | /// [`std::fs::File`] but provides additional DAT-specific functionality. 24 | /// 25 | /// Reads and writes to DAT files are performed only on the data contents of the file. 26 | /// XOR masks are automatically applied as necessary. 27 | /// 28 | /// # Examples 29 | /// ```rust 30 | /// use libxivdat::dat_file::DATFile; 31 | /// use libxivdat::dat_type::DATType; 32 | /// use std::io::Read; 33 | /// 34 | /// let mut dat_file = match DATFile::open("./resources/TEST_XOR.DAT") { 35 | /// Ok(dat_file) => dat_file, 36 | /// Err(_) => panic!("Something broke!") 37 | /// }; 38 | /// 39 | /// match dat_file.file_type() { 40 | /// DATType::Macro => { 41 | /// let mut macro_bytes = vec![0u8; dat_file.content_size() as usize - 1]; 42 | /// match dat_file.read(&mut macro_bytes) { 43 | /// Ok(count) => println!("Read {} bytes.", count), 44 | /// Err(_) => panic!("Reading broke!") 45 | /// } 46 | /// }, 47 | /// _ => panic!("Not a macro file!") 48 | /// }; 49 | /// ``` 50 | #[derive(Debug)] 51 | pub struct DATFile { 52 | /// Size in bytes of the readable content of the DAT file. This size includes a trailing null byte. 53 | /// The size of readable content is 1 less than this value. 54 | content_size: u32, 55 | /// Type of the file. This will be inferred from the header when converting directly from a `File`. 56 | file_type: DATType, 57 | /// A single byte that marks the end of the header. This is `0xFF` for most DAT files, but occasionally varies. 58 | /// The purpose of this byte is unknown. 59 | header_end_byte: u8, 60 | /// Maximum allowed size of the content in bytes. The writeable size is 1 byte less than this value. 61 | /// Excess available space not used by content is null padded. 62 | /// 63 | /// Altering this value from the defaults provided for each file type may 64 | /// produce undefined behavior in the game client. 65 | max_size: u32, 66 | /// The underlying [`std::fs::File`]. 67 | raw_file: File, 68 | } 69 | 70 | impl Read for DATFile { 71 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 72 | // Limit read size to content region of the DAT file. 73 | let cur_pos = self.stream_position()? as u32; 74 | let max_end = self.content_size - 1; 75 | let read_end = match u32::try_from(buf.len()) { 76 | Ok(safe_buf_len) if cur_pos + safe_buf_len < max_end => cur_pos + safe_buf_len, 77 | // Maximum read extent should be the last byte of content (excluding the terminating null). 78 | _ => max_end, 79 | }; 80 | // read_len can never be larger than the input buffer len 81 | let read_len = (read_end - cur_pos) as usize; 82 | if read_len < 1 { 83 | return Ok(0); 84 | } 85 | // Initialize a temporary buffer used for applying masks 86 | let mut internal_buf = vec![0u8; read_len]; 87 | let count = self.raw_file.read(&mut internal_buf)?; 88 | // Apply XOR mask to content data if needed. 89 | if let Some(mask_val) = get_mask_for_type(&self.file_type) { 90 | for byte in internal_buf.iter_mut() { 91 | *byte ^= mask_val; 92 | } 93 | } 94 | // Copy internal buffer into input buffer. 95 | buf[..read_len].clone_from_slice(&internal_buf); 96 | Ok(count) 97 | } 98 | } 99 | 100 | impl Seek for DATFile { 101 | fn seek(&mut self, pos: SeekFrom) -> Result { 102 | let cursor = match pos { 103 | // Match `File` behavior of complaining if cursor goes negative relative to start. 104 | SeekFrom::Current(offset) => { 105 | if self.raw_file.stream_position()? as i64 + offset < HEADER_SIZE as i64 { 106 | return Err(std::io::Error::new( 107 | std::io::ErrorKind::InvalidInput, 108 | "Invalid argument", 109 | )); 110 | } else { 111 | self.raw_file.seek(pos)? 112 | } 113 | } 114 | // Treat content end as EOF and seek backwards from there 115 | SeekFrom::End(offset) => self.raw_file.seek(SeekFrom::End( 116 | offset 117 | - (self.max_size as i64 - self.content_size as i64) 118 | - (MAX_SIZE_OFFSET as i64 - HEADER_SIZE as i64) 119 | - 1, 120 | ))?, 121 | // Just offset the seek, treating first content byte as 0. 122 | SeekFrom::Start(offset) => self.raw_file.seek(SeekFrom::Start(HEADER_SIZE as u64 + offset))?, 123 | }; 124 | Ok(cursor - HEADER_SIZE as u64) 125 | } 126 | } 127 | 128 | impl Write for DATFile { 129 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 130 | // Get current cursor position for length checking. 131 | let content_cursor = self.stream_position()? as u32; 132 | 133 | // A buf longer than u32 max is always too long 134 | let buf_len = match u32::try_from(buf.len()) { 135 | Ok(len) => len, 136 | Err(_) => { 137 | return Err(std::io::Error::new( 138 | std::io::ErrorKind::InvalidInput, 139 | DATError::Overflow("Content too long to write."), 140 | )) 141 | } 142 | }; 143 | 144 | // Update content size if necessary 145 | // A content size > u32 max is always too long. 146 | match content_cursor.checked_add(buf_len + 1) { 147 | Some(new_content_size) if new_content_size > self.content_size => { 148 | // A content size > max size is too long 149 | if new_content_size > self.max_size { 150 | return Err(std::io::Error::new( 151 | std::io::ErrorKind::InvalidInput, 152 | DATError::Overflow("Content size would exdeed maximum size after write."), 153 | )); 154 | } 155 | // Write the new content size 156 | self.write_content_size_header(new_content_size)?; 157 | } 158 | Some(_) => (), 159 | None => { 160 | return Err(std::io::Error::new( 161 | std::io::ErrorKind::InvalidInput, 162 | DATError::Overflow("Content size would exceed maximum possible size (u32::MAX) after write."), 163 | )) 164 | } 165 | }; 166 | 167 | // Copy write buffer and apply XOR mask if needed. 168 | match get_mask_for_type(&self.file_type) { 169 | Some(mask_val) => { 170 | let mut masked_bytes = vec![0u8; buf.len()]; 171 | masked_bytes.copy_from_slice(buf); 172 | for byte in masked_bytes.iter_mut() { 173 | *byte ^= mask_val; 174 | } 175 | Ok(self.raw_file.write(&masked_bytes)?) 176 | } 177 | None => Ok(self.raw_file.write(buf)?), 178 | } 179 | } 180 | 181 | fn flush(&mut self) -> std::io::Result<()> { 182 | self.raw_file.flush() 183 | } 184 | } 185 | 186 | impl DATFile { 187 | /// Returns the size of the current content contained in the DAT file. 188 | /// DAT files store content as a null-terminated CString, so this size 189 | /// is one byte larger than the actual content. 190 | /// 191 | /// # Examples 192 | /// 193 | /// ```rust 194 | /// use libxivdat::dat_file::DATFile; 195 | /// 196 | /// let mut dat_file = DATFile::open("./resources/TEST.DAT").unwrap(); 197 | /// let content_size = dat_file.content_size(); 198 | /// ``` 199 | pub fn content_size(&self) -> u32 { 200 | self.content_size 201 | } 202 | 203 | /// Creates a new DAT file with an empty content block in read/write mode. 204 | /// This will truncate an existing file if one exists at the specified path. 205 | /// 206 | /// By default, this will use the default max size for the specified type from 207 | /// [`get_default_max_size_for_type()`](crate::dat_type::get_default_max_size_for_type()) and 208 | /// default header ending from [`get_default_end_byte_for_type()`](crate::dat_type::get_default_end_byte_for_type()). 209 | /// To cicumvent this behavior, you can use [`create_unsafe()`](Self::create_unsafe()`). Note 210 | /// that DAT files with nonstandard sizes may produce undefined behavior in the game client. 211 | /// 212 | /// # Errors 213 | /// 214 | /// If an I/O error creating the file occurs, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 215 | /// error will be returned wrapping the underlying FS error. 216 | /// 217 | /// # Examples 218 | /// 219 | /// ```rust 220 | /// use libxivdat::dat_file::DATFile; 221 | /// use libxivdat::dat_type::DATType; 222 | /// # extern crate tempfile; 223 | /// # use tempfile::tempdir; 224 | /// # let temp_dir = tempdir().unwrap(); 225 | /// # let path = temp_dir.path().join("TEST.DAT"); 226 | /// 227 | /// let mut dat_file = DATFile::create(&path, DATType::Macro); 228 | /// ``` 229 | pub fn create>(path: P, dat_type: DATType) -> Result { 230 | let max_size = get_default_max_size_for_type(&dat_type).unwrap_or(0); 231 | let end_byte = get_default_end_byte_for_type(&dat_type).unwrap_or(0); 232 | Self::create_unsafe(path, dat_type, 1, max_size, end_byte) 233 | } 234 | 235 | /// Creates a new DAT file with a null-padded content bock of the specifed size in read/write mode. 236 | /// This will truncate an existing file if one exists at the specified path. 237 | /// 238 | /// This function allows a custom, not-necessarily-valid maximum length and end byte to be set. Note 239 | /// that DAT files with nonstandard sizes and headers may produce undefined behavior in the game client. 240 | /// 241 | /// # Errors 242 | /// 243 | /// If an I/O error creating the file occurs, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 244 | /// error will be returned wrapping the underlying FS error. 245 | /// 246 | /// # Examples 247 | /// 248 | /// ```rust 249 | /// use libxivdat::dat_file::DATFile; 250 | /// use libxivdat::dat_type::DATType; 251 | /// 252 | /// # extern crate tempfile; 253 | /// # use tempfile::tempdir; 254 | /// # let temp_dir = tempdir().unwrap(); 255 | /// # let path = temp_dir.path().join("TEST.DAT"); 256 | /// 257 | /// // Create an empty (content length 1) macro file with a custom max size and end byte. This probably isn't valid. 258 | /// let mut dat_file = DATFile::create_unsafe(&path, DATType::Macro, 1, 1024, 0x01); 259 | /// ``` 260 | pub fn create_unsafe>( 261 | path: P, dat_type: DATType, content_size: u32, max_size: u32, end_byte: u8, 262 | ) -> Result { 263 | // Create a minimal content size 0 DAT file, then reopen it as a DATFile. 264 | { 265 | let mut raw_file = File::create(&path)?; 266 | raw_file.set_len((max_size + MAX_SIZE_OFFSET) as u64)?; 267 | // Write header type 268 | raw_file.seek(SeekFrom::Start(INDEX_FILE_TYPE as u64))?; 269 | raw_file.write_all(&(dat_type as i32).to_le_bytes())?; 270 | // Write header max_size 271 | raw_file.seek(SeekFrom::Start(INDEX_MAX_SIZE as u64))?; 272 | raw_file.write_all(&max_size.to_le_bytes())?; 273 | // Write a content size header of 1 (content size 0 is invalid). 274 | // Real content size is set below and padded apprioriately. 275 | raw_file.seek(SeekFrom::Start(INDEX_CONTENT_SIZE as u64))?; 276 | raw_file.write_all(&1u32.to_le_bytes())?; 277 | // End header 278 | raw_file.seek(SeekFrom::Start(HEADER_SIZE as u64 - 1))?; 279 | raw_file.write_all(&[end_byte])?; 280 | } 281 | let mut dat_file = DATFile::open_options(path, OpenOptions::new().read(true).write(true).create(true))?; 282 | // Write the content block and content_size header. 283 | dat_file.set_content_size(content_size)?; 284 | Ok(dat_file) 285 | } 286 | 287 | /// Creates a new DAT file with a specific content block in read/write mode. 288 | /// This will truncate an existing file if one exists at the specified path. 289 | /// 290 | /// This is shorthand for creating the DAT file, then calling 291 | /// [`write()`](Self::write()). This function also resets the cursor to 292 | /// the beginning of the content block after writing. 293 | /// 294 | /// By default, this will use the default max size for the specified type from 295 | /// [`get_default_max_size_for_type()`](crate::dat_type::get_default_max_size_for_type()) and 296 | /// default header ending from [`get_default_end_byte_for_type()`](crate::dat_type::get_default_end_byte_for_type()). 297 | /// To cicumvent this behavior, you can use [`create_unsafe()`](Self::create_unsafe()`). Note 298 | /// that DAT files with nonstandard sizes may produce undefined behavior in the game client. 299 | /// 300 | /// # Errors 301 | /// 302 | /// If an I/O error creating the file occurs, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 303 | /// error will be returned wrapping the underlying FS error. 304 | /// 305 | /// A [`DATError::Overflow`](crate::dat_error::DATError::Overflow) is returned 306 | /// if the provided content size is too large, or if the content size exceeds the maximum size. 307 | /// 308 | /// # Examples 309 | /// 310 | /// ```rust 311 | /// use libxivdat::dat_file::DATFile; 312 | /// use libxivdat::dat_type::DATType; 313 | /// 314 | /// # extern crate tempfile; 315 | /// # use tempfile::tempdir; 316 | /// # let temp_dir = tempdir().unwrap(); 317 | /// # let path = temp_dir.path().join("TEST.DAT"); 318 | /// 319 | /// let mut dat_file = DATFile::create_with_content(&path, DATType::Macro, b"Not really a macro."); 320 | /// ``` 321 | pub fn create_with_content>(path: P, dat_type: DATType, content: &[u8]) -> Result { 322 | let max_size = get_default_max_size_for_type(&dat_type).unwrap_or(0); 323 | let end_byte = get_default_end_byte_for_type(&dat_type).unwrap_or(0); 324 | let mut dat_file = Self::create_unsafe(path, dat_type, 1, max_size, end_byte)?; 325 | dat_file.write_all(&content)?; 326 | dat_file.seek(SeekFrom::Start(0))?; 327 | Ok(dat_file) 328 | } 329 | 330 | /// Returns the file type of the DAT file. 331 | /// 332 | /// # Examples 333 | /// 334 | /// ```rust 335 | /// use libxivdat::dat_file::DATFile; 336 | /// use libxivdat::dat_type::DATType; 337 | /// 338 | /// let mut dat_file = DATFile::open("./resources/TEST_XOR.DAT").unwrap(); 339 | /// match dat_file.file_type() { 340 | /// DATType::Macro => println!("Macro file!"), 341 | /// _ => panic!("Nope!") 342 | /// } 343 | /// ``` 344 | pub fn file_type(&self) -> DATType { 345 | self.file_type 346 | } 347 | 348 | /// Returns the terminating byte of the DAT file's 349 | /// header. The purpose of this byte is unknown, 350 | /// but it is almost always 0xFF. 351 | /// 352 | /// # Examples 353 | /// 354 | /// ```rust 355 | /// use libxivdat::dat_file::DATFile; 356 | /// 357 | /// let mut dat_file = DATFile::open("./resources/TEST_XOR.DAT").unwrap(); 358 | /// let header_end_byte = dat_file.header_end_byte(); 359 | /// ``` 360 | pub fn header_end_byte(&self) -> u8 { 361 | self.header_end_byte 362 | } 363 | 364 | /// Returns the maximum size allowed for the content block 365 | /// of the DAT file. Content is stored as a null-terminated CString, 366 | /// so the actual maximum allowed content is 1 byte less than `max_size`. 367 | /// 368 | /// # Examples 369 | /// 370 | /// ```rust 371 | /// use libxivdat::dat_file::DATFile; 372 | /// 373 | /// let mut dat_file = DATFile::open("./resources/TEST_XOR.DAT").unwrap(); 374 | /// let header_end_byte = dat_file.max_size(); 375 | /// ``` 376 | pub fn max_size(&self) -> u32 { 377 | self.max_size 378 | } 379 | 380 | /// Calls [`metadata()`](std::fs::File::sync_all()) on the underlying [`std::fs::File`]. 381 | /// 382 | /// # Errors 383 | /// 384 | /// This function will return any underling I/O errors as a 385 | /// [`DATError::FileIO`](crate::dat_error::DATError::FileIO). 386 | pub fn metadata(&self) -> Result { 387 | Ok(self.raw_file.metadata()?) 388 | } 389 | 390 | /// Attempts to open a DAT file in read-only mode. 391 | /// To set different file access with [`OpenOptions`](std::fs::OpenOptions), 392 | /// use [`open_options()`](Self::open_options()) 393 | /// 394 | /// # Errors 395 | /// 396 | /// If an I/O error opening the file occurs, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 397 | /// error will be returned wrapping the underlying FS error. 398 | /// 399 | /// A [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) will be returned if the header 400 | /// cannot be validated, indicating a non-DAT or corrupt file. 401 | /// 402 | /// # Examples 403 | /// 404 | /// ```rust 405 | /// use libxivdat::dat_file::DATFile; 406 | /// 407 | /// let mut dat_file = DATFile::open("./resources/TEST.DAT"); 408 | /// ``` 409 | pub fn open>(path: P) -> Result { 410 | let mut raw_file = File::open(path)?; 411 | let mut header_bytes = [0u8; HEADER_SIZE as usize]; 412 | raw_file.read_exact(&mut header_bytes)?; 413 | let (file_type, max_size, content_size, header_end_byte) = get_header_contents(&header_bytes)?; 414 | Ok(DATFile { 415 | content_size, 416 | file_type, 417 | header_end_byte, 418 | max_size, 419 | raw_file, 420 | }) 421 | } 422 | 423 | /// Attempts to open a DAT file using an [`OpenOptions`](std::fs::OpenOptions) builder. 424 | /// A reference to the `OpenOptions` struct itself should be passed in, not the `File` it opens. 425 | /// Do not end the options chain with `open("foo.txt")` as with opening a standard file. 426 | /// 427 | /// # Errors 428 | /// 429 | /// If an I/O error opening the file occurs, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 430 | /// error will be returned wrapping the underlying FS error. 431 | /// 432 | /// A [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) will be returned if the header 433 | /// cannot be validated, indicating a non-DAT or corrupt file. 434 | /// 435 | /// # Examples 436 | /// 437 | /// ```rust 438 | /// use libxivdat::dat_file::DATFile; 439 | /// use std::fs::OpenOptions; 440 | /// 441 | /// let mut open_opts = OpenOptions::new(); 442 | /// open_opts.read(true).write(true); 443 | /// let mut dat_file = DATFile::open_options("./resources/TEST.DAT", &mut open_opts); 444 | /// ``` 445 | pub fn open_options>(path: P, options: &mut OpenOptions) -> Result { 446 | let mut raw_file = options.open(path)?; 447 | let mut header_bytes = [0u8; HEADER_SIZE as usize]; 448 | raw_file.read_exact(&mut header_bytes)?; 449 | let (file_type, max_size, content_size, header_end_byte) = get_header_contents(&header_bytes)?; 450 | Ok(DATFile { 451 | content_size, 452 | file_type, 453 | header_end_byte, 454 | max_size, 455 | raw_file, 456 | }) 457 | } 458 | 459 | /// Truncates or extends the readable content section of the DAT file. 460 | /// This emulates the behavior of [`std::fs::File::set_len()`], but only 461 | /// operates on the content region of the DAT file. Because DAT files store 462 | /// content as null-terminated CStrings, the actual writeable space will be 463 | /// one byte less than specified. 464 | /// 465 | /// # Errors 466 | /// 467 | /// This function will return any underling I/O errors as a 468 | /// [`DATError::FileIO`](crate::dat_error::DATError::FileIO). 469 | /// 470 | /// Additionally, it may return a [`DATError::Overflow`](crate::dat_error::DATError::Overflow) 471 | /// error if the new content size would exceed the maximum allowed size. This size may be adjusted using 472 | /// [`set_max_size()`](Self::set_max_size()), but modifying it may not produce a valid file for the game client. 473 | pub fn set_content_size(&mut self, new_size: u32) -> Result<(), DATError> { 474 | // Quick noop for no change 475 | if new_size == self.content_size { 476 | return Ok(()); 477 | } 478 | // Check for valid size 479 | if new_size == 0 { 480 | return Err(DATError::InvalidInput("Content size must be > 0.")); 481 | } 482 | if new_size > self.max_size { 483 | return Err(DATError::Overflow("Content size would exceed maximum size.")); 484 | } 485 | // Save pre-run cursor. 486 | let pre_cursor = self.raw_file.seek(SeekFrom::Current(0))?; 487 | // For shrinks, fill with actual null bytes starting at new content end. 488 | // For grows, pad with the the content mask byte (null ^ mask) starting at old content end to new end. 489 | let (padding_byte, write_size) = if new_size > self.content_size { 490 | self.seek(SeekFrom::End(0))?; 491 | ( 492 | get_mask_for_type(&self.file_type).unwrap_or(0), 493 | new_size - self.content_size, 494 | ) 495 | } else { 496 | self.seek(SeekFrom::Start(new_size as u64))?; 497 | (0, self.content_size - new_size) 498 | }; 499 | // Handle having to write in chunks for usize = 16. 500 | match usize::try_from(write_size) { 501 | Ok(safe_write_size) => { 502 | self.raw_file.write_all(&vec![padding_byte; safe_write_size])?; 503 | } 504 | Err(_) => { 505 | let mut remaining_bytes = write_size; 506 | loop { 507 | match usize::try_from(remaining_bytes) { 508 | Ok(safe_write_size) => { 509 | self.raw_file.write_all(&vec![padding_byte; safe_write_size])?; 510 | break; 511 | } 512 | Err(_) => { 513 | self.raw_file.write_all(&vec![padding_byte; usize::MAX])?; 514 | remaining_bytes -= usize::MAX as u32; 515 | } 516 | }; 517 | } 518 | } 519 | } 520 | // Write the new content size to the header 521 | self.write_content_size_header(new_size)?; 522 | // Reset file cursor 523 | self.raw_file.seek(SeekFrom::Start(pre_cursor))?; 524 | Ok(()) 525 | } 526 | 527 | /// Truncates or extends the full DAT file. 528 | /// This emulates the behavior of [`std::fs::File::set_len()`]. 529 | /// Because DAT files store content as null-terminated CStrings, 530 | /// the actual useable space will be one byte less than specified. 531 | /// 532 | /// Files with a non-default maximum size may cause undefined behavior in the game client. 533 | /// 534 | /// # Errors 535 | /// 536 | /// This function will return any underling I/O errors as a 537 | /// [`DATError::FileIO`](crate::dat_error::DATError::FileIO). 538 | /// 539 | /// A [`DATError::Overflow`](crate::dat_error::DATError::Overflow) is returned 540 | /// if the maximum size would be shorter than the content size after shrinking. To correct this, 541 | /// first [`set_content_size()`](Self::set_content_size()). 542 | pub fn set_max_size(&mut self, new_size: u32) -> Result<(), DATError> { 543 | // Quick noop for no change 544 | if new_size == self.max_size { 545 | return Ok(()); 546 | } 547 | if new_size == 0 { 548 | return Err(DATError::InvalidInput("Content size must be > 0.")); 549 | } 550 | // Check for valid size 551 | if new_size < self.content_size { 552 | return Err(DATError::Overflow("Content size would exceed maximum size.")); 553 | } 554 | // Safe to resize 555 | self.raw_file.set_len((new_size + MAX_SIZE_OFFSET) as u64)?; 556 | // Write new max len to header 557 | self.write_max_size_header(new_size)?; 558 | Ok(()) 559 | } 560 | 561 | /// Calls [`sync_all()`](std::fs::File::sync_all()) on the underlying [`std::fs::File`]. 562 | /// 563 | /// # Errors 564 | /// 565 | /// This function will return any underling I/O errors as a 566 | /// [`DATError::FileIO`](crate::dat_error::DATError::FileIO). 567 | pub fn sync_all(&self) -> Result<(), DATError> { 568 | Ok(self.raw_file.sync_all()?) 569 | } 570 | 571 | /// Calls [`sync_data()`](std::fs::File::sync_data()) on the underlying [`std::fs::File`]. 572 | /// 573 | /// # Errors 574 | /// 575 | /// This function will return any underling I/O errors as a 576 | /// [`DATError::FileIO`](crate::dat_error::DATError::FileIO). 577 | pub fn sync_data(&self) -> Result<(), DATError> { 578 | Ok(self.raw_file.sync_data()?) 579 | } 580 | 581 | /// Writes a new content size value to the [`DATFile`](Self) header. 582 | /// This updates both the struct and the header of the file on disk. 583 | /// This does not modify the actual content of the file. 584 | /// 585 | /// This should be used to update the `content_size` after writes that alter it. 586 | /// 587 | /// # Errors 588 | /// 589 | /// May return a [`std::io::Error`] if one is returned by an underlying fs operation. 590 | fn write_content_size_header(&mut self, size: u32) -> Result<(), std::io::Error> { 591 | let pre_cursor = self.raw_file.seek(SeekFrom::Current(0))?; 592 | self.raw_file.seek(SeekFrom::Start(INDEX_CONTENT_SIZE as u64))?; 593 | self.raw_file.write_all(&size.to_le_bytes())?; 594 | self.raw_file.seek(SeekFrom::Start(pre_cursor))?; 595 | self.content_size = size; 596 | Ok(()) 597 | } 598 | 599 | /// Writes a new max size value to the [`DATFile`](Self) header. 600 | /// This updates both the struct and the header of the file on disk. 601 | /// This does not modify the actual size of the file. 602 | /// 603 | /// This should be used to update the `max_size` after writes that alter it. 604 | /// 605 | /// # Errors 606 | /// 607 | /// May return a [`std::io::Error`] if one is returned by an underlying fs operation. 608 | fn write_max_size_header(&mut self, size: u32) -> Result<(), std::io::Error> { 609 | let pre_cursor = self.raw_file.seek(SeekFrom::Current(0))?; 610 | self.raw_file.seek(SeekFrom::Start(INDEX_MAX_SIZE as u64))?; 611 | self.raw_file.write_all(&size.to_le_bytes())?; 612 | self.raw_file.seek(SeekFrom::Start(pre_cursor))?; 613 | self.max_size = size; 614 | Ok(()) 615 | } 616 | } 617 | 618 | /// Checks the [`DATType`] of a DAT file based on the header contents. This should be treated as a best guess, 619 | /// since the header is not fully understood. 620 | /// 621 | /// # Errors 622 | /// 623 | /// If an I/O error occurs while reading the file, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 624 | /// error will be returned wrapping the underlying FS error. 625 | /// 626 | /// A [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) will be returned if the file header 627 | /// cannot be validated, indicating a non-DAT or corrupt file. 628 | /// 629 | /// # Examples 630 | /// 631 | /// ```rust 632 | /// use libxivdat::dat_file::check_type; 633 | /// use libxivdat::dat_type::DATType; 634 | /// 635 | /// let dat_type = check_type("./resources/TEST_XOR.DAT").unwrap(); 636 | /// assert_eq!(dat_type, DATType::Macro); 637 | /// ``` 638 | pub fn check_type>(path: P) -> Result { 639 | let dat_file = DATFile::open(path)?; 640 | Ok(dat_file.file_type()) 641 | } 642 | 643 | /// Tries to read an 0x11 length byte array as a DAT file header. 644 | /// Returns a tuple containing (`file_type`, `max_size`, `content_size`, `header_end_byte`). 645 | /// 646 | /// File type is inferred using known static values in the header, but the actual purpose of these bytes 647 | /// is unknown. Inferred type should be treated as a best guess. 648 | /// 649 | /// # Errors 650 | /// This function will return a [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) if the data is not a valid header. 651 | /// 652 | /// # Examples 653 | /// ```rust 654 | /// use libxivdat::dat_file::{get_header_contents, HEADER_SIZE}; 655 | /// use std::fs::File; 656 | /// use std::io::Read; 657 | /// 658 | /// let mut header_bytes = [0u8; HEADER_SIZE as usize]; 659 | /// let mut file = File::open("./resources/TEST.DAT").unwrap(); 660 | /// file.read(&mut header_bytes).unwrap(); 661 | /// let (file_type, max_size, content_size, end_byte) = get_header_contents(&mut header_bytes).unwrap(); 662 | /// ``` 663 | /// 664 | /// # Data Structure 665 | /// ```text 666 | /// 0 1 667 | /// 0 1 2 3 4 5 6 7 8 9 a b c d e f 0 668 | /// |-+-++-+-| |-+-++-+-| |-+-++-+-| |-+-++-+-| | 669 | /// | | | | \_ u8 header_end_byte 670 | /// | | | | 0xFF for all ^0x73 files, unique static values for ^0x31 671 | /// | | | \_ null 672 | /// | | | reserved? 673 | /// | | \_ u32le content_size 674 | /// | | (includes terminating null byte) 675 | /// | \_ u32le max_size 676 | /// | max content_size allowed; size on disk - 32 -> 17 byte header + minimum 15-byte null pad footer 677 | /// \_ u32le file_type 678 | /// constant value(s) per file type; probably actually 2 distinct bytes -> always 679 | /// ``` 680 | pub fn get_header_contents(header: &[u8; HEADER_SIZE as usize]) -> Result<(DATType, u32, u32, u8), DATError> { 681 | // If these fail, something is very wrong. 682 | let file_type_id = u32::from_le_bytes(header[INDEX_FILE_TYPE..INDEX_MAX_SIZE].try_into()?); 683 | let max_size = u32::from_le_bytes(header[INDEX_MAX_SIZE..INDEX_CONTENT_SIZE].try_into()?); 684 | let content_size = u32::from_le_bytes(header[INDEX_CONTENT_SIZE..INDEX_CONTENT_SIZE + 4].try_into()?); 685 | let end_byte = header[HEADER_SIZE as usize - 1]; 686 | 687 | // Validate that file type id bytes are present. 688 | if 0xff00ff00 & file_type_id > 0 { 689 | return Err(DATError::BadHeader("File type ID bytes are absent.")); 690 | } 691 | 692 | // Validate that sizes make sense. 693 | if content_size > max_size { 694 | return Err(DATError::BadHeader("Content size exceeds max size in header.")); 695 | } 696 | 697 | Ok((DATType::from(file_type_id), max_size, content_size, end_byte)) 698 | } 699 | 700 | /// Attempts to read the entire content block of a DAT file, returning a byte vector. 701 | /// This is a convenience function similar to [`std::fs::read`] that automatically 702 | /// handles opening and closing the underlying file. 703 | /// 704 | /// # Errors 705 | /// 706 | /// If an I/O error occurs while reading the file, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 707 | /// error will be returned wrapping the underlying FS error. 708 | /// 709 | /// A [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) will be returned if the file header 710 | /// cannot be validated, indicating a non-DAT or corrupt file. 711 | /// 712 | /// A [`DATError::Overflow`](crate::dat_error::DATError::Overflow) is returned if the content 713 | /// would exceed the maximum size specified in the header. 714 | /// 715 | /// On 16-bit platforms, a [`DATError::Overflow`](crate::dat_error::DATError::Overflow) may be returned 716 | /// if the content is too long to fit into a 16-bit vec. Content length can never exceed u32::MAX, so this error 717 | /// is impossible on other platforms. 718 | /// 719 | /// # Examples 720 | /// 721 | /// ```rust 722 | /// use libxivdat::dat_file::read_content; 723 | /// 724 | /// let dat_bytes = read_content("./resources/TEST.DAT").unwrap(); 725 | /// ``` 726 | pub fn read_content>(path: P) -> Result, DATError> { 727 | let mut dat_file = DATFile::open(path)?; 728 | let safe_content_size = usize::try_from(dat_file.content_size - 1)?; 729 | let mut buf = vec![0u8; safe_content_size]; 730 | dat_file.read_exact(&mut buf)?; 731 | Ok(buf) 732 | } 733 | 734 | /// Attempts to write an input buffer as the content block of a DAT File, 735 | /// replacing the entire existing contents and returning the number of bytes written. 736 | /// This is a convenience function that automatically handles opening and closing the underlying file. 737 | /// 738 | /// This will only write to an existing DAT file. Use [`DATFile::create()`](crate::dat_file::DATFile::create()) 739 | /// to create a new DAT file. 740 | /// 741 | /// # Errors 742 | /// 743 | /// If an I/O error occurs while reading the file, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 744 | /// error will be returned wrapping the underlying FS error. 745 | /// 746 | /// A [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) will be returned if the file header 747 | /// cannot be validated, indicating a non-DAT or corrupt file. 748 | /// 749 | /// A [`DATError::Overflow`](crate::dat_error::DATError::Overflow) is returned if the content 750 | /// would exceed the maximum size specified in the header or the maximum possible size (u32::MAX). 751 | /// 752 | /// # Examples 753 | /// 754 | /// ```rust 755 | /// use libxivdat::dat_file::write_content; 756 | /// # use libxivdat::dat_file::DATFile; 757 | /// # use libxivdat::dat_type::DATType; 758 | /// 759 | /// # extern crate tempfile; 760 | /// # use tempfile::tempdir; 761 | /// # let temp_dir = tempdir().unwrap(); 762 | /// # let path = temp_dir.path().join("TEST.DAT"); 763 | /// # DATFile::create(&path, DATType::Macro).unwrap(); 764 | /// 765 | /// write_content(&path, b"Who's awesome? You're awesome!").unwrap(); 766 | /// ``` 767 | pub fn write_content>(path: P, buf: &[u8]) -> Result { 768 | let mut dat_file = DATFile::open_options(path, OpenOptions::new().read(true).write(true))?; 769 | if let Ok(safe_content_size) = u32::try_from(buf.len() + 1) { 770 | if safe_content_size != dat_file.content_size() { 771 | dat_file.set_content_size(safe_content_size)?; 772 | } 773 | Ok(dat_file.write(&buf)?) 774 | } else { 775 | Err(DATError::Overflow( 776 | "Content size would exceed maximum possible size (u32::MAX).", 777 | )) 778 | } 779 | } 780 | 781 | // --- Unit Tests 782 | 783 | #[cfg(test)] 784 | mod tests { 785 | extern crate tempfile; 786 | use tempfile::tempdir; 787 | 788 | use super::*; 789 | use std::fs::copy; 790 | const TEST_PATH: &str = "./resources/TEST.DAT"; 791 | const TEST_XOR_PATH: &str = "./resources/TEST_XOR.DAT"; 792 | const TEST_EMPTY_PATH: &str = "./resources/TEST_EMPTY.DAT"; 793 | const TEST_CONTENTS: &[u8; 5] = b"Boop!"; 794 | const TEST_XOR_CONTENTS: &[u8; 6] = b"Macro!"; 795 | 796 | // --- Module Functions 797 | 798 | #[test] 799 | fn test_check_type() -> Result<(), String> { 800 | match check_type(TEST_XOR_PATH) { 801 | Ok(dat_type) => Ok(assert_eq!(dat_type, DATType::Macro)), 802 | Err(err) => Err(format!("Read error: {}", err)), 803 | } 804 | } 805 | 806 | #[test] 807 | fn test_get_header_contents() -> Result<(), String> { 808 | let header_bytes = [ 809 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 810 | ]; 811 | let (dat_type, max_size, content_size, end_byte) = match get_header_contents(&header_bytes) { 812 | Ok(res) => (res.0, res.1, res.2, res.3), 813 | Err(err) => return Err(format!("{}", err)), 814 | }; 815 | assert_eq!(dat_type, DATType::Unknown); 816 | assert_eq!(max_size, 2); 817 | assert_eq!(content_size, 2); 818 | assert_eq!(end_byte, 0xFF); 819 | Ok(()) 820 | } 821 | 822 | #[test] 823 | fn test_get_header_contents_error_bad_type() -> Result<(), String> { 824 | let header_bytes = [ 825 | 0x00, 0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 826 | ]; 827 | match get_header_contents(&header_bytes) { 828 | Ok(_) => Err("No error returned.".to_owned()), 829 | Err(err) => match err { 830 | DATError::BadHeader(_) => Ok(()), 831 | _ => Err(format!("Incorrect error: {}", err)), 832 | }, 833 | } 834 | } 835 | 836 | #[test] 837 | fn test_get_header_contents_error_sizes() -> Result<(), String> { 838 | let header_bytes = [ 839 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 840 | ]; 841 | match get_header_contents(&header_bytes) { 842 | Ok(_) => Err("No error returned.".to_owned()), 843 | Err(err) => match err { 844 | DATError::BadHeader(_) => Ok(()), 845 | _ => Err(format!("Incorrect error: {}", err)), 846 | }, 847 | } 848 | } 849 | 850 | #[test] 851 | fn test_read_content() -> Result<(), String> { 852 | match read_content(TEST_PATH) { 853 | Ok(content_bytes) => Ok(assert_eq!(&content_bytes, TEST_CONTENTS)), 854 | Err(err) => Err(format!("Read error: {}", err)), 855 | } 856 | } 857 | 858 | #[test] 859 | fn test_read_content_with_mask() -> Result<(), String> { 860 | match read_content(TEST_XOR_PATH) { 861 | Ok(content_bytes) => Ok(assert_eq!(&content_bytes, TEST_XOR_CONTENTS)), 862 | Err(err) => Err(format!("Read error: {}", err)), 863 | } 864 | } 865 | 866 | #[test] 867 | fn test_write_content() -> Result<(), String> { 868 | // Make a tempfile 869 | let tmp_dir = match tempdir() { 870 | Ok(tmp_dir) => tmp_dir, 871 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 872 | }; 873 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 874 | match copy(TEST_PATH, &tmp_path) { 875 | Ok(_) => (), 876 | Err(err) => return Err(format!("Could not create temp file for testing: {}", err)), 877 | }; 878 | // Write content 879 | let new_content = b"Hi!"; 880 | match write_content(&tmp_path, new_content) { 881 | Ok(_) => (), 882 | Err(err) => return Err(format!("Error writing content: {}", err)), 883 | }; 884 | // Check content 885 | match read_content(&tmp_path) { 886 | Ok(content_bytes) => Ok(assert_eq!(&content_bytes, new_content)), 887 | Err(err) => Err(format!("Error reading file after write: {}", err)), 888 | } 889 | } 890 | 891 | // --- DATFile 892 | 893 | #[test] 894 | fn test_datfile_open() -> Result<(), String> { 895 | match DATFile::open(TEST_PATH) { 896 | Ok(dat_file) => { 897 | assert_eq!(dat_file.content_size(), 6); 898 | assert_eq!(dat_file.max_size, 7); 899 | assert_eq!(dat_file.header_end_byte(), 0xFF); 900 | assert_eq!(dat_file.file_type(), DATType::Unknown); 901 | Ok(()) 902 | } 903 | Err(err) => Err(format!("{}", err)), 904 | } 905 | } 906 | 907 | #[test] 908 | fn test_datfile_open_detect_type() -> Result<(), String> { 909 | match DATFile::open(TEST_XOR_PATH) { 910 | Ok(dat_file) => { 911 | assert_eq!(dat_file.content_size(), 7); 912 | assert_eq!(dat_file.max_size, 8); 913 | assert_eq!(dat_file.header_end_byte(), 0xFF); 914 | assert_eq!(dat_file.file_type(), DATType::Macro); 915 | Ok(()) 916 | } 917 | Err(err) => Err(format!("{}", err)), 918 | } 919 | } 920 | 921 | #[test] 922 | fn test_datfile_open_options() -> Result<(), String> { 923 | let mut opts = std::fs::OpenOptions::new(); 924 | opts.read(true).write(true); 925 | match DATFile::open_options(TEST_PATH, &mut opts) { 926 | Ok(dat_file) => { 927 | assert_eq!(dat_file.content_size(), 6); 928 | assert_eq!(dat_file.max_size, 7); 929 | assert_eq!(dat_file.header_end_byte(), 0xFF); 930 | assert_eq!(dat_file.file_type(), DATType::Unknown); 931 | Ok(()) 932 | } 933 | Err(err) => Err(format!("{}", err)), 934 | } 935 | } 936 | 937 | #[test] 938 | fn test_datfile_create() -> Result<(), String> { 939 | let tmp_dir = match tempdir() { 940 | Ok(tmp_dir) => tmp_dir, 941 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 942 | }; 943 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 944 | match DATFile::create(&tmp_path, DATType::Macro) { 945 | Ok(dat_file) => { 946 | assert_eq!(dat_file.content_size(), 1); 947 | assert_eq!( 948 | dat_file.max_size, 949 | get_default_max_size_for_type(&DATType::Macro).unwrap() 950 | ); 951 | assert_eq!( 952 | dat_file.header_end_byte(), 953 | get_default_end_byte_for_type(&DATType::Macro).unwrap() 954 | ); 955 | assert_eq!(dat_file.file_type(), DATType::Macro); 956 | Ok(()) 957 | } 958 | Err(err) => Err(format!("{}", err)), 959 | } 960 | } 961 | 962 | #[test] 963 | fn test_datfile_create_with_content() -> Result<(), String> { 964 | let tmp_dir = match tempdir() { 965 | Ok(tmp_dir) => tmp_dir, 966 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 967 | }; 968 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 969 | let content = b"Content!"; 970 | match DATFile::create_with_content(&tmp_path, DATType::Macro, content) { 971 | Ok(mut dat_file) => { 972 | assert_eq!(dat_file.content_size(), content.len() as u32 + 1); 973 | assert_eq!( 974 | dat_file.max_size, 975 | get_default_max_size_for_type(&DATType::Macro).unwrap() 976 | ); 977 | assert_eq!( 978 | dat_file.header_end_byte(), 979 | get_default_end_byte_for_type(&DATType::Macro).unwrap() 980 | ); 981 | assert_eq!(dat_file.file_type(), DATType::Macro); 982 | 983 | let mut buf = [0u8; 8]; 984 | match dat_file.read(&mut buf) { 985 | Ok(_) => (), 986 | Err(err) => return Err(format!("Could not read back content: {}", err)), 987 | } 988 | assert_eq!(&buf, content); 989 | 990 | Ok(()) 991 | } 992 | Err(err) => Err(format!("{}", err)), 993 | } 994 | } 995 | 996 | #[test] 997 | fn test_datfile_create_unsafe() -> Result<(), String> { 998 | let tmp_dir = match tempdir() { 999 | Ok(tmp_dir) => tmp_dir, 1000 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1001 | }; 1002 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1003 | match DATFile::create_unsafe(&tmp_path, DATType::Macro, 256, 512, 0) { 1004 | Ok(dat_file) => { 1005 | assert_eq!(dat_file.content_size(), 256); 1006 | assert_eq!(dat_file.max_size, 512); 1007 | assert_eq!(dat_file.header_end_byte(), 0); 1008 | assert_eq!(dat_file.file_type(), DATType::Macro); 1009 | Ok(()) 1010 | } 1011 | Err(err) => Err(format!("{}", err)), 1012 | } 1013 | } 1014 | 1015 | #[test] 1016 | fn test_datfile_set_content_size_grow() -> Result<(), String> { 1017 | let tmp_dir = match tempdir() { 1018 | Ok(tmp_dir) => tmp_dir, 1019 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1020 | }; 1021 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1022 | let mut dat_file = match DATFile::create_with_content(&tmp_path, DATType::Macro, &[34u8; 8]) { 1023 | Ok(dat_file) => dat_file, 1024 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1025 | }; 1026 | match dat_file.set_content_size(17) { 1027 | Ok(_) => { 1028 | assert_eq!(dat_file.content_size(), 17); 1029 | // New content space should be mask^null padded on disk. 1030 | // This is output as null bytes via `read()`, which automatically applies masks. 1031 | let mut buf = [0u8; 16]; 1032 | match dat_file.read(&mut buf) { 1033 | Ok(_) => (), 1034 | Err(err) => return Err(format!("Could not read back content: {}", err)), 1035 | } 1036 | assert_eq!(&buf[..8], &[34u8; 8]); 1037 | assert_eq!(&buf[8..], &[0u8; 8]); 1038 | Ok(()) 1039 | } 1040 | Err(err) => return Err(format!("Error setting content size: {}", err)), 1041 | } 1042 | } 1043 | 1044 | #[test] 1045 | fn test_datfile_set_content_size_shrink() -> Result<(), String> { 1046 | let tmp_dir = match tempdir() { 1047 | Ok(tmp_dir) => tmp_dir, 1048 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1049 | }; 1050 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1051 | let mut dat_file = match DATFile::create_with_content(&tmp_path, DATType::Macro, &[34u8; 8]) { 1052 | Ok(dat_file) => dat_file, 1053 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1054 | }; 1055 | match dat_file.set_content_size(5) { 1056 | Ok(_) => { 1057 | assert_eq!(dat_file.content_size(), 5); 1058 | // Byte 4 should be treated as EOF; the rest of the buffer should remain untouched. 1059 | let mut buf = [1u8; 8]; 1060 | match dat_file.read(&mut buf) { 1061 | Ok(_) => (), 1062 | Err(err) => return Err(format!("Could not read back content: {}", err)), 1063 | } 1064 | assert_eq!(&buf[..4], &[34u8; 4]); 1065 | assert_eq!(&buf[4..], &[1u8; 4]); 1066 | Ok(()) 1067 | } 1068 | Err(err) => return Err(format!("Error setting content size: {}", err)), 1069 | } 1070 | } 1071 | 1072 | #[test] 1073 | fn test_datfile_set_content_size_error_zero_size() -> Result<(), String> { 1074 | let tmp_dir = match tempdir() { 1075 | Ok(tmp_dir) => tmp_dir, 1076 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1077 | }; 1078 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1079 | let mut dat_file = match DATFile::create_with_content(&tmp_path, DATType::Macro, &[34u8; 8]) { 1080 | Ok(dat_file) => dat_file, 1081 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1082 | }; 1083 | match dat_file.set_content_size(0) { 1084 | Ok(_) => Err("No error returned.".to_owned()), 1085 | Err(err) => match err { 1086 | DATError::InvalidInput(_) => Ok(()), 1087 | _ => Err(format!("Incorrect error: {}", err)), 1088 | }, 1089 | } 1090 | } 1091 | 1092 | #[test] 1093 | fn test_datfile_set_content_size_error_over_max() -> Result<(), String> { 1094 | let tmp_dir = match tempdir() { 1095 | Ok(tmp_dir) => tmp_dir, 1096 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1097 | }; 1098 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1099 | let mut dat_file = match DATFile::create_unsafe(&tmp_path, DATType::Unknown, 1, 4, 0) { 1100 | Ok(dat_file) => dat_file, 1101 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1102 | }; 1103 | match dat_file.set_content_size(8) { 1104 | Ok(_) => Err("No error returned.".to_owned()), 1105 | Err(err) => match err { 1106 | DATError::Overflow(_) => Ok(()), 1107 | _ => Err(format!("Incorrect error: {}", err)), 1108 | }, 1109 | } 1110 | } 1111 | 1112 | #[test] 1113 | fn test_datfile_set_max_size_grow() -> Result<(), String> { 1114 | let tmp_dir = match tempdir() { 1115 | Ok(tmp_dir) => tmp_dir, 1116 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1117 | }; 1118 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1119 | let mut dat_file = match DATFile::create_unsafe(&tmp_path, DATType::Macro, 4, 8, 0) { 1120 | Ok(dat_file) => dat_file, 1121 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1122 | }; 1123 | match dat_file.set_max_size(16) { 1124 | Ok(_) => { 1125 | assert_eq!(dat_file.max_size(), 16); 1126 | // File size on disk should equal max size + MAX_SIZE_OFFSET. 1127 | let meta = match dat_file.metadata() { 1128 | Ok(meta) => meta, 1129 | Err(err) => return Err(format!("Could not read back content: {}", err)), 1130 | }; 1131 | assert_eq!(meta.len(), 16 + MAX_SIZE_OFFSET as u64); 1132 | Ok(()) 1133 | } 1134 | Err(err) => return Err(format!("Error setting content size: {}", err)), 1135 | } 1136 | } 1137 | 1138 | #[test] 1139 | fn test_datfile_set_max_size_shrink() -> Result<(), String> { 1140 | let tmp_dir = match tempdir() { 1141 | Ok(tmp_dir) => tmp_dir, 1142 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1143 | }; 1144 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1145 | let mut dat_file = match DATFile::create_unsafe(&tmp_path, DATType::Macro, 4, 8, 0) { 1146 | Ok(dat_file) => dat_file, 1147 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1148 | }; 1149 | match dat_file.set_max_size(6) { 1150 | Ok(_) => { 1151 | assert_eq!(dat_file.max_size(), 6); 1152 | // File size on disk should equal max size + MAX_SIZE_OFFSET. 1153 | let meta = match dat_file.metadata() { 1154 | Ok(meta) => meta, 1155 | Err(err) => return Err(format!("Could not read back content: {}", err)), 1156 | }; 1157 | assert_eq!(meta.len(), 6 + MAX_SIZE_OFFSET as u64); 1158 | Ok(()) 1159 | } 1160 | Err(err) => return Err(format!("Error setting content size: {}", err)), 1161 | } 1162 | } 1163 | 1164 | #[test] 1165 | fn test_datfile_set_max_size_error_zero_size() -> Result<(), String> { 1166 | let tmp_dir = match tempdir() { 1167 | Ok(tmp_dir) => tmp_dir, 1168 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1169 | }; 1170 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1171 | let mut dat_file = match DATFile::create_unsafe(&tmp_path, DATType::Macro, 4, 8, 0) { 1172 | Ok(dat_file) => dat_file, 1173 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1174 | }; 1175 | match dat_file.set_max_size(0) { 1176 | Ok(_) => Err("No error returned.".to_owned()), 1177 | Err(err) => match err { 1178 | DATError::InvalidInput(_) => Ok(()), 1179 | _ => Err(format!("Incorrect error: {}", err)), 1180 | }, 1181 | } 1182 | } 1183 | 1184 | #[test] 1185 | fn test_datfile_set_max_size_error_under_content() -> Result<(), String> { 1186 | let tmp_dir = match tempdir() { 1187 | Ok(tmp_dir) => tmp_dir, 1188 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1189 | }; 1190 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1191 | let mut dat_file = match DATFile::create_unsafe(&tmp_path, DATType::Unknown, 4, 8, 0) { 1192 | Ok(dat_file) => dat_file, 1193 | Err(err) => return Err(format!("Error creating temp file: {}", err)), 1194 | }; 1195 | match dat_file.set_max_size(2) { 1196 | Ok(_) => Err("No error returned.".to_owned()), 1197 | Err(err) => match err { 1198 | DATError::Overflow(_) => Ok(()), 1199 | _ => Err(format!("Incorrect error: {}", err)), 1200 | }, 1201 | } 1202 | } 1203 | 1204 | #[test] 1205 | fn test_datfile_read() -> Result<(), String> { 1206 | let mut dat_file = match DATFile::open(TEST_PATH) { 1207 | Ok(dat_file) => dat_file, 1208 | Err(err) => return Err(format!("Open error: {}", err)), 1209 | }; 1210 | let mut buf = [0u8; 1]; 1211 | match dat_file.read(&mut buf) { 1212 | Ok(_) => Ok(assert_eq!(buf, TEST_CONTENTS[0..1])), 1213 | Err(err) => Err(format!("Read error: {}", err)), 1214 | } 1215 | } 1216 | 1217 | #[test] 1218 | fn test_datfile_read_with_mask() -> Result<(), String> { 1219 | let mut dat_file = match DATFile::open(TEST_XOR_PATH) { 1220 | Ok(dat_file) => dat_file, 1221 | Err(err) => return Err(format!("Open error: {}", err)), 1222 | }; 1223 | let mut buf = [0u8; 1]; 1224 | match dat_file.read(&mut buf) { 1225 | Ok(_) => Ok(assert_eq!(buf, TEST_XOR_CONTENTS[0..1])), 1226 | Err(err) => Err(format!("Read error: {}", err)), 1227 | } 1228 | } 1229 | 1230 | #[test] 1231 | fn test_datfile_read_past_end() -> Result<(), String> { 1232 | let mut dat_file = match DATFile::open(TEST_PATH) { 1233 | Ok(dat_file) => dat_file, 1234 | Err(err) => return Err(format!("Open error: {}", err)), 1235 | }; 1236 | let mut buf = [1u8; 8]; 1237 | match dat_file.read(&mut buf) { 1238 | Ok(_) => { 1239 | assert_eq!(&buf[0..5], TEST_CONTENTS); 1240 | // Bytes past content end should be untouched. 1241 | assert_eq!(buf[5..], [1u8; 3]); 1242 | Ok(()) 1243 | } 1244 | Err(err) => Err(format!("Read error: {}", err)), 1245 | } 1246 | } 1247 | 1248 | #[test] 1249 | fn test_datfile_seek_current() -> Result<(), String> { 1250 | let mut dat_file = match DATFile::open(TEST_PATH) { 1251 | Ok(dat_file) => dat_file, 1252 | Err(err) => return Err(format!("Open error: {}", err)), 1253 | }; 1254 | match dat_file.seek(SeekFrom::Current(1)) { 1255 | // Seek should be 1 byte into content 1256 | Ok(_) => Ok(assert_eq!( 1257 | dat_file.raw_file.stream_position().unwrap(), 1258 | HEADER_SIZE as u64 + 1 1259 | )), 1260 | Err(err) => Err(format!("Seek error: {}", err)), 1261 | } 1262 | } 1263 | 1264 | #[test] 1265 | fn test_datfile_seek_start() -> Result<(), String> { 1266 | let mut dat_file = match DATFile::open(TEST_PATH) { 1267 | Ok(dat_file) => dat_file, 1268 | Err(err) => return Err(format!("Open error: {}", err)), 1269 | }; 1270 | match dat_file.seek(SeekFrom::Start(1)) { 1271 | // Seek should be 1 byte into content 1272 | Ok(_) => Ok(assert_eq!( 1273 | dat_file.raw_file.stream_position().unwrap(), 1274 | HEADER_SIZE as u64 + 1 1275 | )), 1276 | Err(err) => Err(format!("Seek error: {}", err)), 1277 | } 1278 | } 1279 | 1280 | #[test] 1281 | fn test_datfile_seek_end() -> Result<(), String> { 1282 | let mut dat_file = match DATFile::open(TEST_PATH) { 1283 | Ok(dat_file) => dat_file, 1284 | Err(err) => return Err(format!("Open error: {}", err)), 1285 | }; 1286 | match dat_file.seek(SeekFrom::End(-1)) { 1287 | // Seek should be 1 byte from content (end measured without including the terminating null byte) 1288 | Ok(_) => Ok(assert_eq!( 1289 | dat_file.raw_file.stream_position().unwrap(), 1290 | HEADER_SIZE as u64 + dat_file.content_size() as u64 - 2 1291 | )), 1292 | Err(err) => Err(format!("Seek error: {}", err)), 1293 | } 1294 | } 1295 | 1296 | #[test] 1297 | fn test_datfile_seek_current_error_negative() -> Result<(), String> { 1298 | let mut dat_file = match DATFile::open(TEST_PATH) { 1299 | Ok(dat_file) => dat_file, 1300 | Err(err) => return Err(format!("Open error: {}", err)), 1301 | }; 1302 | match dat_file.seek(SeekFrom::Current(-10)) { 1303 | Ok(_) => Err("No error returned.".to_owned()), 1304 | Err(err) => match err.kind() { 1305 | std::io::ErrorKind::InvalidInput => Ok(()), 1306 | _ => Err(format!("Incorrect error: {}", err)), 1307 | }, 1308 | } 1309 | } 1310 | 1311 | #[test] 1312 | fn test_datfile_write() -> Result<(), String> { 1313 | // Make a tempfile 1314 | let tmp_dir = match tempdir() { 1315 | Ok(tmp_dir) => tmp_dir, 1316 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1317 | }; 1318 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1319 | match copy(TEST_PATH, &tmp_path) { 1320 | Ok(_) => (), 1321 | Err(err) => return Err(format!("Could not create temp file for testing: {}", err)), 1322 | }; 1323 | // Open tempfile 1324 | let mut opts = OpenOptions::new(); 1325 | opts.read(true).write(true); 1326 | let mut dat_file = match DATFile::open_options(&tmp_path, &mut opts) { 1327 | Ok(dat_file) => dat_file, 1328 | Err(err) => return Err(format!("Error opening temp file: {}", err)), 1329 | }; 1330 | // Write 1331 | let new_content = b"Hi!"; 1332 | match dat_file.write(new_content) { 1333 | Ok(_) => (), 1334 | Err(err) => return Err(format!("Error writing content: {}", err)), 1335 | }; 1336 | // Seek back to start 1337 | match dat_file.seek(SeekFrom::Start(0)) { 1338 | Ok(_) => (), 1339 | Err(err) => return Err(format!("Error seeking in file: {}", err)), 1340 | } 1341 | // Check content 1342 | match read_content(&tmp_path) { 1343 | Ok(content_bytes) => Ok(assert_eq!(&content_bytes, b"Hi!p!")), 1344 | Err(err) => Err(format!("Error reading file after write: {}", err)), 1345 | } 1346 | } 1347 | 1348 | #[test] 1349 | fn test_datfile_write_extend_content_size() -> Result<(), String> { 1350 | // Make a tempfile 1351 | let tmp_dir = match tempdir() { 1352 | Ok(tmp_dir) => tmp_dir, 1353 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1354 | }; 1355 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1356 | match copy(TEST_EMPTY_PATH, &tmp_path) { 1357 | Ok(_) => (), 1358 | Err(err) => return Err(format!("Could not create temp file for testing: {}", err)), 1359 | }; 1360 | // Open tempfile 1361 | let mut opts = OpenOptions::new(); 1362 | opts.read(true).write(true); 1363 | let mut dat_file = match DATFile::open_options(&tmp_path, &mut opts) { 1364 | Ok(dat_file) => dat_file, 1365 | Err(err) => return Err(format!("Error opening temp file: {}", err)), 1366 | }; 1367 | // Write 1368 | let new_content = b"Long!"; 1369 | match dat_file.write(new_content) { 1370 | Ok(_) => (), 1371 | Err(err) => return Err(format!("Error writing content: {}", err)), 1372 | }; 1373 | // Seek back to start 1374 | match dat_file.seek(SeekFrom::Start(0)) { 1375 | Ok(_) => (), 1376 | Err(err) => return Err(format!("Error seeking in file: {}", err)), 1377 | } 1378 | // Check content 1379 | match read_content(&tmp_path) { 1380 | Ok(content_bytes) => { 1381 | assert_eq!(&content_bytes, new_content); 1382 | assert_eq!(dat_file.content_size(), new_content.len() as u32 + 1); 1383 | Ok(()) 1384 | } 1385 | Err(err) => Err(format!("Error reading file after write: {}", err)), 1386 | } 1387 | } 1388 | 1389 | #[test] 1390 | fn test_datfile_write_error_over_max_size() -> Result<(), String> { 1391 | // Make a tempfile 1392 | let tmp_dir = match tempdir() { 1393 | Ok(tmp_dir) => tmp_dir, 1394 | Err(err) => return Err(format!("Error creating temp dir: {}", err)), 1395 | }; 1396 | let tmp_path = tmp_dir.path().join("TEST.DAT"); 1397 | match copy(TEST_EMPTY_PATH, &tmp_path) { 1398 | Ok(_) => (), 1399 | Err(err) => return Err(format!("Could not create temp file for testing: {}", err)), 1400 | }; 1401 | // Open tempfile 1402 | let mut opts = OpenOptions::new(); 1403 | opts.read(true).write(true); 1404 | let mut dat_file = match DATFile::open_options(&tmp_path, &mut opts) { 1405 | Ok(dat_file) => dat_file, 1406 | Err(err) => return Err(format!("Error opening temp file: {}", err)), 1407 | }; 1408 | // Write 1409 | let new_content = b"Looooooooooooooooooong!"; 1410 | match dat_file.write(new_content) { 1411 | Ok(_) => Err("No error returned.".to_owned()), 1412 | Err(err) => match err.kind() { 1413 | std::io::ErrorKind::InvalidInput => Ok(()), 1414 | _ => Err(format!("Incorrect error: {}", err)), 1415 | }, 1416 | } 1417 | } 1418 | } 1419 | -------------------------------------------------------------------------------- /src/dat_type.rs: -------------------------------------------------------------------------------- 1 | /// Enumeration of known FFXIV DAT file types. 2 | /// The value of each element represents the first 4 header bytes as a little-endian i32. 3 | /// These bytes are known static values that differentiate file types. 4 | /// 5 | /// File types may be referenced using a human readable descriptor -- `DATType::GoldSaucer` -- 6 | /// or the filename used by FFXIV -- `DATType::GS`. These methods are interchangable and considered 7 | /// equivalent. `DATType::GoldSaucer == DATType::GS`. 8 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 9 | pub enum DATType { 10 | /// GEARSET.DAT 11 | Gearset = 0x006b0005, 12 | /// GS.DAT 13 | GoldSaucer = 0x0067000A, 14 | /// HOTBAR.DAT 15 | Hotbar = 0x00040002, 16 | /// ITEMFDR.DAT 17 | ItemFinder = 0x00CA0008, 18 | /// ITEMODR.DAT 19 | ItemOrder = 0x00670007, 20 | /// KEYBIND.DAT 21 | Keybind = 0x00650003, 22 | /// LOGFLTR.DAT 23 | LogFilter = 0x00030004, 24 | /// MACRO.DAT (Character) & MACROSYS.DAT (Global) 25 | Macro = 0x00020001, 26 | /// ACQ.DAT (Acquaintances?) 27 | RecentTells = 0x00640006, 28 | /// UISAVE.DAT 29 | UISave = 0x00010009, 30 | Unknown = 0, 31 | } 32 | 33 | /// Provides alises matching exact file names rather than human-readable descriptors. 34 | impl DATType { 35 | pub const ACQ: DATType = DATType::RecentTells; 36 | pub const GEARSET: DATType = DATType::Gearset; 37 | pub const GS: DATType = DATType::GoldSaucer; 38 | pub const ITEMFDR: DATType = DATType::ItemFinder; 39 | pub const ITEMODR: DATType = DATType::ItemOrder; 40 | pub const KEYBIND: DATType = DATType::Keybind; 41 | pub const LOGFLTR: DATType = DATType::LogFilter; 42 | pub const MACRO: DATType = DATType::Macro; 43 | pub const MACROSYS: DATType = DATType::Macro; 44 | pub const UISAVE: DATType = DATType::UISave; 45 | } 46 | 47 | impl From for DATType { 48 | fn from(x: u32) -> DATType { 49 | match x { 50 | x if x == DATType::Gearset as u32 => DATType::Gearset, 51 | x if x == DATType::GoldSaucer as u32 => DATType::GoldSaucer, 52 | x if x == DATType::Hotbar as u32 => DATType::Hotbar, 53 | x if x == DATType::ItemFinder as u32 => DATType::ItemFinder, 54 | x if x == DATType::ItemOrder as u32 => DATType::ItemOrder, 55 | x if x == DATType::Keybind as u32 => DATType::Keybind, 56 | x if x == DATType::LogFilter as u32 => DATType::LogFilter, 57 | x if x == DATType::Macro as u32 => DATType::Macro, 58 | x if x == DATType::RecentTells as u32 => DATType::RecentTells, 59 | x if x == DATType::UISave as u32 => DATType::UISave, 60 | _ => DATType::Unknown, 61 | } 62 | } 63 | } 64 | 65 | /// Gets the XOR mask used for the contents of a binary DAT file. 66 | /// The mask is applied to only the file data content, not the header, footer, or padding null bytes. 67 | /// Returns `None` if the file is of unknown type or does not have a mask. 68 | /// 69 | /// # Examples 70 | /// ```rust 71 | /// use libxivdat::dat_type::{DATType, get_mask_for_type}; 72 | /// 73 | /// let mask = get_mask_for_type(&DATType::Macro).unwrap(); 74 | /// # let mut raw_macro_bytes = [0u8; 1]; 75 | /// for byte in raw_macro_bytes.iter_mut() { 76 | /// *byte = *byte ^ mask; 77 | /// } 78 | /// ``` 79 | pub fn get_mask_for_type(file_type: &DATType) -> Option { 80 | match file_type { 81 | DATType::Gearset 82 | | DATType::GoldSaucer 83 | | DATType::ItemFinder 84 | | DATType::ItemOrder 85 | | DATType::Keybind 86 | | DATType::Macro 87 | | DATType::RecentTells => Some(0x73), 88 | DATType::Hotbar | DATType::UISave => Some(0x31), 89 | DATType::LogFilter => Some(0x00), 90 | _ => None, 91 | } 92 | } 93 | 94 | /// Gets the default header ending byte for a given DAT type. 95 | /// The purpose of this value is unknown, but it is a fixed value based on file type. 96 | /// Returns `None` if the file is of unknown type. 97 | /// 98 | /// # Examples 99 | /// ```rust 100 | /// use libxivdat::dat_type::{DATType, get_default_end_byte_for_type}; 101 | /// let end_byte = get_default_end_byte_for_type(&DATType::Macro); 102 | /// ``` 103 | pub fn get_default_end_byte_for_type(file_type: &DATType) -> Option { 104 | match file_type { 105 | DATType::Gearset 106 | | DATType::GoldSaucer 107 | | DATType::ItemFinder 108 | | DATType::ItemOrder 109 | | DATType::Keybind 110 | | DATType::Macro 111 | | DATType::RecentTells => Some(0xFF), 112 | DATType::Hotbar => Some(0x31), 113 | DATType::LogFilter => Some(0x00), 114 | DATType::UISave => Some(0x21), 115 | _ => None, 116 | } 117 | } 118 | 119 | /// Gets the default maximum content size of a DAT file for a given type. 120 | /// Returns `None` if the file is of unknown type or has no standard size. 121 | /// 122 | /// # Examples 123 | /// ```rust 124 | /// use libxivdat::dat_type::{DATType, get_default_max_size_for_type}; 125 | /// let max_size = get_default_max_size_for_type(&DATType::Macro).unwrap(); 126 | /// ``` 127 | pub fn get_default_max_size_for_type(file_type: &DATType) -> Option { 128 | match file_type { 129 | DATType::Gearset => Some(44849), 130 | DATType::GoldSaucer => Some(649), 131 | DATType::Hotbar => Some(204800), 132 | DATType::ItemFinder => Some(14030), 133 | DATType::ItemOrder => Some(15193), 134 | DATType::Keybind => Some(20480), 135 | DATType::LogFilter => Some(2048), 136 | DATType::Macro => Some(286720), 137 | DATType::RecentTells => Some(2048), 138 | DATType::UISave => Some(64512), 139 | _ => None, 140 | } 141 | } 142 | 143 | #[cfg(test)] 144 | mod tests { 145 | use super::*; 146 | use std::fs::File; 147 | use std::io::Read; 148 | 149 | const FILE_TYPE_MAP: [(DATType, &str); 9] = [ 150 | (DATType::ACQ, "./resources/default_dats/ACQ.DAT"), 151 | (DATType::GEARSET, "./resources/default_dats/GEARSET.DAT"), 152 | (DATType::GS, "./resources/default_dats/GS.DAT"), 153 | (DATType::ITEMFDR, "./resources/default_dats/ITEMFDR.DAT"), 154 | (DATType::ITEMODR, "./resources/default_dats/ITEMODR.DAT"), 155 | (DATType::KEYBIND, "./resources/default_dats/KEYBIND.DAT"), 156 | (DATType::LOGFLTR, "./resources/default_dats/LOGFLTR.DAT"), 157 | (DATType::MACRO, "./resources/default_dats/MACRO.DAT"), 158 | (DATType::UISAVE, "./resources/default_dats/UISAVE.DAT"), 159 | ]; 160 | 161 | #[test] 162 | fn test_from_header_bytes() -> Result<(), String> { 163 | for case in FILE_TYPE_MAP.iter() { 164 | let mut file = match File::open(case.1) { 165 | Ok(file) => file, 166 | Err(err) => return Err(format!("Error opening file: {}", err)), 167 | }; 168 | let mut buf = [0u8; 4]; 169 | match file.read(&mut buf) { 170 | Ok(_) => (), 171 | Err(err) => return Err(format!("Error reading file: {}", err)), 172 | }; 173 | let id_bytes = u32::from_le_bytes(buf); 174 | assert_eq!(DATType::from(id_bytes), case.0); 175 | } 176 | Ok(()) 177 | } 178 | 179 | #[test] 180 | fn test_get_default_end_byte_for_type() -> Result<(), String> { 181 | for case in FILE_TYPE_MAP.iter() { 182 | match get_default_end_byte_for_type(&case.0) { 183 | Some(_) => (), 184 | None => return Err(format!("No value returned for case {}.", case.1)), 185 | }; 186 | } 187 | Ok(()) 188 | } 189 | 190 | #[test] 191 | fn test_get_default_max_size_for_type() -> Result<(), String> { 192 | for case in FILE_TYPE_MAP.iter() { 193 | match get_default_max_size_for_type(&case.0) { 194 | Some(_) => (), 195 | None => return Err(format!("No value returned for case {}.", case.1)), 196 | }; 197 | } 198 | Ok(()) 199 | } 200 | 201 | #[test] 202 | fn test_get_mask_for_type() -> Result<(), String> { 203 | for case in FILE_TYPE_MAP.iter() { 204 | match get_mask_for_type(&case.0) { 205 | Some(_) => (), 206 | None => return Err(format!("No value returned for case {}.", case.1)), 207 | }; 208 | } 209 | Ok(()) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/high_level.rs: -------------------------------------------------------------------------------- 1 | use crate::dat_error::DATError; 2 | 3 | /// Defines a high-level data struct that can be represented as a writeable byte vector. 4 | pub trait AsBytes { 5 | /// Returns a byte vector representing the struct. This can then be written 6 | /// back to the DAT file on disk. 7 | /// 8 | /// # Errors 9 | /// 10 | /// Returns a [`DATError::Overflow`] if the content of a data block 11 | /// would exceed the maximum allowable length. 12 | /// 13 | /// # Examples 14 | /// 15 | /// ```rust 16 | /// use libxivdat::high_level::AsBytes; 17 | /// use libxivdat::xiv_macro::Macro; 18 | /// use libxivdat::xiv_macro::icon::MacroIcon; 19 | /// 20 | /// let a_macro = Macro::new( 21 | /// "Title".to_string(), 22 | /// vec!["Circle".to_string()], 23 | /// MacroIcon::SymbolCircle 24 | /// ).unwrap(); 25 | /// 26 | /// let bytes = a_macro.as_bytes(); 27 | /// assert!(bytes.is_ok()); 28 | /// ``` 29 | fn as_bytes(&self) -> Result, DATError>; 30 | } 31 | 32 | /// Defines a high-level data struct that can be validated against a spec used by the game client. 33 | pub trait Validate { 34 | /// Validates the struct data against the spec expected by the game client. 35 | /// Returns a [`DATError`] describing the error if validation fails, or [`None`] 36 | /// if validation is successful. 37 | /// 38 | /// # Examples 39 | /// 40 | /// ```rust 41 | /// use libxivdat::high_level::Validate; 42 | /// use libxivdat::xiv_macro::Macro; 43 | /// 44 | /// let a_macro = Macro { 45 | /// icon_id: "0000000".to_string(), 46 | /// icon_key: "000".to_string(), 47 | /// lines: vec![String::new(); 15], 48 | /// title: "Title".to_string() 49 | /// }; 50 | /// assert!(a_macro.validate().is_none()); 51 | /// ``` 52 | /// 53 | /// ```rust 54 | /// use libxivdat::high_level::Validate; 55 | /// use libxivdat::xiv_macro::Macro; 56 | /// 57 | /// let a_macro = Macro { 58 | /// icon_id: "123456".to_string(), 59 | /// icon_key: "XYZ".to_string(), 60 | /// lines: vec![String::new(); 1], 61 | /// title: "Looooooooooooooooong Title".to_string() 62 | /// }; 63 | /// assert!(a_macro.validate().is_some()); 64 | /// ``` 65 | fn validate(&self) -> Option; 66 | } 67 | -------------------------------------------------------------------------------- /src/high_level_modules/macro/icon.rs: -------------------------------------------------------------------------------- 1 | /// Enum of all possible macro icons. This includes only the default macro icons, not icons 2 | /// configured with the `/micon ` command. Internally, the macro data preserves the 3 | /// icon chosen via GUI, ignoring the /micon command. 4 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 5 | pub enum MacroIcon { 6 | /// Default macro icon 7 | DefaultIcon, 8 | /// DPS symbol with number 1 9 | DPS1, 10 | /// DPS symbol with number 2 11 | DPS2, 12 | /// DPS symbol with number 3 13 | DPS3, 14 | /// Tank symbol with number 1 15 | Tank1, 16 | /// Tank symbol with number 2 17 | Tank2, 18 | /// Tank symbol with number 3 19 | Tank3, 20 | /// Healer symbol with number 1 21 | Healer1, 22 | /// Healer symbol with number 2 23 | Healer2, 24 | /// Healer symbol with number 3 25 | Healer3, 26 | /// Crafter symbol with number 1 in purple 27 | CrafterPurple1, 28 | /// Crafter symbol with number 2 in purple 29 | CrafterPurple2, 30 | /// Crafter symbol with number 3 in purple 31 | CrafterPurple3, 32 | /// Crafter symbol with number 1 in yellow 33 | CrafterYellow1, 34 | /// Crafter symbol with number 2 in yellow 35 | CrafterYellow2, 36 | /// Crafter symbol with number 3 in yellow 37 | CrafterYellow3, 38 | /// Crafter symbol with number 1 in green 39 | CrafterGreen1, 40 | /// Crafter symbol with number 2 in green 41 | CrafterGreen2, 42 | /// Crafter symbol with number 3 in green 43 | CrafterGreen3, 44 | /// Hammer with gold border 45 | ItemHammer, 46 | /// Sword with gold border 47 | ItemSword, 48 | /// Shield with gold border 49 | ItemShield, 50 | /// Ring with gold border 51 | ItemRing, 52 | /// Shoes with gold border 53 | ItemShoes, 54 | /// Hat with gold border 55 | ItemHat, 56 | /// Bottle with gold border 57 | ItemBottle, 58 | /// Bread with gold border 59 | ItemBread, 60 | /// Gather symbol with number 1 61 | Gatherer1, 62 | /// Gather symbol with number 2 63 | Gatherer2, 64 | /// Gather symbol with number 3 65 | Gatherer3, 66 | /// The number 0 in gold on a blue background 67 | Number0, 68 | /// The number 1 in gold on a blue background 69 | Number1, 70 | /// The number 2 in gold on a blue background 71 | Number2, 72 | /// The number 3 in gold on a blue background 73 | Number3, 74 | /// The number 4 in gold on a blue background 75 | Number4, 76 | /// The number 5 in gold on a blue background 77 | Number5, 78 | /// The number 6 in gold on a blue background 79 | Number6, 80 | /// The number 7 in gold on a blue background 81 | Number7, 82 | /// The number 8 in gold on a blue background 83 | Number8, 84 | /// The number 9 in gold on a blue background 85 | Number9, 86 | /// The number 10 in gold on a blue background 87 | Number10, 88 | /// The number 0 in blue on a gold background 89 | InverseNumber0, 90 | /// The number 1 in blue on a gold background 91 | InverseNumber1, 92 | /// The number 2 in blue on a gold background 93 | InverseNumber2, 94 | /// The number 3 in blue on a gold background 95 | InverseNumber3, 96 | /// The number 4 in blue on a gold background 97 | InverseNumber4, 98 | /// The number 5 in blue on a gold background 99 | InverseNumber5, 100 | /// The number 6 in blue on a gold background 101 | InverseNumber6, 102 | /// The number 7 in blue on a gold background 103 | InverseNumber7, 104 | /// The number 8 in blue on a gold background 105 | InverseNumber8, 106 | /// The number 9 in blue on a gold background 107 | InverseNumber9, 108 | /// The number 10 in blue on a gold background 109 | InverseNumber10, 110 | /// A gray left arrow symbol 111 | SymbolArrowLeft, 112 | /// A gray right arrow symbol 113 | SymbolArrowRight, 114 | /// A gray up arrow symbol 115 | SymbolArrowUp, 116 | /// A gray down arrow symbol 117 | SymbolArrowDown, 118 | /// A gray circle symbol 119 | SymbolCircle, 120 | /// A gray triangle symbol 121 | SymbolTriangle, 122 | /// A gray square symbol 123 | SymbolSquare, 124 | /// A gray x symbol 125 | SymbolX, 126 | /// A gray no symbol 127 | SymbolNo, 128 | /// A gray warning symbol 129 | SymbolWarning, 130 | /// A gray check mark symbol 131 | SymbolCheck, 132 | /// A gray star symbol 133 | SymbolStar, 134 | /// A gray question mark symbol 135 | SymbolQuestion, 136 | /// A gray exclamation mark symbol 137 | SymbolExclamation, 138 | /// A gray plus symbol 139 | SymbolPlus, 140 | /// A gray minus symbol 141 | SymbolMinus, 142 | /// A gray clock symbol 143 | SymbolClock, 144 | /// A gray light bulb symbol 145 | SymbolBulb, 146 | /// A gray cog wheel / settings symbol 147 | SymbolCog, 148 | /// A gray magnifying glass / search symbol 149 | SymbolSearch, 150 | /// A gray speech bubble symbol 151 | SymbolSpeech, 152 | /// A gray heart symbol 153 | SymbolHeart, 154 | /// A gray spade symbol 155 | SymbolSpade, 156 | /// A gray club symbol 157 | SymbolClub, 158 | /// A gray diamond symbol 159 | SymbolDiamond, 160 | /// A gray dice symbol 161 | SymbolDice, 162 | /// A fire crystal item icon 163 | CrystalFire, 164 | /// An ice crystal item icon 165 | CrystalIce, 166 | /// A wind crystal item icon 167 | CrystalWind, 168 | /// An earth crystal item icon 169 | CrystalEarth, 170 | /// A lightning crystal item icon 171 | CrystalLightning, 172 | /// A water crystal item icon 173 | CrystalWater, 174 | /// A valid macro with no icon; this is technically possible, although the gui doesn't allow it 175 | NoIcon, 176 | } 177 | 178 | /// Returns the [`MacroIcon`] corresponding to the raw values of the key and icon 179 | /// [`Sections`](crate::section::Section) of a macro. 180 | /// 181 | /// `[None]` is only returned if the provided key and id values are completely invalid. 182 | /// Values of "000" and "0000000" (used for undefined macros) are considered valid and return 183 | /// [`MacroIcon::NoIcon`]. 184 | /// 185 | /// # Examples 186 | /// ```rust 187 | /// use libxivdat::xiv_macro::icon::{MacroIcon,macro_icon_from_key_and_id}; 188 | /// 189 | /// let key_val = "014"; 190 | /// let id_val = "00005E9"; 191 | /// assert_eq!(macro_icon_from_key_and_id(key_val, id_val).unwrap(), MacroIcon::ItemHammer); 192 | /// ``` 193 | pub fn macro_icon_from_key_and_id(key: &str, id: &str) -> Option { 194 | match (key, id) { 195 | ("000", "0000000") => Some(MacroIcon::NoIcon), 196 | ("001", "00101D1") => Some(MacroIcon::DefaultIcon), 197 | ("002", "0010235") => Some(MacroIcon::DPS1), 198 | ("003", "0010236") => Some(MacroIcon::DPS1), 199 | ("004", "0010237") => Some(MacroIcon::DPS1), 200 | ("005", "0010249") => Some(MacroIcon::Tank1), 201 | ("006", "001024A") => Some(MacroIcon::Tank2), 202 | ("007", "001024B") => Some(MacroIcon::Tank3), 203 | ("008", "001025D") => Some(MacroIcon::Healer1), 204 | ("009", "001025E") => Some(MacroIcon::Healer2), 205 | ("00A", "001025F") => Some(MacroIcon::Healer3), 206 | ("00B", "00101E5") => Some(MacroIcon::CrafterPurple1), 207 | ("00C", "00101E6") => Some(MacroIcon::CrafterPurple2), 208 | ("00D", "00101E7") => Some(MacroIcon::CrafterPurple3), 209 | ("00E", "00101F9") => Some(MacroIcon::CrafterYellow1), 210 | ("00F", "00101FA") => Some(MacroIcon::CrafterYellow2), 211 | ("010", "00101FB") => Some(MacroIcon::CrafterYellow3), 212 | ("011", "001020D") => Some(MacroIcon::CrafterGreen1), 213 | ("012", "001020E") => Some(MacroIcon::CrafterGreen2), 214 | ("013", "001020F") => Some(MacroIcon::CrafterGreen3), 215 | ("014", "00005E9") => Some(MacroIcon::ItemHammer), 216 | ("015", "000061B") => Some(MacroIcon::ItemSword), 217 | ("016", "000064D") => Some(MacroIcon::ItemShield), 218 | ("017", "000067E") => Some(MacroIcon::ItemRing), 219 | ("018", "00006B1") => Some(MacroIcon::ItemShoes), 220 | ("019", "00006E2") => Some(MacroIcon::ItemHat), 221 | ("01A", "0000715") => Some(MacroIcon::ItemBottle), 222 | ("01B", "0000746") => Some(MacroIcon::ItemBread), 223 | ("01C", "0010221") => Some(MacroIcon::Gatherer1), 224 | ("01D", "0010222") => Some(MacroIcon::Gatherer2), 225 | ("01E", "0010223") => Some(MacroIcon::Gatherer3), 226 | ("01F", "0010271") => Some(MacroIcon::Number0), 227 | ("020", "0010272") => Some(MacroIcon::Number1), 228 | ("021", "0010273") => Some(MacroIcon::Number2), 229 | ("022", "0010274") => Some(MacroIcon::Number3), 230 | ("023", "0010275") => Some(MacroIcon::Number4), 231 | ("024", "0010276") => Some(MacroIcon::Number5), 232 | ("025", "0010277") => Some(MacroIcon::Number6), 233 | ("026", "0010278") => Some(MacroIcon::Number7), 234 | ("027", "0010279") => Some(MacroIcon::Number8), 235 | ("028", "001027A") => Some(MacroIcon::Number9), 236 | ("029", "001027B") => Some(MacroIcon::Number10), 237 | ("02A", "0010285") => Some(MacroIcon::InverseNumber0), 238 | ("02B", "0010286") => Some(MacroIcon::InverseNumber1), 239 | ("02C", "0010287") => Some(MacroIcon::InverseNumber2), 240 | ("02D", "0010288") => Some(MacroIcon::InverseNumber3), 241 | ("02E", "0010289") => Some(MacroIcon::InverseNumber4), 242 | ("02F", "001028A") => Some(MacroIcon::InverseNumber5), 243 | ("030", "001028B") => Some(MacroIcon::InverseNumber6), 244 | ("031", "001028C") => Some(MacroIcon::InverseNumber7), 245 | ("032", "001028D") => Some(MacroIcon::InverseNumber8), 246 | ("033", "001028E") => Some(MacroIcon::InverseNumber9), 247 | ("034", "001028F") => Some(MacroIcon::InverseNumber10), 248 | ("035", "00102FD") => Some(MacroIcon::SymbolArrowLeft), 249 | ("036", "00102FE") => Some(MacroIcon::SymbolArrowRight), 250 | ("037", "00102FF") => Some(MacroIcon::SymbolArrowUp), 251 | ("038", "0010300") => Some(MacroIcon::SymbolArrowDown), 252 | ("039", "0010301") => Some(MacroIcon::SymbolCircle), 253 | ("03A", "0010302") => Some(MacroIcon::SymbolTriangle), 254 | ("03B", "0010303") => Some(MacroIcon::SymbolSquare), 255 | ("03C", "0010304") => Some(MacroIcon::SymbolX), 256 | ("03D", "0010305") => Some(MacroIcon::SymbolNo), 257 | ("03E", "0010306") => Some(MacroIcon::SymbolWarning), 258 | ("03F", "0010307") => Some(MacroIcon::SymbolCheck), 259 | ("040", "0010308") => Some(MacroIcon::SymbolStar), 260 | ("041", "0010309") => Some(MacroIcon::SymbolQuestion), 261 | ("042", "001030A") => Some(MacroIcon::SymbolExclamation), 262 | ("043", "001030B") => Some(MacroIcon::SymbolPlus), 263 | ("044", "001030C") => Some(MacroIcon::SymbolMinus), 264 | ("045", "001030D") => Some(MacroIcon::SymbolClock), 265 | ("046", "001030E") => Some(MacroIcon::SymbolBulb), 266 | ("047", "001030F") => Some(MacroIcon::SymbolCog), 267 | ("048", "0010310") => Some(MacroIcon::SymbolSearch), 268 | ("049", "0010311") => Some(MacroIcon::SymbolSpeech), 269 | ("04A", "0010312") => Some(MacroIcon::SymbolHeart), 270 | ("04B", "0010313") => Some(MacroIcon::SymbolSpade), 271 | ("04C", "0010314") => Some(MacroIcon::SymbolClub), 272 | ("04D", "0010315") => Some(MacroIcon::SymbolDiamond), 273 | ("04E", "0010316") => Some(MacroIcon::SymbolDice), 274 | ("04F", "0004E27") => Some(MacroIcon::CrystalFire), 275 | ("050", "0004E29") => Some(MacroIcon::CrystalIce), 276 | ("051", "0004E2A") => Some(MacroIcon::CrystalWind), 277 | ("052", "0004E2C") => Some(MacroIcon::CrystalEarth), 278 | ("053", "0004E2B") => Some(MacroIcon::CrystalLightning), 279 | ("054", "0004E28") => Some(MacroIcon::CrystalWater), 280 | _ => None, 281 | } 282 | } 283 | 284 | /// Returns the key and id [`Section`](crate::section::Section) contents 285 | /// corresponding to a [`MacroIcon`]. 286 | /// 287 | /// # Examples 288 | /// ```rust 289 | /// use libxivdat::xiv_macro::icon::{MacroIcon,macro_icon_to_key_and_id}; 290 | /// 291 | /// let (key_val, id_val) = macro_icon_to_key_and_id(&MacroIcon::ItemHammer); 292 | /// assert_eq!(key_val, "014"); 293 | /// assert_eq!(id_val, "00005E9"); 294 | /// ``` 295 | pub fn macro_icon_to_key_and_id(macro_icon: &MacroIcon) -> (&str, &str) { 296 | match macro_icon { 297 | MacroIcon::NoIcon => ("000", "0000000"), 298 | MacroIcon::DefaultIcon => ("001", "00101D1"), 299 | MacroIcon::DPS1 => ("002", "0010235"), 300 | MacroIcon::DPS2 => ("003", "0010236"), 301 | MacroIcon::DPS3 => ("004", "0010237"), 302 | MacroIcon::Tank1 => ("005", "0010249"), 303 | MacroIcon::Tank2 => ("006", "001024A"), 304 | MacroIcon::Tank3 => ("007", "001024B"), 305 | MacroIcon::Healer1 => ("008", "001025D"), 306 | MacroIcon::Healer2 => ("009", "001025E"), 307 | MacroIcon::Healer3 => ("00A", "001025F"), 308 | MacroIcon::CrafterPurple1 => ("00B", "00101E5"), 309 | MacroIcon::CrafterPurple2 => ("00C", "00101E6"), 310 | MacroIcon::CrafterPurple3 => ("00D", "00101E7"), 311 | MacroIcon::CrafterYellow1 => ("00E", "00101F9"), 312 | MacroIcon::CrafterYellow2 => ("00F", "00101FA"), 313 | MacroIcon::CrafterYellow3 => ("010", "00101FB"), 314 | MacroIcon::CrafterGreen1 => ("011", "001020D"), 315 | MacroIcon::CrafterGreen2 => ("012", "001020E"), 316 | MacroIcon::CrafterGreen3 => ("013", "001020F"), 317 | MacroIcon::ItemHammer => ("014", "00005E9"), 318 | MacroIcon::ItemSword => ("015", "000061B"), 319 | MacroIcon::ItemShield => ("016", "000064D"), 320 | MacroIcon::ItemRing => ("017", "000067E"), 321 | MacroIcon::ItemShoes => ("018", "00006B1"), 322 | MacroIcon::ItemHat => ("019", "00006E2"), 323 | MacroIcon::ItemBottle => ("01A", "0000715"), 324 | MacroIcon::ItemBread => ("01B", "0000746"), 325 | MacroIcon::Gatherer1 => ("01C", "0010221"), 326 | MacroIcon::Gatherer2 => ("01D", "0010222"), 327 | MacroIcon::Gatherer3 => ("01E", "0010223"), 328 | MacroIcon::Number0 => ("01F", "0010271"), 329 | MacroIcon::Number1 => ("020", "0010272"), 330 | MacroIcon::Number2 => ("021", "0010273"), 331 | MacroIcon::Number3 => ("022", "0010274"), 332 | MacroIcon::Number4 => ("023", "0010275"), 333 | MacroIcon::Number5 => ("024", "0010276"), 334 | MacroIcon::Number6 => ("025", "0010277"), 335 | MacroIcon::Number7 => ("026", "0010278"), 336 | MacroIcon::Number8 => ("027", "0010279"), 337 | MacroIcon::Number9 => ("028", "001027A"), 338 | MacroIcon::Number10 => ("029", "001027B"), 339 | MacroIcon::InverseNumber0 => ("02A", "0010285"), 340 | MacroIcon::InverseNumber1 => ("02B", "0010286"), 341 | MacroIcon::InverseNumber2 => ("02C", "0010287"), 342 | MacroIcon::InverseNumber3 => ("02D", "0010288"), 343 | MacroIcon::InverseNumber4 => ("02E", "0010289"), 344 | MacroIcon::InverseNumber5 => ("02F", "001028A"), 345 | MacroIcon::InverseNumber6 => ("030", "001028B"), 346 | MacroIcon::InverseNumber7 => ("031", "001028C"), 347 | MacroIcon::InverseNumber8 => ("032", "001028D"), 348 | MacroIcon::InverseNumber9 => ("033", "001028E"), 349 | MacroIcon::InverseNumber10 => ("034", "001028F"), 350 | MacroIcon::SymbolArrowLeft => ("035", "00102FD"), 351 | MacroIcon::SymbolArrowRight => ("036", "00102FE"), 352 | MacroIcon::SymbolArrowUp => ("037", "00102FF"), 353 | MacroIcon::SymbolArrowDown => ("038", "0010300"), 354 | MacroIcon::SymbolCircle => ("039", "0010301"), 355 | MacroIcon::SymbolTriangle => ("03A", "0010302"), 356 | MacroIcon::SymbolSquare => ("03B", "0010303"), 357 | MacroIcon::SymbolX => ("03C", "0010304"), 358 | MacroIcon::SymbolNo => ("03D", "0010305"), 359 | MacroIcon::SymbolWarning => ("03E", "0010306"), 360 | MacroIcon::SymbolCheck => ("03F", "0010307"), 361 | MacroIcon::SymbolStar => ("040", "0010308"), 362 | MacroIcon::SymbolQuestion => ("041", "0010309"), 363 | MacroIcon::SymbolExclamation => ("042", "001030A"), 364 | MacroIcon::SymbolPlus => ("043", "001030B"), 365 | MacroIcon::SymbolMinus => ("044", "001030C"), 366 | MacroIcon::SymbolClock => ("045", "001030D"), 367 | MacroIcon::SymbolBulb => ("046", "001030E"), 368 | MacroIcon::SymbolCog => ("047", "001030F"), 369 | MacroIcon::SymbolSearch => ("048", "0010310"), 370 | MacroIcon::SymbolSpeech => ("049", "0010311"), 371 | MacroIcon::SymbolHeart => ("04A", "0010312"), 372 | MacroIcon::SymbolSpade => ("04B", "0010313"), 373 | MacroIcon::SymbolClub => ("04C", "0010314"), 374 | MacroIcon::SymbolDiamond => ("04D", "0010315"), 375 | MacroIcon::SymbolDice => ("04E", "0010316"), 376 | MacroIcon::CrystalFire => ("04F", "0004E27"), 377 | MacroIcon::CrystalIce => ("050", "0004E29"), 378 | MacroIcon::CrystalWind => ("051", "0004E2A"), 379 | MacroIcon::CrystalEarth => ("052", "0004E2C"), 380 | MacroIcon::CrystalLightning => ("053", "0004E2B"), 381 | MacroIcon::CrystalWater => ("054", "0004E28"), 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/high_level_modules/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "macro")] 2 | pub mod r#macro; 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Carrie J Vrtis 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! A Rust library for working with Final Fantasy XIV .DAT files. 16 | //! These files store client-side game config including macros, hotkeys, and ui settings. 17 | //! 18 | //! Libxivdat provides low-level file i/o via [`DATFile`](crate::dat_file::DATFile), 19 | //! a [`std::fs::File`]-like interface that automatically manages the header, footer, and content 20 | //! masking of DAT files. 21 | //! 22 | //! Each DAT file contains unique data structures. Higher-level support for specific file types is 23 | //! implemented on a type-by-type basis as optional features. See the chart below 24 | //! for more information and feature names. 25 | //! 26 | //! # DAT Data Structures 27 | //! 28 | //! Internally, some DAT file content blocks use a variable-length data structure referred to as a [`section`](crate::section) 29 | //! in this library. A section consists of a single UTF-8 char type tag, u16le size, and a null-terminated 30 | //! UTF-8 string. A single resource (ie, a macro) is then comprised of a repeating pattern of sections. 31 | //! Other DAT files use fixed-size resource blocks, with each resource immediately following the last. 32 | //! These are referred to as "Block DATs" below. 33 | //! 34 | //! Some DAT files contain unique binary data that does not follow the "standard" DAT format. Others contain 35 | //! UTF-8 plaintext and are not binary files at all. Support for these files is not currently planned. 36 | //! 37 | //! ## DAT Support Table 38 | //! 39 | //! | Symbol | Description | 40 | //! |--------|-----------------| 41 | //! | ✅ | Full support | 42 | //! | 🌀 | Partial support | 43 | //! | ❌ | No support | 44 | //! 45 | //! | File | Contains | Type | DATFile Read/Write | High Level Module | 46 | //! |--------------------|----------------------------------|------------|--------------------|-------------------| 47 | //! | ACQ.DAT | Recent /tell history | Section | ✅ | 🌀 - `section` | 48 | //! | ADDON.DAT | ? | Unique | ❌ | ❌ | 49 | //! | COMMON.DAT | Character configuration | Plaintext | ❌ | ❌ | 50 | //! | CONTROL0.DAT | Gamepad control config | Plaintext | ❌ | ❌ | 51 | //! | CONTROL1.DAT | Keyboard/mouse control config | Plaintext | ❌ | ❌ | 52 | //! | FFXIV_CHARA_XX.DAT | Character appearance presets | Unique | ❌ | ❌ | 53 | //! | GEARSET.DAT | Gearsets | Block | ✅ | ❌ | 54 | //! | GS.DAT | Gold Saucer config (Triad decks) | Block | ✅ | ❌ | 55 | //! | HOTBAR.DAT | Hotbar layouts | Block | ✅ | ❌ | 56 | //! | ITEMFDR.DAT | "Search for item" indexing? | Block | ✅ | ❌ | 57 | //! | ITEMODR.DAT | Item order in bags | Block | ✅ | ❌ | 58 | //! | KEYBIND.DAT | Keybinds | Section | ✅ | 🌀 - `section` | 59 | //! | LOGFLTR.DAT | Chat log filters? | Block | ✅ | ❌ | 60 | //! | MACRO.DAT | Character-specific macros | Section | ✅ | ✅ - `macro` | 61 | //! | MACROSYS.DAT | System-wide macros | Section | ✅ | ✅ - `macro` | 62 | //! | UISAVE.DAT | UI config | Block | ✅ | ❌ | 63 | //! 64 | //! # Examples: 65 | //! 66 | //! ## Reading a file: 67 | //! 68 | //! ```rust 69 | //! use libxivdat::dat_file::read_content; 70 | //! # let path_to_dat_file = "./resources/TEST.DAT"; 71 | //! let data_vec = read_content(&path_to_dat_file).unwrap(); 72 | //! ``` 73 | //! 74 | //! ## Writing to an existing file: 75 | //! DAT files contain metadata in the header that pertains to how data should be written. 76 | //! Because of this, creating a new DAT file and writing contents are separate steps. 77 | //! 78 | //! ```rust 79 | //! use libxivdat::dat_file::write_content; 80 | //! # use libxivdat::dat_file::DATFile; 81 | //! # use libxivdat::dat_type::DATType; 82 | //! # extern crate tempfile; 83 | //! # use tempfile::tempdir; 84 | //! # let temp_dir = tempdir().unwrap(); 85 | //! # let path_to_dat_file = temp_dir.path().join("TEST.DAT"); 86 | //! # DATFile::create(&path_to_dat_file, DATType::Macro).unwrap(); 87 | //! let data_vec = write_content(&path_to_dat_file, b"This is some data.").unwrap(); 88 | //! ``` 89 | //! 90 | //! ## Creating a new file: 91 | //! 92 | //! ```rust 93 | //! use libxivdat::dat_file::DATFile; 94 | //! use libxivdat::dat_type::DATType; 95 | //! # extern crate tempfile; 96 | //! # use tempfile::tempdir; 97 | //! # let temp_dir = tempdir().unwrap(); 98 | //! # let path_to_dat_file = temp_dir.path().join("TEST.DAT"); 99 | //! DATFile::create_with_content(&path_to_dat_file, DATType::Macro, b"This is some data.").unwrap(); 100 | //! ``` 101 | //! 102 | //! ## File-like access: 103 | //! 104 | //! ```rust 105 | //! use libxivdat::dat_file::read_content; 106 | //! use libxivdat::dat_file::DATFile; 107 | //! use libxivdat::dat_type::DATType; 108 | //! use std::io::{Read,Seek,SeekFrom,Write}; 109 | //! # extern crate tempfile; 110 | //! # use tempfile::tempdir; 111 | //! # let temp_dir = tempdir().unwrap(); 112 | //! # let path_to_dat_file = temp_dir.path().join("TEST.DAT"); 113 | //! # DATFile::create(&path_to_dat_file, DATType::Macro).unwrap(); 114 | //! 115 | //! let mut dat_file = DATFile::open(&path_to_dat_file).unwrap(); 116 | //! 117 | //! let mut first_256_bytes = [0u8; 256]; 118 | //! dat_file.read(&mut first_256_bytes).unwrap(); 119 | //! ``` 120 | 121 | /// Contains the [`DATError`](crate::dat_error::DATError) wrapper error. This error type is used 122 | /// for all functions that do not implement a `std::io` trait. 123 | pub mod dat_error; 124 | /// Contains a generic, low-level tool set for working with any standard binary DAT files. 125 | /// This provides the convenience functions [`read_content()`](crate::dat_file::read_content) 126 | /// and [`write_content()`](crate::dat_file::write_content) as well as the [`std::fs::File`]-like 127 | /// [`DATFile`](crate::dat_file::DATFile) interface. 128 | pub mod dat_file; 129 | /// Contains the enum of all supported file types, [`DATType`](crate::dat_type::DATType) and 130 | /// functions for accessing default header and mask values specific to each type. 131 | pub mod dat_type; 132 | /// Contains general-purpose traits and functions applicable to all high-level, file-type-specific 133 | /// modules such as [`xiv_macro`]. 134 | /// 135 | /// Enabled by feature `high-level`, which is implied by any file type feature. 136 | #[cfg(feature = "high-level")] 137 | pub mod high_level; 138 | /// Contains a generic tool set for working with any section-based binary DAT files. 139 | /// This module contains two equivalent implementations: [`Section`](crate::section::Section), 140 | /// [`read_section()`](crate::section::read_section), and [`read_section_content()`](crate::section::read_section_content) 141 | /// for working with files on disk and [`SectionData`](`crate::section::SectionData), 142 | /// [`as_section()`](crate::section::as_section`), and [`as_section_vec()`](crate::section::as_section_vec) 143 | /// for working with pre-allocated byte arrays. 144 | /// 145 | /// Because sections are variable-length data structures, no functions for writing sections in-place are 146 | /// provided. The recommended approach to writing section-based files is to read the entire file, then 147 | /// write an entirely new content block with [`write_content()`](crate::dat_file::write_content). 148 | pub mod section; 149 | /// Contains the high-level toolkit for working with macro files, `MACRO.DAT` and `MACROSYS.DAT`. 150 | /// This module contains two equivalent implementations: [`Macro`](crate::xiv_macro::Macro), 151 | /// [`read_macro()`](crate::xiv_macro::read_macro), and [`read_macro_content()`](crate::xiv_macro::read_macro_content) 152 | /// for working with files on disk and [`MacroData`](`crate::xiv_macro::MacroData), 153 | /// [`as_macro()`](crate::xiv_macro::as_macro`), and [`as_macro_vec()`](crate::xiv_macro::as_macro_vec) 154 | /// for working with pre-allocated byte arrays and [`SectionData](crate::section::SectionData). 155 | /// 156 | /// Enabled by feature `macro`. 157 | /// 158 | /// # Examples 159 | /// 160 | /// ## Reading a macro file 161 | /// ```rust 162 | /// use libxivdat::xiv_macro::read_macro_content; 163 | /// use libxivdat::xiv_macro::icon::MacroIcon; 164 | /// 165 | /// let macro_contents = read_macro_content("./resources/TEST_MACRO.DAT").unwrap(); 166 | /// 167 | /// assert_eq!(macro_contents[0].title, "0"); 168 | /// assert_eq!(macro_contents[0].lines[0], "DefaultIcon"); 169 | /// assert_eq!(macro_contents[0].get_icon().unwrap(), MacroIcon::DefaultIcon); 170 | /// 171 | /// assert_eq!(macro_contents[1].title, "1"); 172 | /// assert_eq!(macro_contents[1].lines[0], "DPS1"); 173 | /// assert_eq!(macro_contents[1].get_icon().unwrap(), MacroIcon::DPS1); 174 | /// ``` 175 | /// 176 | /// ## Writing a macro file 177 | /// Macro files use variable-length [`Sections`](crate::section::Section) to store data on disk, 178 | /// so it is not possible to easily overwrite a single macro in-place. The recommended approach to modifying 179 | /// macros is to read and write the entire file as a block. 180 | /// ```rust 181 | /// use libxivdat::dat_file::write_content; 182 | /// use libxivdat::xiv_macro::{read_macro_content, to_writeable_bytes, Macro}; 183 | /// use libxivdat::xiv_macro::icon::MacroIcon; 184 | /// # use libxivdat::dat_file::DATFile; 185 | /// # use libxivdat::dat_type::DATType; 186 | /// 187 | /// # extern crate tempfile; 188 | /// # use tempfile::tempdir; 189 | /// # let temp_dir = tempdir().unwrap(); 190 | /// # let out_path = temp_dir.path().join("TEST.DAT"); 191 | /// # DATFile::create(&out_path, DATType::Macro).unwrap(); 192 | /// 193 | /// let mut macro_vec = read_macro_content("./resources/TEST_MACRO.DAT").unwrap(); 194 | /// // Replace macro #0 with a new macro. Macro::new will enforce the game's specs for macros. 195 | /// macro_vec[0] = Macro::new( 196 | /// String::from("libxivdat was here"), 197 | /// vec![String::from("/sh I <3 libxivdat!!!")], 198 | /// MacroIcon::SymbolExclamation 199 | /// ).unwrap(); 200 | /// 201 | /// // Write it back out to a file. to_writeable_bytes will validate every macro in the vector. 202 | /// let out_bytes = to_writeable_bytes(¯o_vec).unwrap(); 203 | /// write_content(&out_path, &out_bytes); 204 | /// ``` 205 | #[cfg(feature = "macro")] 206 | pub mod xiv_macro { 207 | pub use crate::high_level_modules::r#macro::*; 208 | } 209 | /// High-level, file-type-specific submodules container. 210 | mod high_level_modules; 211 | -------------------------------------------------------------------------------- /src/section.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use std::io::{Read, Seek, SeekFrom}; 3 | 4 | use crate::dat_error::DATError; 5 | use crate::dat_file::{check_type, read_content, DATFile}; 6 | use crate::dat_type::DATType; 7 | use std::cmp::Ordering; 8 | use std::path::Path; 9 | use std::str::from_utf8; 10 | 11 | /// Array of [`DATTypes`](crate::dat_type::DATType) that have `Section`-based contents. [`DATType::Unknown`] is allowed, 12 | /// since its contents are not known. 13 | pub const SECTION_BASED_TYPES: [DATType; 5] = [ 14 | DATType::ACQ, 15 | DATType::KEYBIND, 16 | DATType::MACRO, 17 | DATType::MACROSYS, 18 | DATType::Unknown, 19 | ]; 20 | 21 | /// Length of a section header in bytes. 22 | pub const SECTION_HEADER_SIZE: usize = 3; 23 | 24 | /// A `Section` is variable-length data structure common to several binary DAT files. 25 | /// A `Resource` (ie, a Macro or Gearset) is then made out of a repeating pattern of sections. 26 | /// [`Section`] owns its constituent data and is returned from helper functions like [`read_section()`]. 27 | /// To build a section with refrences to a pre-allocated buffer, use [`SectionData`]. 28 | /// 29 | /// # Section-using file types 30 | /// `ACQ`, `KEYBIND`, `MACRO`, and `MACROSYS`. See [`SECTION_BASED_TYPES`]. 31 | /// 32 | /// # Data Structure 33 | /// ```text 34 | /// 0 35 | /// 0 1 2 3 ... 36 | /// | |--| |- ... 37 | /// | | \_ null-terminated utf8 string 38 | /// | \_ u16le content_size 39 | /// \_ utf8 char section_type 40 | /// ``` 41 | #[derive(Clone, Debug, Eq, PartialEq)] 42 | pub struct Section { 43 | /// Data content of the section. 44 | pub content: String, 45 | /// Length of section content in bytes. Includes terminating null. 46 | pub content_size: u16, 47 | /// Single char string data type tag. The meaning of this tag varies by file type. 48 | /// Some tags are reused with different meanings between types. 49 | pub tag: String, 50 | } 51 | 52 | /// A `Section` is variable-length data structure common to several binary DAT files. 53 | /// A `Resource` (ie, a Macro or Gearset) is then made out of a repeating pattern of sections. 54 | /// [`SectionData`] is used to build sections with references to pre-allocated buffers. 55 | /// To build a section that owns its own data, use [`Section`]. 56 | /// 57 | /// # Section-using file types 58 | /// `ACQ`, `KEYBIND`, `MACRO`, and `MACROSYS`. See [`SECTION_BASED_TYPES`]. 59 | /// 60 | /// # Data Structure 61 | /// ```text 62 | /// 0 63 | /// 0 1 2 3 ... 64 | /// | |--| |- ... 65 | /// | | \_ null-terminated utf8 string 66 | /// | \_ u16le content_size 67 | /// \_ utf8 char section_type 68 | /// ``` 69 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 70 | pub struct SectionData<'a> { 71 | /// Data content of the section. 72 | pub content: &'a str, 73 | /// Length of section content in bytes. Includes terminating null. 74 | pub content_size: u16, 75 | /// Single char string data type tag. The meaning of this tag varies by file type. 76 | /// Some tags are reused with different meanings between types. 77 | pub tag: &'a str, 78 | } 79 | 80 | impl From<&SectionData<'_>> for Section { 81 | fn from(x: &SectionData) -> Self { 82 | Section { 83 | content: x.content.to_owned(), 84 | content_size: x.content_size, 85 | tag: x.tag.to_owned(), 86 | } 87 | } 88 | } 89 | 90 | impl From
for Vec { 91 | fn from(x: Section) -> Self { 92 | let tag_bytes = x.tag.as_bytes(); 93 | let content_size_bytes = x.content_size.to_le_bytes(); 94 | let content_bytes = x.content.as_bytes(); 95 | tag_bytes 96 | .iter() 97 | .chain(&content_size_bytes) 98 | .chain(content_bytes) 99 | .chain(&[0u8; 1]) 100 | .copied() 101 | .collect() 102 | } 103 | } 104 | 105 | impl TryFrom<&[u8]> for Section { 106 | type Error = DATError; 107 | fn try_from(x: &[u8]) -> Result { 108 | let header_bytes = x[0..SECTION_HEADER_SIZE].try_into()?; 109 | let (tag, content_size) = get_section_header_contents(&header_bytes)?; 110 | let remaining_buf_size = x.len() - 3; 111 | 112 | match usize::from(content_size).cmp(&remaining_buf_size) { 113 | Ordering::Greater => Err(DATError::Overflow( 114 | "Data buffer is too small for content_size specified in header.", 115 | )), 116 | Ordering::Less => Err(DATError::Underflow( 117 | "Data buffer is too large for content_size specified in header.", 118 | )), 119 | Ordering::Equal => Ok(Section { 120 | content: String::from_utf8(x[3..x.len() - 1].to_vec())?, 121 | content_size, 122 | tag: tag.to_owned(), 123 | }), 124 | } 125 | } 126 | } 127 | 128 | impl<'a> From<&'a Section> for SectionData<'a> { 129 | fn from(x: &'a Section) -> Self { 130 | SectionData { 131 | content: &x.content, 132 | content_size: x.content_size, 133 | tag: &x.tag, 134 | } 135 | } 136 | } 137 | 138 | impl From> for Vec { 139 | fn from(x: SectionData) -> Self { 140 | let tag_bytes = x.tag.as_bytes(); 141 | let content_size_bytes = x.content_size.to_le_bytes(); 142 | let content_bytes = x.content.as_bytes(); 143 | tag_bytes 144 | .iter() 145 | .chain(&content_size_bytes) 146 | .chain(content_bytes) 147 | .chain(&[0u8; 1]) 148 | .copied() 149 | .collect() 150 | } 151 | } 152 | 153 | impl<'a> TryFrom<&'a [u8]> for SectionData<'a> { 154 | type Error = DATError; 155 | fn try_from(x: &'a [u8]) -> Result { 156 | let tag = from_utf8(&x[..1])?; 157 | let content_size = u16::from_le_bytes(x[1..SECTION_HEADER_SIZE].try_into()?); 158 | let remaining_buf_size = x.len() - 3; 159 | 160 | match usize::from(content_size).cmp(&remaining_buf_size) { 161 | Ordering::Greater => Err(DATError::Overflow( 162 | "Data buffer is too small for content_size specified in header.", 163 | )), 164 | Ordering::Less => Err(DATError::Underflow( 165 | "Data buffer is too large for content_size specified in header.", 166 | )), 167 | Ordering::Equal => Ok(SectionData { 168 | content: from_utf8(&x[3..x.len() - 1])?, 169 | content_size, 170 | tag, 171 | }), 172 | } 173 | } 174 | } 175 | 176 | impl Section { 177 | /// Builds a new [`Section`] with a given tag and content 178 | /// 179 | /// # Examples 180 | /// ```rust 181 | /// use libxivdat::section::Section; 182 | /// 183 | /// let new_section = Section::new("T".to_string(), "Macro title!".to_string()).unwrap(); 184 | /// assert_eq!(new_section.tag, "T"); 185 | /// assert_eq!(new_section.content, "Macro title!"); 186 | /// assert_eq!(new_section.content_size, 13); 187 | /// ``` 188 | pub fn new(tag: String, content: String) -> Result { 189 | if tag.len() != 1 { 190 | return Err(DATError::InvalidInput("Tags may only be a single character in length.")); 191 | } 192 | // Include space for terminating null 193 | let content_size = match u16::try_from(content.len() + 1) { 194 | Ok(content_size) => content_size, 195 | Err(_) => { 196 | return Err(DATError::Overflow( 197 | "Section content exceeds maximum possible size (u16::MAX - 1).", 198 | )) 199 | } 200 | }; 201 | Ok(Section { 202 | content, 203 | content_size, 204 | tag, 205 | }) 206 | } 207 | } 208 | 209 | impl<'a> SectionData<'a> { 210 | /// Builds a new [`SectionData`] with a given tag and content 211 | /// 212 | /// # Examples 213 | /// ```rust 214 | /// use libxivdat::section::SectionData; 215 | /// 216 | /// let new_section = SectionData::new("T", "Macro title!").unwrap(); 217 | /// assert_eq!(new_section.tag, "T"); 218 | /// assert_eq!(new_section.content, "Macro title!"); 219 | /// assert_eq!(new_section.content_size, 13); 220 | /// ``` 221 | pub fn new(tag: &'a str, content: &'a str) -> Result { 222 | if tag.len() != 1 { 223 | return Err(DATError::InvalidInput("Tags may only be a single character in length.")); 224 | } 225 | // Include space for terminating null 226 | let content_size = match u16::try_from(content.len() + 1) { 227 | Ok(content_size) => content_size, 228 | Err(_) => { 229 | return Err(DATError::Overflow( 230 | "Section content exceeds maximum possible size (u16::MAX - 1).", 231 | )) 232 | } 233 | }; 234 | Ok(SectionData::<'a> { 235 | content, 236 | content_size, 237 | tag, 238 | }) 239 | } 240 | } 241 | 242 | /// Interprets a byte slice as [`SectionData`]. 243 | /// 244 | /// # Errors 245 | /// 246 | /// Returns a [`DATError::Overflow`](crate::dat_error::DATError::Overflow) or 247 | /// [`DATError::Underflow`](crate::dat_error::DATError::Underflow) if the slice length 248 | /// does not match the content_size specified in the section header. 249 | /// 250 | /// If the tag or content is not valid utf8 text, a [`DATError::BadEncoding`](crate::dat_error::DATError::BadEncoding) 251 | /// error will be returned. 252 | /// 253 | /// # Examples 254 | /// ```rust 255 | /// use libxivdat::dat_file::DATFile; 256 | /// use libxivdat::section::{as_section,SECTION_HEADER_SIZE}; 257 | /// use std::io::Read; 258 | /// 259 | /// let mut dat_file = DATFile::open("./resources/TEST_SECTION.DAT").unwrap(); 260 | /// let mut content_bytes = [0u8; 24 + SECTION_HEADER_SIZE]; 261 | /// dat_file.read_exact(&mut content_bytes).unwrap(); 262 | /// let section = as_section(&content_bytes).unwrap(); 263 | /// 264 | /// assert_eq!(section.tag, "T"); 265 | /// assert_eq!(section.content_size, 24); 266 | /// assert_eq!(section.content, "This is a test section."); 267 | /// ``` 268 | pub fn as_section(bytes: &[u8]) -> Result { 269 | SectionData::try_from(bytes) 270 | } 271 | 272 | /// Interprets a byte slice as a block of [`SectionData`], returning a [`Vec`] of them. 273 | /// 274 | /// # Errors 275 | /// 276 | /// Returns a [`DATError::Overflow`](crate::dat_error::DATError::Overflow) or 277 | /// [`DATError::Underflow`](crate::dat_error::DATError::Underflow) if a section content block 278 | /// does not match the expected length specified in the section header. 279 | /// 280 | /// If the tag or content is not valid utf8 text, a [`DATError::BadEncoding`](crate::dat_error::DATError::BadEncoding) 281 | /// error will be returned. 282 | /// 283 | /// # Examples 284 | /// ```rust 285 | /// use libxivdat::dat_file::read_content; 286 | /// use libxivdat::section::as_section_vec; 287 | /// 288 | /// let content_bytes = read_content("./resources/TEST_SECTION.DAT").unwrap(); 289 | /// let section = as_section_vec(&content_bytes).unwrap(); 290 | /// 291 | /// assert_eq!(section[0].tag, "T"); 292 | /// assert_eq!(section[0].content_size, 24); 293 | /// assert_eq!(section[0].content, "This is a test section."); 294 | /// 295 | /// assert_eq!(section[1].tag, "A"); 296 | /// assert_eq!(section[1].content_size, 22); 297 | /// assert_eq!(section[1].content, "Another test section."); 298 | /// ``` 299 | pub fn as_section_vec<'a>(bytes: &'a [u8]) -> Result>, DATError> { 300 | let mut cursor = 0usize; 301 | let mut res_vec = Vec::>::new(); 302 | let buf_len = bytes.len(); 303 | while cursor < buf_len { 304 | // Read header block 305 | let header_bytes = &bytes[cursor..cursor + SECTION_HEADER_SIZE]; 306 | let (tag, content_size) = get_section_header_contents(header_bytes.try_into()?)?; 307 | cursor += SECTION_HEADER_SIZE; 308 | // Read content block; leave the terminating null out of the content slice 309 | let content_bytes = &bytes[cursor..cursor + usize::from(content_size) - 1]; 310 | cursor += usize::from(content_size); 311 | // Validate content size 312 | if content_bytes.contains(&0u8) { 313 | return Err(DATError::Underflow("Section content ended early.")); 314 | } 315 | if bytes[cursor - 1] != 0u8 { 316 | return Err(DATError::Overflow("Section data did not end at the expected index.")); 317 | } 318 | // Build a section and push to vec 319 | res_vec.push(SectionData::<'a> { 320 | content: from_utf8(content_bytes)?, 321 | content_size, 322 | tag, 323 | }); 324 | } 325 | Ok(res_vec) 326 | } 327 | 328 | /// Tries to read a [`SECTION_HEADER_SIZE`] byte array as a [`Section`] header. 329 | /// Returns a tuple containing (`tag`, `content_size`). 330 | /// 331 | /// # Errors 332 | /// This function will return a [`DATError::BadEncoding`](crate::dat_error::DATError::BadEncoding) if the 333 | /// `tag` is not a valid utf8 character. 334 | /// 335 | /// # Examples 336 | /// ```rust 337 | /// use libxivdat::section::get_section_header_contents; 338 | /// 339 | /// let bytes = [97, 01, 00]; 340 | /// let (tag, content_size) = get_section_header_contents(&bytes).unwrap(); 341 | /// assert_eq!(tag, "a"); 342 | /// assert_eq!(content_size, 1); 343 | /// ``` 344 | pub fn get_section_header_contents(bytes: &[u8; SECTION_HEADER_SIZE]) -> Result<(&str, u16), DATError> { 345 | Ok((from_utf8(&bytes[..1])?, u16::from_le_bytes(bytes[1..].try_into()?))) 346 | } 347 | 348 | /// Reads the next [`Section`] from a [`DATFile`](crate::dat_file::DATFile). 349 | /// 350 | /// # Errors 351 | /// 352 | /// Returns [`DATError::IncorrectType`] if the file appears to be of a [`DATType`] 353 | /// that does not contain sections. 354 | /// 355 | /// Returns [`DATError::EndOfFile`] if there is not a full section remaining in the file. 356 | /// 357 | /// If an I/O error occurs while reading the file, a [`DATError::FileIO`] 358 | /// error will be returned wrapping the underlying FS error. 359 | /// 360 | /// If the tag or content is not valid utf8 text, a [`DATError::BadEncoding`] 361 | /// error will be returned. 362 | /// 363 | /// # Examples 364 | /// ```rust 365 | /// use libxivdat::dat_file::DATFile; 366 | /// use libxivdat::section::read_section; 367 | /// 368 | /// let mut dat_file = DATFile::open("./resources/TEST_SECTION.DAT").unwrap(); 369 | /// let section = read_section(&mut dat_file).unwrap(); 370 | /// 371 | /// assert_eq!(section.tag, "T"); 372 | /// assert_eq!(section.content_size, 24); 373 | /// assert_eq!(section.content, "This is a test section."); 374 | /// 375 | /// ``` 376 | pub fn read_section(dat_file: &mut DATFile) -> Result { 377 | if SECTION_BASED_TYPES.contains(&dat_file.file_type()) { 378 | Ok(read_section_unsafe(dat_file)?) 379 | } else { 380 | Err(DATError::IncorrectType( 381 | "Target file is of a type that should not contain sections.", 382 | )) 383 | } 384 | } 385 | 386 | /// Reads all [`Sections`](Section) from a specified DAT file, returning a [`Vec`] of them. 387 | /// This performs only one read operation on the underlying file, loading the entire content into memory 388 | /// to prevent repeat file access. This is similar to [`read_content()`](crate::dat_file::read_content), 389 | /// but returns a `Vec
` instead of raw bytes. 390 | /// 391 | /// # Errors 392 | /// 393 | /// Returns [`DATError::IncorrectType`] if the file appears to be of a [`DATType`] 394 | /// that does not contain sections. 395 | /// 396 | /// Returns a [`DATError::Overflow`](crate::dat_error::DATError::Overflow) or 397 | /// [`DATError::Underflow`](crate::dat_error::DATError::Underflow) if a section content block 398 | /// does not match the expected length specified in the section header. 399 | /// 400 | /// Returns a [`DATError::BadEncoding`](crate::dat_error::DATError::BadEncoding) if a section does not 401 | /// contain valid utf8 text. 402 | /// 403 | /// Returns a [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) if the specified file does not 404 | /// have a valid DAT header. 405 | /// 406 | /// If an I/O error occurs while reading the file, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 407 | /// error will be returned wrapping the underlying FS error. 408 | /// 409 | /// # Examples 410 | /// ```rust 411 | /// use libxivdat::section::read_section_content; 412 | /// 413 | /// let section = read_section_content("./resources/TEST_SECTION.DAT").unwrap(); 414 | /// 415 | /// assert_eq!(section[0].tag, "T"); 416 | /// assert_eq!(section[0].content_size, 24); 417 | /// assert_eq!(section[0].content, "This is a test section."); 418 | /// 419 | /// assert_eq!(section[1].tag, "A"); 420 | /// assert_eq!(section[1].content_size, 22); 421 | /// assert_eq!(section[1].content, "Another test section."); 422 | /// ``` 423 | pub fn read_section_content>(path: P) -> Result, DATError> { 424 | if SECTION_BASED_TYPES.contains(&check_type(&path)?) { 425 | Ok(read_section_content_unsafe(path)?) 426 | } else { 427 | Err(DATError::IncorrectType( 428 | "Target file is of a type that should not contain sections.", 429 | )) 430 | } 431 | } 432 | 433 | /// Reads the next [`Section`] from a [`DATFile`](crate::dat_file::DATFile). This does not check that the 434 | /// file is of a type that should contain sections. 435 | /// 436 | /// # Errors 437 | /// 438 | /// Returns [`DATError::EndOfFile`] if there is not a full section remaining in the file. 439 | /// 440 | /// If an I/O error occurs while reading the file, a [`DATError::FileIO`] 441 | /// error will be returned wrapping the underlying FS error. 442 | /// 443 | /// If the tag or content is not valid utf8 text, a [`DATError::BadEncoding`] 444 | /// error will be returned. 445 | /// 446 | /// # Examples 447 | /// ```rust 448 | /// use libxivdat::dat_file::DATFile; 449 | /// use libxivdat::section::read_section_unsafe; 450 | /// 451 | /// let mut dat_file = DATFile::open("./resources/TEST_SECTION.DAT").unwrap(); 452 | /// let section = read_section_unsafe(&mut dat_file).unwrap(); 453 | /// 454 | /// assert_eq!(section.tag, "T"); 455 | /// assert_eq!(section.content_size, 24); 456 | /// assert_eq!(section.content, "This is a test section."); 457 | /// 458 | /// ``` 459 | pub fn read_section_unsafe(dat_file: &mut DATFile) -> Result { 460 | // Read section header. 461 | let mut sec_header_bytes = [0u8; SECTION_HEADER_SIZE]; 462 | // Manually wrap EOF into DATError EOF 463 | match dat_file.read_exact(&mut sec_header_bytes) { 464 | Ok(_) => (), 465 | Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => { 466 | return Err(DATError::EndOfFile("Found EOF looking for next section.")) 467 | } 468 | Err(err) => return Err(DATError::from(err)), 469 | }; 470 | let (tag, content_size) = get_section_header_contents(&sec_header_bytes)?; 471 | // Read section content 472 | let mut sec_content_bytes = vec![0u8; usize::from(content_size - 1)]; 473 | match dat_file.read_exact(&mut sec_content_bytes) { 474 | Ok(_) => (), 475 | Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => { 476 | return Err(DATError::EndOfFile("Found EOF reading next section.")) 477 | } 478 | Err(err) => return Err(DATError::from(err)), 479 | }; 480 | // Skip null byte. Doing it this way avoids having to re-slice content bytes. 481 | dat_file.seek(SeekFrom::Current(1))?; 482 | Ok(Section { 483 | content: String::from_utf8(sec_content_bytes)?, 484 | content_size, 485 | tag: tag.to_owned(), 486 | }) 487 | } 488 | 489 | /// Reads all [`Sections`](Section) from a specified DAT file, returning a [`Vec`] of them. 490 | /// This performs only one read operation on the underlying file, loading the entire content into memory 491 | /// to prevent repeat file access. This is similar to [`read_content()`](crate::dat_file::read_content), 492 | /// but returns a `Vec
` instead of raw bytes. This does not check that the 493 | /// file is of a type that should contain sections. 494 | /// 495 | /// # Errors 496 | /// 497 | /// Returns a [`DATError::Overflow`](crate::dat_error::DATError::Overflow) or 498 | /// [`DATError::Underflow`](crate::dat_error::DATError::Underflow) if a section content block 499 | /// does not match the expected length specified in the section header. 500 | /// 501 | /// Returns a [`DATError::BadEncoding`](crate::dat_error::DATError::BadEncoding) if a section does not 502 | /// contain valid utf8 text. 503 | /// 504 | /// Returns a [`DATError::BadHeader`](crate::dat_error::DATError::BadHeader) if the specified file does not 505 | /// have a valid DAT header. 506 | /// 507 | /// If an I/O error occurs while reading the file, a [`DATError::FileIO`](crate::dat_error::DATError::FileIO) 508 | /// error will be returned wrapping the underlying FS error. 509 | /// 510 | /// # Examples 511 | /// ```rust 512 | /// use libxivdat::section::read_section_content_unsafe; 513 | /// 514 | /// let section = read_section_content_unsafe("./resources/TEST_SECTION.DAT").unwrap(); 515 | /// 516 | /// assert_eq!(section[0].tag, "T"); 517 | /// assert_eq!(section[0].content_size, 24); 518 | /// assert_eq!(section[0].content, "This is a test section."); 519 | /// 520 | /// assert_eq!(section[1].tag, "A"); 521 | /// assert_eq!(section[1].content_size, 22); 522 | /// assert_eq!(section[1].content, "Another test section."); 523 | /// ``` 524 | pub fn read_section_content_unsafe>(path: P) -> Result, DATError> { 525 | let content_bytes = read_content(path)?; 526 | let section_data = as_section_vec(&content_bytes)?; 527 | Ok(section_data.iter().map(Section::from).collect()) 528 | } 529 | 530 | // --- Unit Tests 531 | 532 | #[cfg(test)] 533 | mod tests { 534 | use super::*; 535 | use crate::dat_file::{read_content, DATFile}; 536 | 537 | const TEST_FILE_PATH: &str = "./resources/TEST_SECTION.DAT"; 538 | const TEST_NON_SECTION_PATH: &str = "./resources/TEST_BLOCK.DAT"; 539 | const TEST_FILE_SEC1_CONTENTS: (&str, u16, &str) = ("T", 24, "This is a test section."); 540 | const TEST_FILE_SEC2_CONTENTS: (&str, u16, &str) = ("A", 22, "Another test section."); 541 | const TEST_SEC: [u8; 7] = [0x41, 0x04, 0x00, 0x41, 0x42, 0x43, 0x00]; 542 | const TEST_SEC_CONTENTS: (&str, u16, &str) = ("A", 4, "ABC"); 543 | const TEST_SEC_NOT_UTF8: [u8; 7] = [0xc2, 0x04, 0x00, 0x01, 0x01, 0x01, 0x00]; 544 | const TEST_SEC_TOO_SHORT: [u8; 6] = [0x41, 0x04, 0x00, 0x41, 0x42, 0x00]; 545 | const TEST_SEC_TOO_LONG: [u8; 8] = [0x41, 0x04, 0x00, 0x41, 0x42, 0x43, 0x44, 0x00]; 546 | 547 | // --- Module Functions 548 | 549 | #[test] 550 | fn test_as_section() -> Result<(), String> { 551 | match as_section(&TEST_SEC[..]) { 552 | Ok(section) => { 553 | assert_eq!(section.tag, TEST_SEC_CONTENTS.0); 554 | assert_eq!(section.content_size, TEST_SEC_CONTENTS.1); 555 | assert_eq!(section.content, TEST_SEC_CONTENTS.2); 556 | Ok(()) 557 | } 558 | Err(err) => Err(format!("Error: {}", err)), 559 | } 560 | } 561 | 562 | #[test] 563 | fn test_as_section_vec() -> Result<(), String> { 564 | let sec_bytes = match read_content(TEST_FILE_PATH) { 565 | Ok(sec_bytes) => sec_bytes, 566 | Err(err) => return Err(format!("Error reading file: {}", err)), 567 | }; 568 | match as_section_vec(&sec_bytes) { 569 | Ok(section) => { 570 | assert_eq!(section[0].tag, TEST_FILE_SEC1_CONTENTS.0); 571 | assert_eq!(section[0].content_size, TEST_FILE_SEC1_CONTENTS.1); 572 | assert_eq!(section[0].content, TEST_FILE_SEC1_CONTENTS.2); 573 | assert_eq!(section[1].tag, TEST_FILE_SEC2_CONTENTS.0); 574 | assert_eq!(section[1].content_size, TEST_FILE_SEC2_CONTENTS.1); 575 | assert_eq!(section[1].content, TEST_FILE_SEC2_CONTENTS.2); 576 | Ok(()) 577 | } 578 | Err(err) => Err(format!("Error: {}", err)), 579 | } 580 | } 581 | 582 | #[test] 583 | fn test_as_section_vec_error_overflow() -> Result<(), String> { 584 | let mut sec_bytes = match read_content(TEST_FILE_PATH) { 585 | Ok(sec_bytes) => sec_bytes, 586 | Err(err) => return Err(format!("Error reading file: {}", err)), 587 | }; 588 | // Remove the terminating null from the first section and add in arbitrary data. 589 | sec_bytes[SECTION_HEADER_SIZE + usize::from(TEST_FILE_SEC1_CONTENTS.1) - 1] = 0x41; 590 | match as_section_vec(&sec_bytes) { 591 | Ok(_) => Err("No error returned.".to_owned()), 592 | Err(err) => match err { 593 | DATError::Overflow(_) => Ok(()), 594 | _ => Err(format!("Incorrect error: {}", err)), 595 | }, 596 | } 597 | } 598 | 599 | #[test] 600 | fn test_as_section_vec_error_underflow() -> Result<(), String> { 601 | let mut sec_bytes = match read_content(TEST_FILE_PATH) { 602 | Ok(sec_bytes) => sec_bytes, 603 | Err(err) => return Err(format!("Error reading file: {}", err)), 604 | }; 605 | // Add an early terminating null to the first section. 606 | sec_bytes[SECTION_HEADER_SIZE + usize::from(TEST_FILE_SEC1_CONTENTS.1) - 2] = 0x00; 607 | match as_section_vec(&sec_bytes) { 608 | Ok(_) => Err("No error returned.".to_owned()), 609 | Err(err) => match err { 610 | DATError::Underflow(_) => Ok(()), 611 | _ => Err(format!("Incorrect error: {}", err)), 612 | }, 613 | } 614 | } 615 | 616 | #[test] 617 | fn test_as_section_vec_error_encoding() -> Result<(), String> { 618 | let mut sec_bytes = match read_content(TEST_FILE_PATH) { 619 | Ok(sec_bytes) => sec_bytes, 620 | Err(err) => return Err(format!("Error reading file: {}", err)), 621 | }; 622 | // Add a random utf8 control char. 623 | sec_bytes[SECTION_HEADER_SIZE + usize::from(TEST_FILE_SEC1_CONTENTS.1) - 2] = 0xc2; 624 | match as_section_vec(&sec_bytes) { 625 | Ok(_) => Err("No error returned.".to_owned()), 626 | Err(err) => match err { 627 | DATError::BadEncoding(_) => Ok(()), 628 | _ => Err(format!("Incorrect error: {}", err)), 629 | }, 630 | } 631 | } 632 | 633 | #[test] 634 | fn test_read_section() -> Result<(), String> { 635 | let mut dat_file = match DATFile::open(TEST_FILE_PATH) { 636 | Ok(dat_file) => dat_file, 637 | Err(err) => return Err(format!("Error opening file: {}", err)), 638 | }; 639 | match read_section(&mut dat_file) { 640 | Ok(section) => { 641 | assert_eq!(section.tag, TEST_FILE_SEC1_CONTENTS.0); 642 | assert_eq!(section.content_size, TEST_FILE_SEC1_CONTENTS.1); 643 | assert_eq!(section.content, TEST_FILE_SEC1_CONTENTS.2); 644 | Ok(()) 645 | } 646 | Err(err) => Err(format!("Error: {}", err)), 647 | } 648 | } 649 | 650 | #[test] 651 | fn test_read_section_error_type() -> Result<(), String> { 652 | let mut dat_file = match DATFile::open(TEST_NON_SECTION_PATH) { 653 | Ok(dat_file) => dat_file, 654 | Err(err) => return Err(format!("Error opening file: {}", err)), 655 | }; 656 | match read_section(&mut dat_file) { 657 | Ok(_) => Err("No error returned.".to_owned()), 658 | Err(err) => match err { 659 | DATError::IncorrectType(_) => Ok(()), 660 | _ => Err(format!("Incorrect error: {}", err)), 661 | }, 662 | } 663 | } 664 | 665 | #[test] 666 | fn test_read_section_error_eof() -> Result<(), String> { 667 | let mut dat_file = match DATFile::open(TEST_FILE_PATH) { 668 | Ok(dat_file) => dat_file, 669 | Err(err) => return Err(format!("Error opening file: {}", err)), 670 | }; 671 | dat_file.seek(SeekFrom::End(-1)).unwrap(); 672 | match read_section(&mut dat_file) { 673 | Ok(_) => Err("No error returned.".to_owned()), 674 | Err(err) => match err { 675 | DATError::EndOfFile(_) => Ok(()), 676 | _ => Err(format!("Incorrect error: {}", err)), 677 | }, 678 | } 679 | } 680 | 681 | #[test] 682 | fn test_read_section_content() -> Result<(), String> { 683 | // Errors are indirectly tested by as_section_vec tests. 684 | match read_section_content(TEST_FILE_PATH) { 685 | Ok(section) => { 686 | assert_eq!(section[0].tag, TEST_FILE_SEC1_CONTENTS.0); 687 | assert_eq!(section[0].content_size, TEST_FILE_SEC1_CONTENTS.1); 688 | assert_eq!(section[0].content, TEST_FILE_SEC1_CONTENTS.2); 689 | assert_eq!(section[1].tag, TEST_FILE_SEC2_CONTENTS.0); 690 | assert_eq!(section[1].content_size, TEST_FILE_SEC2_CONTENTS.1); 691 | assert_eq!(section[1].content, TEST_FILE_SEC2_CONTENTS.2); 692 | Ok(()) 693 | } 694 | Err(err) => Err(format!("Error: {}", err)), 695 | } 696 | } 697 | 698 | #[test] 699 | fn test_read_section_content_error_type() -> Result<(), String> { 700 | match read_section_content(TEST_NON_SECTION_PATH) { 701 | Ok(_) => Err("No error returned.".to_owned()), 702 | Err(err) => match err { 703 | DATError::IncorrectType(_) => Ok(()), 704 | _ => Err(format!("Incorrect error: {}", err)), 705 | }, 706 | } 707 | } 708 | 709 | // --- Section 710 | 711 | #[test] 712 | fn test_section_new() -> Result<(), String> { 713 | match Section::new("T".to_string(), "Test".to_string()) { 714 | Ok(section) => { 715 | assert_eq!(section.tag, "T"); 716 | assert_eq!(section.content_size, 5); 717 | assert_eq!(section.content, "Test"); 718 | Ok(()) 719 | } 720 | Err(err) => Err(format!("Error: {}", err)), 721 | } 722 | } 723 | 724 | #[test] 725 | fn test_section_new_error_title_size() -> Result<(), String> { 726 | match Section::new("Too long".to_string(), "Test".to_string()) { 727 | Ok(_) => Err("No error returned".to_owned()), 728 | Err(err) => match err { 729 | DATError::InvalidInput(_) => Ok(()), 730 | _ => Err(format!("Incorrect error: {}", err)), 731 | }, 732 | } 733 | } 734 | 735 | #[test] 736 | fn test_section_new_error_content_size() -> Result<(), String> { 737 | match Section::new("T".to_string(), (0..u16::MAX).map(|_| "X").collect()) { 738 | Ok(_) => Err("No error returned".to_owned()), 739 | Err(err) => match err { 740 | DATError::Overflow(_) => Ok(()), 741 | _ => Err(format!("Incorrect error: {}", err)), 742 | }, 743 | } 744 | } 745 | 746 | #[test] 747 | fn test_section_from_bytes() -> Result<(), String> { 748 | match Section::try_from(&TEST_SEC[..]) { 749 | Ok(section) => { 750 | assert_eq!(section.tag, TEST_SEC_CONTENTS.0); 751 | assert_eq!(section.content_size, TEST_SEC_CONTENTS.1); 752 | assert_eq!(section.content, TEST_SEC_CONTENTS.2); 753 | Ok(()) 754 | } 755 | Err(err) => Err(format!("Error: {}", err)), 756 | } 757 | } 758 | 759 | #[test] 760 | fn test_section_from_bytes_error_overflow() -> Result<(), String> { 761 | match Section::try_from(&TEST_SEC_TOO_SHORT[..]) { 762 | Ok(_) => Err("No error returned.".to_owned()), 763 | Err(err) => match err { 764 | DATError::Overflow(_) => Ok(()), 765 | _ => Err(format!("Incorrect error: {}", err)), 766 | }, 767 | } 768 | } 769 | 770 | #[test] 771 | fn test_section_from_bytes_error_underflow() -> Result<(), String> { 772 | match Section::try_from(&TEST_SEC_TOO_LONG[..]) { 773 | Ok(_) => Err("No error returned.".to_owned()), 774 | Err(err) => match err { 775 | DATError::Underflow(_) => Ok(()), 776 | _ => Err(format!("Incorrect error: {}", err)), 777 | }, 778 | } 779 | } 780 | 781 | #[test] 782 | fn test_section_from_bytes_error_encoding() -> Result<(), String> { 783 | match Section::try_from(&TEST_SEC_NOT_UTF8[..]) { 784 | Ok(_) => Err("No error returned.".to_owned()), 785 | Err(err) => match err { 786 | DATError::BadEncoding(_) => Ok(()), 787 | _ => Err(format!("Incorrect error: {}", err)), 788 | }, 789 | } 790 | } 791 | 792 | #[test] 793 | fn test_section_to_bytes() -> Result<(), String> { 794 | let sec_bytes = Vec::::from(Section { 795 | tag: TEST_SEC_CONTENTS.0.to_owned(), 796 | content_size: TEST_SEC_CONTENTS.1, 797 | content: TEST_SEC_CONTENTS.2.to_owned(), 798 | }); 799 | Ok(assert_eq!(sec_bytes, TEST_SEC)) 800 | } 801 | 802 | #[test] 803 | fn test_section_to_sectiondata() -> Result<(), String> { 804 | let test_sec = Section { 805 | tag: TEST_SEC_CONTENTS.0.to_owned(), 806 | content_size: TEST_SEC_CONTENTS.1, 807 | content: TEST_SEC_CONTENTS.2.to_owned(), 808 | }; 809 | let sec_data = SectionData::from(&test_sec); 810 | assert_eq!(sec_data.tag, TEST_SEC_CONTENTS.0); 811 | assert_eq!(sec_data.content_size, TEST_SEC_CONTENTS.1); 812 | assert_eq!(sec_data.content, TEST_SEC_CONTENTS.2); 813 | Ok(()) 814 | } 815 | 816 | // --- SectionData 817 | 818 | #[test] 819 | fn test_sectiondata_new() -> Result<(), String> { 820 | match SectionData::new("T", "Test") { 821 | Ok(section) => { 822 | assert_eq!(section.tag, "T"); 823 | assert_eq!(section.content_size, 5); 824 | assert_eq!(section.content, "Test"); 825 | Ok(()) 826 | } 827 | Err(err) => Err(format!("Error: {}", err)), 828 | } 829 | } 830 | 831 | #[test] 832 | fn test_sectiondata_new_error_title_size() -> Result<(), String> { 833 | match SectionData::new("Too long", "Test") { 834 | Ok(_) => Err("No error returned".to_owned()), 835 | Err(err) => match err { 836 | DATError::InvalidInput(_) => Ok(()), 837 | _ => Err(format!("Incorrect error: {}", err)), 838 | }, 839 | } 840 | } 841 | 842 | #[test] 843 | fn test_sectiondata_new_error_content_size() -> Result<(), String> { 844 | match SectionData::new("T", &(0..u16::MAX).map(|_| "X").collect::()) { 845 | Ok(_) => Err("No error returned".to_owned()), 846 | Err(err) => match err { 847 | DATError::Overflow(_) => Ok(()), 848 | _ => Err(format!("Incorrect error: {}", err)), 849 | }, 850 | } 851 | } 852 | 853 | #[test] 854 | fn test_sectiondata_from_bytes() -> Result<(), String> { 855 | match SectionData::try_from(&TEST_SEC[..]) { 856 | Ok(section) => { 857 | assert_eq!(section.tag, TEST_SEC_CONTENTS.0); 858 | assert_eq!(section.content_size, TEST_SEC_CONTENTS.1); 859 | assert_eq!(section.content, TEST_SEC_CONTENTS.2); 860 | Ok(()) 861 | } 862 | Err(err) => Err(format!("Error: {}", err)), 863 | } 864 | } 865 | 866 | #[test] 867 | fn test_sectiondata_from_bytes_error_overflow() -> Result<(), String> { 868 | match SectionData::try_from(&TEST_SEC_TOO_SHORT[..]) { 869 | Ok(_) => Err("No error returned.".to_owned()), 870 | Err(err) => match err { 871 | DATError::Overflow(_) => Ok(()), 872 | _ => Err(format!("Incorrect error: {}", err)), 873 | }, 874 | } 875 | } 876 | 877 | #[test] 878 | fn test_sectiondata_from_bytes_error_underflow() -> Result<(), String> { 879 | match SectionData::try_from(&TEST_SEC_TOO_LONG[..]) { 880 | Ok(_) => Err("No error returned.".to_owned()), 881 | Err(err) => match err { 882 | DATError::Underflow(_) => Ok(()), 883 | _ => Err(format!("Incorrect error: {}", err)), 884 | }, 885 | } 886 | } 887 | 888 | #[test] 889 | fn test_sectiondata_from_bytes_error_encoding() -> Result<(), String> { 890 | match SectionData::try_from(&TEST_SEC_NOT_UTF8[..]) { 891 | Ok(_) => Err("No error returned.".to_owned()), 892 | Err(err) => match err { 893 | DATError::BadEncoding(_) => Ok(()), 894 | _ => Err(format!("Incorrect error: {}", err)), 895 | }, 896 | } 897 | } 898 | 899 | #[test] 900 | fn test_sectiondata_to_bytes() -> Result<(), String> { 901 | let sec_bytes = Vec::::from(SectionData { 902 | tag: TEST_SEC_CONTENTS.0, 903 | content_size: TEST_SEC_CONTENTS.1, 904 | content: TEST_SEC_CONTENTS.2, 905 | }); 906 | Ok(assert_eq!(sec_bytes, TEST_SEC)) 907 | } 908 | 909 | #[test] 910 | fn test_sectiondata_to_section() -> Result<(), String> { 911 | let test_sec = SectionData { 912 | tag: TEST_SEC_CONTENTS.0, 913 | content_size: TEST_SEC_CONTENTS.1, 914 | content: TEST_SEC_CONTENTS.2, 915 | }; 916 | let section = Section::from(&test_sec); 917 | assert_eq!(section.tag, TEST_SEC_CONTENTS.0); 918 | assert_eq!(section.content_size, TEST_SEC_CONTENTS.1); 919 | assert_eq!(section.content, TEST_SEC_CONTENTS.2); 920 | Ok(()) 921 | } 922 | } 923 | --------------------------------------------------------------------------------