├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── mft.pyi ├── pyproject.toml ├── rust-toolchain ├── samples └── MFT ├── scripts └── mft_dump.py ├── setup.py ├── src ├── attribute.rs ├── entry.rs ├── err.rs ├── lib.rs └── utils.rs └── tests └── test_mft.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "[0-9]+.[0-9]+.[0-9]+" 7 | pull_request: 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Install Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.10' 19 | 20 | - name: Build instrumentation wheels 21 | uses: messense/maturin-action@v1 22 | with: 23 | manylinux: auto 24 | command: build 25 | args: --release -i python3.10 -o dist 26 | env: 27 | RUSTFLAGS: "-Cprofile-generate=${{ github.workspace }}/pgo-data" 28 | 29 | - name: Install Rust 30 | uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: stable 33 | profile: minimal 34 | override: true 35 | target: x86_64-unknown-linux-gnu 36 | components: llvm-tools-preview 37 | 38 | - name: PGO optimize 39 | run: | 40 | PATH=$HOME/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/:$PATH 41 | pip install --no-index --find-links ./dist mft 42 | echo "Running instrumented binary" 43 | sudo mkdir -p $PWD/pgo-data 44 | sudo chmod -R 777 $PWD/pgo-data 45 | for i in $(find $PWD/samples -name "MFT"); do 46 | echo "Processing $i" 47 | python scripts/mft_dump.py $i 1>/dev/null 48 | done 49 | echo "Merging profile data" 50 | llvm-profdata merge -o $PWD/pgo-data/merged.profdata $PWD/pgo-data 51 | 52 | - name: Build native wheels 53 | uses: messense/maturin-action@v1 54 | with: 55 | manylinux: auto 56 | command: build 57 | args: -i python3.7 python3.8 python3.9 python3.10 python3.11 --release -o dist 58 | env: 59 | RUSTFLAGS: "-Cprofile-use=${{ github.workspace }}/pgo-data/merged.profdata" 60 | 61 | - name: build abi3 wheel 62 | uses: messense/maturin-action@v1 63 | with: 64 | manylinux: auto 65 | command: build 66 | args: --features=abi3 --release -o dist 67 | env: 68 | RUSTFLAGS: "-Cprofile-use=${{ github.workspace }}/pgo-data/merged.profdata" 69 | 70 | - name: Upload wheels 71 | uses: actions/upload-artifact@v2 72 | with: 73 | name: wheels 74 | path: dist 75 | - run: pip install -U pytest 76 | - run: pip install --no-index --find-links ./dist mft 77 | - run: pytest 78 | 79 | windows: 80 | runs-on: windows-latest 81 | steps: 82 | - uses: actions/checkout@v2 83 | - name: Build native wheels 84 | uses: messense/maturin-action@v1 85 | with: 86 | command: build 87 | args: --release -o dist 88 | - name: Build abi3 wheel 89 | uses: messense/maturin-action@v1 90 | with: 91 | manylinux: auto 92 | command: build 93 | args: --features=abi3 --release -o dist 94 | - name: Upload wheels 95 | uses: actions/upload-artifact@v2 96 | with: 97 | name: wheels 98 | path: dist 99 | - run: pip install -U pytest 100 | - run: pip install --no-index --find-links ./dist mft 101 | - run: pytest 102 | 103 | macos: 104 | runs-on: macos-latest 105 | steps: 106 | - uses: actions/checkout@v2 107 | - name: Build native wheels 108 | uses: messense/maturin-action@v1 109 | with: 110 | command: build 111 | args: --release -o dist --universal2 112 | - name: Build abi3 wheel 113 | uses: messense/maturin-action@v1 114 | with: 115 | manylinux: auto 116 | command: build 117 | args: --features=abi3 --release -o dist 118 | - name: Upload wheels 119 | uses: actions/upload-artifact@v2 120 | with: 121 | name: wheels 122 | path: dist 123 | - run: pip install -U pytest 124 | - run: pip install --no-index --find-links ./dist mft 125 | - run: pytest 126 | 127 | macos-m1: 128 | runs-on: macos-latest 129 | steps: 130 | - uses: actions/checkout@v2 131 | - name: Build native wheels 132 | uses: messense/maturin-action@v1 133 | with: 134 | command: build 135 | target: aarch64-apple-darwin 136 | args: --release -o dist --zig 137 | - name: Build abi3 wheel 138 | uses: messense/maturin-action@v1 139 | with: 140 | manylinux: auto 141 | command: build 142 | target: aarch64-apple-darwin 143 | args: --features=abi3 --release -o dist --zig 144 | - name: Upload wheels 145 | uses: actions/upload-artifact@v2 146 | with: 147 | name: wheels 148 | path: dist 149 | 150 | release: 151 | name: Release 152 | runs-on: ubuntu-latest 153 | if: "startsWith(github.ref, 'refs/tags/')" 154 | needs: [ macos, windows, linux, macos-m1 ] 155 | steps: 156 | - uses: actions/download-artifact@v2 157 | with: 158 | name: wheels 159 | - name: Publish to PyPI 160 | uses: messense/maturin-action@v1 161 | env: 162 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 163 | with: 164 | command: upload 165 | args: --skip-existing * 166 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .venv 3 | /target 4 | **/*.rs.bk 5 | 6 | *.pyc 7 | *.egg-info/ 8 | *.so 9 | .idea 10 | *.pyd 11 | .pytest_cache 12 | .history 13 | dist/ 14 | out/ 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": [ 3 | "abi3" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [0.6.1] 10 | 11 | - Massive dependency update 12 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "aho-corasick" 18 | version = "0.7.18" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 21 | dependencies = [ 22 | "memchr", 23 | ] 24 | 25 | [[package]] 26 | name = "anyhow" 27 | version = "1.0.41" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" 30 | 31 | [[package]] 32 | name = "atty" 33 | version = "0.2.14" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 36 | dependencies = [ 37 | "hermit-abi 0.1.19", 38 | "libc", 39 | "winapi", 40 | ] 41 | 42 | [[package]] 43 | name = "autocfg" 44 | version = "1.0.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 47 | 48 | [[package]] 49 | name = "bitflags" 50 | version = "1.3.2" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 53 | 54 | [[package]] 55 | name = "bstr" 56 | version = "0.2.16" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" 59 | dependencies = [ 60 | "lazy_static", 61 | "memchr", 62 | "regex-automata", 63 | "serde", 64 | ] 65 | 66 | [[package]] 67 | name = "bytecount" 68 | version = "0.6.2" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" 71 | 72 | [[package]] 73 | name = "byteorder" 74 | version = "1.4.3" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 77 | 78 | [[package]] 79 | name = "camino" 80 | version = "1.1.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "c77df041dc383319cc661b428b6961a005db4d6808d5e12536931b1ca9556055" 83 | dependencies = [ 84 | "serde", 85 | ] 86 | 87 | [[package]] 88 | name = "cargo-platform" 89 | version = "0.1.1" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "0226944a63d1bf35a3b5f948dd7c59e263db83695c9e8bffc4037de02e30f1d7" 92 | dependencies = [ 93 | "serde", 94 | ] 95 | 96 | [[package]] 97 | name = "cargo_metadata" 98 | version = "0.14.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" 101 | dependencies = [ 102 | "camino", 103 | "cargo-platform", 104 | "semver", 105 | "serde", 106 | "serde_json", 107 | ] 108 | 109 | [[package]] 110 | name = "cc" 111 | version = "1.0.79" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 114 | 115 | [[package]] 116 | name = "cfg-if" 117 | version = "1.0.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 120 | 121 | [[package]] 122 | name = "chrono" 123 | version = "0.4.19" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 126 | dependencies = [ 127 | "libc", 128 | "num-integer", 129 | "num-traits", 130 | "serde", 131 | "time 0.1.43", 132 | "winapi", 133 | ] 134 | 135 | [[package]] 136 | name = "clap" 137 | version = "4.1.6" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" 140 | dependencies = [ 141 | "bitflags", 142 | "clap_lex", 143 | "is-terminal", 144 | "strsim", 145 | "termcolor", 146 | ] 147 | 148 | [[package]] 149 | name = "clap_lex" 150 | version = "0.3.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" 153 | dependencies = [ 154 | "os_str_bytes", 155 | ] 156 | 157 | [[package]] 158 | name = "console" 159 | version = "0.15.5" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 162 | dependencies = [ 163 | "encode_unicode", 164 | "lazy_static", 165 | "libc", 166 | "unicode-width", 167 | "windows-sys 0.42.0", 168 | ] 169 | 170 | [[package]] 171 | name = "csv" 172 | version = "1.1.6" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" 175 | dependencies = [ 176 | "bstr", 177 | "csv-core", 178 | "itoa 0.4.7", 179 | "ryu", 180 | "serde", 181 | ] 182 | 183 | [[package]] 184 | name = "csv-core" 185 | version = "0.1.10" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 188 | dependencies = [ 189 | "memchr", 190 | ] 191 | 192 | [[package]] 193 | name = "dialoguer" 194 | version = "0.10.3" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2" 197 | dependencies = [ 198 | "console", 199 | "shell-words", 200 | "tempfile", 201 | "zeroize", 202 | ] 203 | 204 | [[package]] 205 | name = "either" 206 | version = "1.6.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 209 | 210 | [[package]] 211 | name = "encode_unicode" 212 | version = "0.3.6" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 215 | 216 | [[package]] 217 | name = "encoding" 218 | version = "0.2.33" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" 221 | dependencies = [ 222 | "encoding-index-japanese", 223 | "encoding-index-korean", 224 | "encoding-index-simpchinese", 225 | "encoding-index-singlebyte", 226 | "encoding-index-tradchinese", 227 | ] 228 | 229 | [[package]] 230 | name = "encoding-index-japanese" 231 | version = "1.20141219.5" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" 234 | dependencies = [ 235 | "encoding_index_tests", 236 | ] 237 | 238 | [[package]] 239 | name = "encoding-index-korean" 240 | version = "1.20141219.5" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" 243 | dependencies = [ 244 | "encoding_index_tests", 245 | ] 246 | 247 | [[package]] 248 | name = "encoding-index-simpchinese" 249 | version = "1.20141219.5" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" 252 | dependencies = [ 253 | "encoding_index_tests", 254 | ] 255 | 256 | [[package]] 257 | name = "encoding-index-singlebyte" 258 | version = "1.20141219.5" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" 261 | dependencies = [ 262 | "encoding_index_tests", 263 | ] 264 | 265 | [[package]] 266 | name = "encoding-index-tradchinese" 267 | version = "1.20141219.5" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" 270 | dependencies = [ 271 | "encoding_index_tests", 272 | ] 273 | 274 | [[package]] 275 | name = "encoding_index_tests" 276 | version = "0.1.4" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" 279 | 280 | [[package]] 281 | name = "env_logger" 282 | version = "0.7.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 285 | dependencies = [ 286 | "atty", 287 | "humantime", 288 | "log", 289 | "regex", 290 | "termcolor", 291 | ] 292 | 293 | [[package]] 294 | name = "errno" 295 | version = "0.2.8" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 298 | dependencies = [ 299 | "errno-dragonfly", 300 | "libc", 301 | "winapi", 302 | ] 303 | 304 | [[package]] 305 | name = "errno-dragonfly" 306 | version = "0.1.2" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 309 | dependencies = [ 310 | "cc", 311 | "libc", 312 | ] 313 | 314 | [[package]] 315 | name = "error-chain" 316 | version = "0.12.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" 319 | dependencies = [ 320 | "version_check", 321 | ] 322 | 323 | [[package]] 324 | name = "getrandom" 325 | version = "0.2.3" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 328 | dependencies = [ 329 | "cfg-if", 330 | "libc", 331 | "wasi", 332 | ] 333 | 334 | [[package]] 335 | name = "glob" 336 | version = "0.3.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 339 | 340 | [[package]] 341 | name = "hashbrown" 342 | version = "0.13.2" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 345 | dependencies = [ 346 | "ahash", 347 | ] 348 | 349 | [[package]] 350 | name = "hermit-abi" 351 | version = "0.1.19" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 354 | dependencies = [ 355 | "libc", 356 | ] 357 | 358 | [[package]] 359 | name = "hermit-abi" 360 | version = "0.3.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 363 | 364 | [[package]] 365 | name = "humantime" 366 | version = "1.3.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 369 | dependencies = [ 370 | "quick-error", 371 | ] 372 | 373 | [[package]] 374 | name = "indoc" 375 | version = "1.0.9" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" 378 | 379 | [[package]] 380 | name = "indoc" 381 | version = "2.0.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "6fe2b9d82064e8a0226fddb3547f37f28eaa46d0fc210e275d835f08cf3b76a7" 384 | 385 | [[package]] 386 | name = "instant" 387 | version = "0.1.9" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" 390 | dependencies = [ 391 | "cfg-if", 392 | ] 393 | 394 | [[package]] 395 | name = "io-lifetimes" 396 | version = "1.0.5" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" 399 | dependencies = [ 400 | "libc", 401 | "windows-sys 0.45.0", 402 | ] 403 | 404 | [[package]] 405 | name = "is-terminal" 406 | version = "0.4.3" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" 409 | dependencies = [ 410 | "hermit-abi 0.3.1", 411 | "io-lifetimes", 412 | "rustix", 413 | "windows-sys 0.45.0", 414 | ] 415 | 416 | [[package]] 417 | name = "itertools" 418 | version = "0.10.5" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 421 | dependencies = [ 422 | "either", 423 | ] 424 | 425 | [[package]] 426 | name = "itoa" 427 | version = "0.4.7" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 430 | 431 | [[package]] 432 | name = "itoa" 433 | version = "1.0.5" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 436 | 437 | [[package]] 438 | name = "lazy_static" 439 | version = "1.4.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 442 | 443 | [[package]] 444 | name = "libc" 445 | version = "0.2.139" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 448 | 449 | [[package]] 450 | name = "linux-raw-sys" 451 | version = "0.1.4" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 454 | 455 | [[package]] 456 | name = "lock_api" 457 | version = "0.4.4" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" 460 | dependencies = [ 461 | "scopeguard", 462 | ] 463 | 464 | [[package]] 465 | name = "log" 466 | version = "0.4.14" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 469 | dependencies = [ 470 | "cfg-if", 471 | ] 472 | 473 | [[package]] 474 | name = "lru" 475 | version = "0.9.0" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" 478 | dependencies = [ 479 | "hashbrown", 480 | ] 481 | 482 | [[package]] 483 | name = "memchr" 484 | version = "2.5.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 487 | 488 | [[package]] 489 | name = "memoffset" 490 | version = "0.8.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" 493 | dependencies = [ 494 | "autocfg", 495 | ] 496 | 497 | [[package]] 498 | name = "mft" 499 | version = "0.6.1" 500 | dependencies = [ 501 | "chrono", 502 | "csv", 503 | "log", 504 | "mft 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 505 | "num-traits", 506 | "pyo3", 507 | "pyo3-file", 508 | "serde_json", 509 | ] 510 | 511 | [[package]] 512 | name = "mft" 513 | version = "0.6.1" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "52cf53faa705fc7f6574f99a34fae16ffa12b6391eabd9f7269377738e583e99" 516 | dependencies = [ 517 | "anyhow", 518 | "bitflags", 519 | "byteorder", 520 | "chrono", 521 | "clap", 522 | "csv", 523 | "dialoguer", 524 | "encoding", 525 | "indoc 2.0.0", 526 | "itertools", 527 | "log", 528 | "lru", 529 | "num-derive", 530 | "num-traits", 531 | "rand", 532 | "serde", 533 | "serde_json", 534 | "simplelog", 535 | "skeptic", 536 | "thiserror", 537 | "winstructs", 538 | ] 539 | 540 | [[package]] 541 | name = "num-derive" 542 | version = "0.3.3" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 545 | dependencies = [ 546 | "proc-macro2", 547 | "quote", 548 | "syn", 549 | ] 550 | 551 | [[package]] 552 | name = "num-integer" 553 | version = "0.1.44" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 556 | dependencies = [ 557 | "autocfg", 558 | "num-traits", 559 | ] 560 | 561 | [[package]] 562 | name = "num-traits" 563 | version = "0.2.14" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 566 | dependencies = [ 567 | "autocfg", 568 | ] 569 | 570 | [[package]] 571 | name = "num_threads" 572 | version = "0.1.6" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 575 | dependencies = [ 576 | "libc", 577 | ] 578 | 579 | [[package]] 580 | name = "once_cell" 581 | version = "1.17.1" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 584 | 585 | [[package]] 586 | name = "os_str_bytes" 587 | version = "6.4.1" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 590 | 591 | [[package]] 592 | name = "parking_lot" 593 | version = "0.11.1" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" 596 | dependencies = [ 597 | "instant", 598 | "lock_api", 599 | "parking_lot_core", 600 | ] 601 | 602 | [[package]] 603 | name = "parking_lot_core" 604 | version = "0.8.3" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" 607 | dependencies = [ 608 | "cfg-if", 609 | "instant", 610 | "libc", 611 | "redox_syscall", 612 | "smallvec", 613 | "winapi", 614 | ] 615 | 616 | [[package]] 617 | name = "ppv-lite86" 618 | version = "0.2.10" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 621 | 622 | [[package]] 623 | name = "proc-macro2" 624 | version = "1.0.27" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 627 | dependencies = [ 628 | "unicode-xid", 629 | ] 630 | 631 | [[package]] 632 | name = "pulldown-cmark" 633 | version = "0.9.2" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" 636 | dependencies = [ 637 | "bitflags", 638 | "memchr", 639 | "unicase", 640 | ] 641 | 642 | [[package]] 643 | name = "pyo3" 644 | version = "0.18.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "06a3d8e8a46ab2738109347433cb7b96dffda2e4a218b03ef27090238886b147" 647 | dependencies = [ 648 | "cfg-if", 649 | "indoc 1.0.9", 650 | "libc", 651 | "memoffset", 652 | "parking_lot", 653 | "pyo3-build-config", 654 | "pyo3-ffi", 655 | "pyo3-macros", 656 | "unindent", 657 | ] 658 | 659 | [[package]] 660 | name = "pyo3-build-config" 661 | version = "0.18.1" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "75439f995d07ddfad42b192dfcf3bc66a7ecfd8b4a1f5f6f046aa5c2c5d7677d" 664 | dependencies = [ 665 | "once_cell", 666 | "target-lexicon", 667 | ] 668 | 669 | [[package]] 670 | name = "pyo3-ffi" 671 | version = "0.18.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "839526a5c07a17ff44823679b68add4a58004de00512a95b6c1c98a6dcac0ee5" 674 | dependencies = [ 675 | "libc", 676 | "pyo3-build-config", 677 | ] 678 | 679 | [[package]] 680 | name = "pyo3-file" 681 | version = "0.6.0" 682 | source = "git+https://github.com/omerbenamram/pyo3-file#2bc4c1a00551ce1942ebf20cb4ebee4884b3aad1" 683 | dependencies = [ 684 | "pyo3", 685 | "skeptic", 686 | ] 687 | 688 | [[package]] 689 | name = "pyo3-macros" 690 | version = "0.18.1" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "bd44cf207476c6a9760c4653559be4f206efafb924d3e4cbf2721475fc0d6cc5" 693 | dependencies = [ 694 | "proc-macro2", 695 | "pyo3-macros-backend", 696 | "quote", 697 | "syn", 698 | ] 699 | 700 | [[package]] 701 | name = "pyo3-macros-backend" 702 | version = "0.18.1" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "dc1f43d8e30460f36350d18631ccf85ded64c059829208fe680904c65bcd0a4c" 705 | dependencies = [ 706 | "proc-macro2", 707 | "quote", 708 | "syn", 709 | ] 710 | 711 | [[package]] 712 | name = "quick-error" 713 | version = "1.2.3" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 716 | 717 | [[package]] 718 | name = "quote" 719 | version = "1.0.9" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 722 | dependencies = [ 723 | "proc-macro2", 724 | ] 725 | 726 | [[package]] 727 | name = "rand" 728 | version = "0.8.4" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 731 | dependencies = [ 732 | "libc", 733 | "rand_chacha", 734 | "rand_core", 735 | "rand_hc", 736 | ] 737 | 738 | [[package]] 739 | name = "rand_chacha" 740 | version = "0.3.1" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 743 | dependencies = [ 744 | "ppv-lite86", 745 | "rand_core", 746 | ] 747 | 748 | [[package]] 749 | name = "rand_core" 750 | version = "0.6.3" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 753 | dependencies = [ 754 | "getrandom", 755 | ] 756 | 757 | [[package]] 758 | name = "rand_hc" 759 | version = "0.3.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 762 | dependencies = [ 763 | "rand_core", 764 | ] 765 | 766 | [[package]] 767 | name = "redox_syscall" 768 | version = "0.2.9" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" 771 | dependencies = [ 772 | "bitflags", 773 | ] 774 | 775 | [[package]] 776 | name = "regex" 777 | version = "1.5.4" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 780 | dependencies = [ 781 | "aho-corasick", 782 | "memchr", 783 | "regex-syntax", 784 | ] 785 | 786 | [[package]] 787 | name = "regex-automata" 788 | version = "0.1.10" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 791 | 792 | [[package]] 793 | name = "regex-syntax" 794 | version = "0.6.25" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 797 | 798 | [[package]] 799 | name = "remove_dir_all" 800 | version = "0.5.3" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 803 | dependencies = [ 804 | "winapi", 805 | ] 806 | 807 | [[package]] 808 | name = "rustix" 809 | version = "0.36.8" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" 812 | dependencies = [ 813 | "bitflags", 814 | "errno", 815 | "io-lifetimes", 816 | "libc", 817 | "linux-raw-sys", 818 | "windows-sys 0.45.0", 819 | ] 820 | 821 | [[package]] 822 | name = "ryu" 823 | version = "1.0.5" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 826 | 827 | [[package]] 828 | name = "same-file" 829 | version = "1.0.6" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 832 | dependencies = [ 833 | "winapi-util", 834 | ] 835 | 836 | [[package]] 837 | name = "scopeguard" 838 | version = "1.1.0" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 841 | 842 | [[package]] 843 | name = "semver" 844 | version = "1.0.16" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" 847 | dependencies = [ 848 | "serde", 849 | ] 850 | 851 | [[package]] 852 | name = "serde" 853 | version = "1.0.126" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" 856 | dependencies = [ 857 | "serde_derive", 858 | ] 859 | 860 | [[package]] 861 | name = "serde_derive" 862 | version = "1.0.126" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" 865 | dependencies = [ 866 | "proc-macro2", 867 | "quote", 868 | "syn", 869 | ] 870 | 871 | [[package]] 872 | name = "serde_json" 873 | version = "1.0.64" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 876 | dependencies = [ 877 | "itoa 0.4.7", 878 | "ryu", 879 | "serde", 880 | ] 881 | 882 | [[package]] 883 | name = "shell-words" 884 | version = "1.1.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 887 | 888 | [[package]] 889 | name = "simplelog" 890 | version = "0.12.0" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "48dfff04aade74dd495b007c831cd6f4e0cee19c344dd9dc0884c0289b70a786" 893 | dependencies = [ 894 | "log", 895 | "termcolor", 896 | "time 0.3.19", 897 | ] 898 | 899 | [[package]] 900 | name = "skeptic" 901 | version = "0.13.7" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" 904 | dependencies = [ 905 | "bytecount", 906 | "cargo_metadata", 907 | "error-chain", 908 | "glob", 909 | "pulldown-cmark", 910 | "tempfile", 911 | "walkdir", 912 | ] 913 | 914 | [[package]] 915 | name = "smallvec" 916 | version = "1.6.1" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 919 | 920 | [[package]] 921 | name = "strsim" 922 | version = "0.10.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 925 | 926 | [[package]] 927 | name = "syn" 928 | version = "1.0.73" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" 931 | dependencies = [ 932 | "proc-macro2", 933 | "quote", 934 | "unicode-xid", 935 | ] 936 | 937 | [[package]] 938 | name = "target-lexicon" 939 | version = "0.12.6" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5" 942 | 943 | [[package]] 944 | name = "tempfile" 945 | version = "3.2.0" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 948 | dependencies = [ 949 | "cfg-if", 950 | "libc", 951 | "rand", 952 | "redox_syscall", 953 | "remove_dir_all", 954 | "winapi", 955 | ] 956 | 957 | [[package]] 958 | name = "termcolor" 959 | version = "1.1.2" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 962 | dependencies = [ 963 | "winapi-util", 964 | ] 965 | 966 | [[package]] 967 | name = "thiserror" 968 | version = "1.0.26" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" 971 | dependencies = [ 972 | "thiserror-impl", 973 | ] 974 | 975 | [[package]] 976 | name = "thiserror-impl" 977 | version = "1.0.26" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" 980 | dependencies = [ 981 | "proc-macro2", 982 | "quote", 983 | "syn", 984 | ] 985 | 986 | [[package]] 987 | name = "time" 988 | version = "0.1.43" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 991 | dependencies = [ 992 | "libc", 993 | "winapi", 994 | ] 995 | 996 | [[package]] 997 | name = "time" 998 | version = "0.3.19" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" 1001 | dependencies = [ 1002 | "itoa 1.0.5", 1003 | "libc", 1004 | "num_threads", 1005 | "serde", 1006 | "time-core", 1007 | "time-macros", 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "time-core" 1012 | version = "0.1.0" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" 1015 | 1016 | [[package]] 1017 | name = "time-macros" 1018 | version = "0.2.7" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" 1021 | dependencies = [ 1022 | "time-core", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "unicase" 1027 | version = "2.6.0" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1030 | dependencies = [ 1031 | "version_check", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "unicode-width" 1036 | version = "0.1.8" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 1039 | 1040 | [[package]] 1041 | name = "unicode-xid" 1042 | version = "0.2.2" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1045 | 1046 | [[package]] 1047 | name = "unindent" 1048 | version = "0.1.7" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" 1051 | 1052 | [[package]] 1053 | name = "version_check" 1054 | version = "0.9.4" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1057 | 1058 | [[package]] 1059 | name = "walkdir" 1060 | version = "2.3.2" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1063 | dependencies = [ 1064 | "same-file", 1065 | "winapi", 1066 | "winapi-util", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "wasi" 1071 | version = "0.10.2+wasi-snapshot-preview1" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1074 | 1075 | [[package]] 1076 | name = "winapi" 1077 | version = "0.3.9" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1080 | dependencies = [ 1081 | "winapi-i686-pc-windows-gnu", 1082 | "winapi-x86_64-pc-windows-gnu", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "winapi-i686-pc-windows-gnu" 1087 | version = "0.4.0" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1090 | 1091 | [[package]] 1092 | name = "winapi-util" 1093 | version = "0.1.5" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1096 | dependencies = [ 1097 | "winapi", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "winapi-x86_64-pc-windows-gnu" 1102 | version = "0.4.0" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1105 | 1106 | [[package]] 1107 | name = "windows-sys" 1108 | version = "0.42.0" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1111 | dependencies = [ 1112 | "windows_aarch64_gnullvm", 1113 | "windows_aarch64_msvc", 1114 | "windows_i686_gnu", 1115 | "windows_i686_msvc", 1116 | "windows_x86_64_gnu", 1117 | "windows_x86_64_gnullvm", 1118 | "windows_x86_64_msvc", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "windows-sys" 1123 | version = "0.45.0" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1126 | dependencies = [ 1127 | "windows-targets", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "windows-targets" 1132 | version = "0.42.1" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 1135 | dependencies = [ 1136 | "windows_aarch64_gnullvm", 1137 | "windows_aarch64_msvc", 1138 | "windows_i686_gnu", 1139 | "windows_i686_msvc", 1140 | "windows_x86_64_gnu", 1141 | "windows_x86_64_gnullvm", 1142 | "windows_x86_64_msvc", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "windows_aarch64_gnullvm" 1147 | version = "0.42.1" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 1150 | 1151 | [[package]] 1152 | name = "windows_aarch64_msvc" 1153 | version = "0.42.1" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 1156 | 1157 | [[package]] 1158 | name = "windows_i686_gnu" 1159 | version = "0.42.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 1162 | 1163 | [[package]] 1164 | name = "windows_i686_msvc" 1165 | version = "0.42.1" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 1168 | 1169 | [[package]] 1170 | name = "windows_x86_64_gnu" 1171 | version = "0.42.1" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 1174 | 1175 | [[package]] 1176 | name = "windows_x86_64_gnullvm" 1177 | version = "0.42.1" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 1180 | 1181 | [[package]] 1182 | name = "windows_x86_64_msvc" 1183 | version = "0.42.1" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 1186 | 1187 | [[package]] 1188 | name = "winstructs" 1189 | version = "0.3.0" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "13eb723aae62864dbb48c23bd55a51be9c53a1880c7762805efdd62570c22acf" 1192 | dependencies = [ 1193 | "bitflags", 1194 | "byteorder", 1195 | "chrono", 1196 | "env_logger", 1197 | "log", 1198 | "num-derive", 1199 | "num-traits", 1200 | "serde", 1201 | "serde_json", 1202 | "thiserror", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "zeroize" 1207 | version = "1.5.7" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" 1210 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mft" 3 | version = "0.6.1" 4 | authors = ["Omer Ben-Amram "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [features] 11 | abi3 = ["pyo3/abi3-py37"] 12 | 13 | [dependencies] 14 | mft_rs = { version = "0.6.1", no-default-features = true, package = "mft" } 15 | pyo3 = { version = "0.18.1", features = ["extension-module"] } 16 | num-traits = "^0.2" 17 | log = "^0.4" 18 | chrono = "^0.4" 19 | pyo3-file = { git = "https://github.com/omerbenamram/pyo3-file" } 20 | serde_json = "^1" 21 | csv = "^1" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Download 6 | 7 |
8 | 9 | 10 | # pymft-rs 11 | 12 | Python bindings for `https://github.com/omerbenamram/mft/`. 13 | 14 | ## Installation 15 | 16 | Available on PyPi - https://pypi.org/project/mft/. 17 | 18 | To install from PyPi - `pip install mft` 19 | 20 | ### Wheels 21 | Wheels are currently automatically built for python 3.7+ for all platforms (Windows, macOS - including M1, and `manylinux`). 22 | 23 | ### Installation from sources 24 | Installation is possible for other platforms by installing from sources, this requires a rust compiler and `setuptools-rust`. 25 | 26 | Run `python setup.py install` 27 | 28 | 29 | ## Usage 30 | 31 | Note that the iterators created by `parser.entries()` and `entry.attributes()` may return `RuntimeError` objects if there was an error while trying 32 | to parse one of the attributes, so check them before continuing. 33 | 34 | ```python 35 | from mft import PyMftParser, PyMftAttributeX10, PyMftAttributeX30 36 | 37 | def main(): 38 | parser = PyMftParser("/Users/omerba/Workspace/pymft-rs/samples/MFT") 39 | for entry_or_error in parser.entries(): 40 | if isinstance(entry_or_error, RuntimeError): 41 | continue 42 | 43 | print(f'Entry ID: {entry_or_error.entry_id}') 44 | 45 | for attribute_or_error in entry_or_error.attributes(): 46 | if isinstance(attribute_or_error, RuntimeError): 47 | continue 48 | 49 | resident_content = attribute_or_error.attribute_content 50 | if resident_content: 51 | if isinstance(resident_content, PyMftAttributeX10): 52 | print(f'Found an X10 attribute') 53 | print(f'Modified: {resident_content.modified}') 54 | if isinstance(resident_content, PyMftAttributeX30): 55 | print(f'Found an X30 attribute') 56 | print(f'Modified: {resident_content.modified}') 57 | print(f'Name: {resident_content.name}') 58 | 59 | print('--------------------------------') 60 | ``` 61 | 62 | Will print: 63 | 64 | ``` 65 | Entry ID: 0 66 | Found an X10 attribute 67 | Modified: 2007-06-30 12:50:52.252395+00:00 68 | Found an X30 attribute 69 | Modified: 2007-06-30 12:50:52.252395+00:00 70 | Name: $MFT 71 | -------------------------------- 72 | Entry ID: 1 73 | Found an X10 attribute 74 | Modified: 2007-06-30 12:50:52.252395+00:00 75 | Found an X30 attribute 76 | Modified: 2007-06-30 12:50:52.252395+00:00 77 | Name: $MFTMirr 78 | -------------------------------- 79 | ..... 80 | ``` 81 | -------------------------------------------------------------------------------- /mft.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | class PyMftAttribute: 4 | attribute_content: Any 5 | data_flags: Any 6 | data_size: Any 7 | is_resident: Any 8 | name: Any 9 | type_code: Any 10 | type_name: Any 11 | @classmethod 12 | def __init__(cls, *args, **kwargs) -> None: ... 13 | 14 | class PyMftAttributeOther: 15 | data: Any 16 | @classmethod 17 | def __init__(cls, *args, **kwargs) -> None: ... 18 | 19 | class PyMftAttributeX10: 20 | accessed: Any 21 | class_id: Any 22 | created: Any 23 | file_flags: Any 24 | max_version: Any 25 | mft_modified: Any 26 | modified: Any 27 | owner_id: Any 28 | quota: Any 29 | security_id: Any 30 | usn: Any 31 | version: Any 32 | @classmethod 33 | def __init__(cls, *args, **kwargs) -> None: ... 34 | 35 | class PyMftAttributeX20: 36 | @classmethod 37 | def __init__(cls, *args, **kwargs) -> None: ... 38 | def entries(self, *args, **kwargs) -> Any: ... 39 | 40 | class PyMftAttributeX30: 41 | accessed: Any 42 | created: Any 43 | flags: Any 44 | logical_size: Any 45 | mft_modified: Any 46 | modified: Any 47 | name: Any 48 | namespace: Any 49 | parent_entry_id: Any 50 | parent_entry_sequence: Any 51 | physical_size: Any 52 | reparse_value: Any 53 | @classmethod 54 | def __init__(cls, *args, **kwargs) -> None: ... 55 | 56 | class PyMftAttributeX40: 57 | birth_object_id: Any 58 | birth_volume_id: Any 59 | domain_id: Any 60 | object_id: Any 61 | @classmethod 62 | def __init__(cls, *args, **kwargs) -> None: ... 63 | 64 | class PyMftAttributeX80: 65 | data: Any 66 | @classmethod 67 | def __init__(cls, *args, **kwargs) -> None: ... 68 | 69 | class PyMftAttributeX90: 70 | attribute_type: Any 71 | collation_rule: Any 72 | index_entry_number_of_cluster_blocks: Any 73 | index_entry_size: Any 74 | @classmethod 75 | def __init__(cls, *args, **kwargs) -> None: ... 76 | 77 | class PyMftAttributesIter: 78 | @classmethod 79 | def __init__(cls, *args, **kwargs) -> None: ... 80 | def __iter__(self) -> Any: ... 81 | def __next__(self) -> Any: ... 82 | 83 | class PyMftEntriesIterator: 84 | @classmethod 85 | def __init__(cls, *args, **kwargs) -> None: ... 86 | def __iter__(self) -> Any: ... 87 | def __next__(self) -> Any: ... 88 | 89 | class PyMftEntry: 90 | base_entry_id: Any 91 | base_entry_sequence: Any 92 | entry_id: Any 93 | file_size: Any 94 | flags: Any 95 | full_path: Any 96 | hard_link_count: Any 97 | sequence: Any 98 | total_entry_size: Any 99 | used_entry_size: Any 100 | @classmethod 101 | def __init__(cls, *args, **kwargs) -> None: ... 102 | def attributes(self, *args, **kwargs) -> Any: ... 103 | 104 | class PyMftParser: 105 | @classmethod 106 | def __init__(cls, *args, **kwargs) -> None: ... 107 | def entries(self, *args, **kwargs) -> Any: ... 108 | def entries_csv(self, *args, **kwargs) -> Any: ... 109 | def entries_json(self, *args, **kwargs) -> Any: ... 110 | def number_of_entries(self, *args, **kwargs) -> Any: ... 111 | def __iter__(self) -> Any: ... 112 | def __next__(self) -> Any: ... 113 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=0.14,<0.15"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "mft" 7 | requires-python = ">=3.7" 8 | classifiers = [ 9 | "Development Status :: 3 - Alpha", 10 | "Intended Audience :: Developers", 11 | "License :: OSI Approved :: MIT License", 12 | "Operating System :: MacOS :: MacOS X", 13 | "Operating System :: POSIX", 14 | "Programming Language :: Python :: Implementation :: CPython", 15 | "Programming Language :: Python :: Implementation :: PyPy", 16 | "Programming Language :: Python", 17 | "Programming Language :: Rust", 18 | ] 19 | 20 | [project.optional-dependencies] 21 | test = ["pytest"] 22 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | stable 2 | -------------------------------------------------------------------------------- /samples/MFT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omerbenamram/pymft-rs/68292570df855750df36149ff4393678532a7ea0/samples/MFT -------------------------------------------------------------------------------- /scripts/mft_dump.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from mft import PyMftParser, PyMftAttributeX10, PyMftAttributeX30 4 | 5 | 6 | def main(): 7 | mft_file = os.path.abspath(os.path.expanduser(sys.argv[1])) 8 | parser = PyMftParser(mft_file) 9 | for entry_or_error in parser.entries(): 10 | if isinstance(entry_or_error, RuntimeError): 11 | continue 12 | 13 | print(f"Entry ID: {entry_or_error.entry_id}") 14 | 15 | for attribute_or_error in entry_or_error.attributes(): 16 | if isinstance(attribute_or_error, RuntimeError): 17 | continue 18 | 19 | resident_content = attribute_or_error.attribute_content 20 | if resident_content: 21 | if isinstance(resident_content, PyMftAttributeX10): 22 | print(f"Found an X10 attribute") 23 | print(f"Modified: {resident_content.modified}") 24 | if isinstance(resident_content, PyMftAttributeX30): 25 | print(f"Found an X30 attribute") 26 | print(f"Modified: {resident_content.modified}") 27 | print(f"Name: {resident_content.name}") 28 | 29 | print("--------------------------------") 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup 5 | from setuptools_rust import RustExtension 6 | 7 | 8 | def get_py_version_cfgs(): 9 | # For now each Cfg Py_3_X flag is interpreted as "at least 3.X" 10 | version = sys.version_info[0:2] 11 | py3_min = 5 12 | out_cfg = [] 13 | for minor in range(py3_min, version[1] + 1): 14 | out_cfg.append("--cfg=Py_3_%d" % minor) 15 | 16 | return out_cfg 17 | 18 | 19 | install_requires = ["setuptools-rust"] 20 | 21 | setup( 22 | name="mft", 23 | version="0.5.6", 24 | classifiers=[ 25 | "License :: OSI Approved :: MIT License", 26 | "Development Status :: 3 - Alpha", 27 | "Intended Audience :: Developers", 28 | "Programming Language :: Python", 29 | "Programming Language :: Rust", 30 | "Operating System :: POSIX", 31 | "Operating System :: MacOS :: MacOS X", 32 | ], 33 | rust_extensions=[ 34 | RustExtension( 35 | target="mft", 36 | path="Cargo.toml", 37 | debug=os.getenv("MFT_DEBUG", False), 38 | rustc_flags=get_py_version_cfgs(), 39 | ), 40 | ], 41 | extras_require={ 42 | 'dev': [ 43 | 'pytest' 44 | ] 45 | }, 46 | install_requires=install_requires, 47 | include_package_data=True, 48 | zip_safe=False, 49 | ) 50 | -------------------------------------------------------------------------------- /src/attribute.rs: -------------------------------------------------------------------------------- 1 | use mft_rs::attribute::header::ResidentialHeader; 2 | use mft_rs::attribute::raw::RawAttribute; 3 | use mft_rs::attribute::x40::ObjectIdAttr; 4 | use mft_rs::attribute::x80::DataAttr; 5 | use mft_rs::attribute::x90::IndexRootAttr; 6 | use mft_rs::attribute::MftAttributeContent; 7 | use mft_rs::{FileNameAttr, MftAttribute, StandardInfoAttr}; 8 | 9 | use num_traits::cast::ToPrimitive; 10 | 11 | use mft_rs::attribute::x20::{AttributeListAttr, AttributeListEntry}; 12 | use pyo3::prelude::*; 13 | use pyo3::{ffi, Py, PyResult, Python, ToPyObject}; 14 | 15 | use crate::utils::date_to_pyobject; 16 | 17 | #[pyclass] 18 | pub struct PyMftAttribute { 19 | inner: MftAttribute, 20 | /// Hex value of attribute type 21 | #[pyo3(get)] 22 | pub type_code: u32, 23 | /// String value of attribute type 24 | #[pyo3(get)] 25 | pub type_name: String, 26 | /// Attribute name (can be empty) 27 | #[pyo3(get)] 28 | pub name: String, 29 | #[pyo3(get)] 30 | pub data_flags: String, 31 | #[pyo3(get)] 32 | pub is_resident: bool, 33 | #[pyo3(get)] 34 | pub data_size: u32, 35 | } 36 | 37 | impl PyMftAttribute { 38 | pub fn from_mft_attribute(py: Python, attr: MftAttribute) -> PyResult> { 39 | Py::new( 40 | py, 41 | PyMftAttribute { 42 | type_name: format!("{:?}", &attr.header.type_code), 43 | type_code: attr.header.type_code.to_u32().unwrap(), 44 | name: attr.header.name.clone(), 45 | data_flags: format!("{:?}", &attr.header.data_flags), 46 | is_resident: { 47 | matches!( 48 | attr.header.residential_header, 49 | ResidentialHeader::Resident(_) 50 | ) 51 | }, 52 | data_size: attr.header.record_length, 53 | inner: attr, 54 | }, 55 | ) 56 | } 57 | } 58 | 59 | #[pymethods] 60 | impl PyMftAttribute { 61 | /// Will be one of 62 | /// - `PyMftAttributeX10` 63 | /// - `PyMftAttributeX20` 64 | /// - `PyMftAttributeX30` 65 | /// - `PyMftAttributeX40` 66 | /// - `PyMftAttributeX80` 67 | /// - `PyMftAttributeX90` 68 | /// - `PyMftAttributeOther` (Currently unparsed in rust) 69 | /// - `None` (if attribute content is non-resident) 70 | #[getter] 71 | pub fn attribute_content(&self) -> PyResult { 72 | Python::with_gil(|py| { 73 | Ok(match &self.inner.data { 74 | MftAttributeContent::AttrX10(info) => { 75 | PyMftAttributeX10::from_x10(py, info.clone())?.to_object(py) 76 | } 77 | MftAttributeContent::AttrX20(info) => { 78 | PyMftAttributeX20::from_x20(py, info.clone())?.to_object(py) 79 | } 80 | MftAttributeContent::AttrX30(info) => { 81 | PyMftAttributeX30::from_x30(py, info.clone())?.to_object(py) 82 | } 83 | MftAttributeContent::AttrX40(info) => { 84 | PyMftAttributeX40::from_x40(py, info.clone())?.to_object(py) 85 | } 86 | MftAttributeContent::AttrX80(info) => { 87 | PyMftAttributeX80::from_x80(py, info.clone())?.to_object(py) 88 | } 89 | MftAttributeContent::AttrX90(info) => { 90 | PyMftAttributeX90::from_x90(py, info.clone())?.to_object(py) 91 | } 92 | MftAttributeContent::Raw(raw) => { 93 | PyMftAttributeOther::from_raw(py, raw.clone())?.to_object(py) 94 | } 95 | MftAttributeContent::None => unsafe { 96 | PyObject::from_borrowed_ptr(py, ffi::Py_None()) 97 | }, 98 | }) 99 | }) 100 | } 101 | } 102 | 103 | #[pyclass] 104 | pub struct PyMftAttributeX10 { 105 | inner: StandardInfoAttr, 106 | #[pyo3(get)] 107 | pub max_version: u32, 108 | #[pyo3(get)] 109 | pub version: u32, 110 | #[pyo3(get)] 111 | pub class_id: u32, 112 | #[pyo3(get)] 113 | pub owner_id: u32, 114 | #[pyo3(get)] 115 | pub security_id: u32, 116 | #[pyo3(get)] 117 | pub quota: u64, 118 | #[pyo3(get)] 119 | pub usn: u64, 120 | } 121 | 122 | impl PyMftAttributeX10 { 123 | pub fn from_x10(py: Python, attr: StandardInfoAttr) -> PyResult> { 124 | Py::new( 125 | py, 126 | PyMftAttributeX10 { 127 | max_version: attr.max_version, 128 | version: attr.version, 129 | class_id: attr.class_id, 130 | owner_id: attr.owner_id, 131 | security_id: attr.security_id, 132 | quota: attr.quota, 133 | usn: attr.usn, 134 | inner: attr, 135 | }, 136 | ) 137 | } 138 | } 139 | 140 | #[pymethods] 141 | impl PyMftAttributeX10 { 142 | #[getter] 143 | pub fn created(&self) -> PyResult { 144 | date_to_pyobject(&self.inner.created) 145 | } 146 | #[getter] 147 | pub fn modified(&self) -> PyResult { 148 | date_to_pyobject(&self.inner.modified) 149 | } 150 | #[getter] 151 | pub fn mft_modified(&self) -> PyResult { 152 | date_to_pyobject(&self.inner.mft_modified) 153 | } 154 | 155 | #[getter] 156 | pub fn accessed(&self) -> PyResult { 157 | date_to_pyobject(&self.inner.accessed) 158 | } 159 | 160 | #[getter] 161 | pub fn file_flags(&self) -> PyResult { 162 | Ok(format!("{:?}", self.inner.file_flags)) 163 | } 164 | } 165 | 166 | #[pyclass] 167 | pub struct PyMftAttributeX20Entry { 168 | #[pyo3(get)] 169 | pub attribute_type: u32, 170 | #[pyo3(get)] 171 | pub lowest_vcn: u64, 172 | #[pyo3(get)] 173 | pub name: String, 174 | } 175 | 176 | impl PyMftAttributeX20Entry { 177 | pub fn from_x20_entry(py: Python, attr: &AttributeListEntry) -> PyResult> { 178 | Py::new( 179 | py, 180 | PyMftAttributeX20Entry { 181 | attribute_type: attr.attribute_type, 182 | lowest_vcn: attr.lowest_vcn, 183 | name: attr.name.clone(), 184 | }, 185 | ) 186 | } 187 | } 188 | 189 | #[pyclass] 190 | pub struct PyMftAttributeX20 { 191 | inner: AttributeListAttr, 192 | } 193 | 194 | #[pyclass] 195 | pub struct PyMftX20EntriesIter { 196 | inner: Box + Send>, 197 | } 198 | 199 | #[pymethods] 200 | impl PyMftX20EntriesIter { 201 | fn __iter__(slf: PyRefMut) -> PyResult> { 202 | Ok(slf.into()) 203 | } 204 | 205 | fn __next__(mut slf: PyRefMut) -> PyResult> { 206 | slf.next() 207 | } 208 | } 209 | 210 | impl PyMftX20EntriesIter { 211 | fn next(&mut self) -> PyResult> { 212 | // Extract the result out of the iterator, so iteration will return error, but can continue. 213 | Ok(self.inner.next()) 214 | } 215 | } 216 | 217 | impl PyMftAttributeX20 { 218 | pub fn from_x20(py: Python, attr: AttributeListAttr) -> PyResult> { 219 | Py::new(py, PyMftAttributeX20 { inner: attr }) 220 | } 221 | } 222 | 223 | #[pymethods] 224 | impl PyMftAttributeX20 { 225 | pub fn entries(&self) -> PyResult> { 226 | Python::with_gil(|py| { 227 | let mut attributes = vec![]; 228 | 229 | for entry in &self.inner.entries { 230 | match PyMftAttributeX20Entry::from_x20_entry(py, entry) 231 | .map(|entry| entry.to_object(py)) 232 | { 233 | Ok(obj) => attributes.push(obj), 234 | Err(e) => attributes.push(e.to_object(py)), 235 | } 236 | } 237 | 238 | Py::new( 239 | py, 240 | PyMftX20EntriesIter { 241 | inner: Box::new(attributes.into_iter()), 242 | }, 243 | ) 244 | }) 245 | } 246 | } 247 | 248 | #[pyclass] 249 | pub struct PyMftAttributeX30 { 250 | inner: FileNameAttr, 251 | #[pyo3(get)] 252 | pub parent_entry_id: u64, 253 | #[pyo3(get)] 254 | pub parent_entry_sequence: u16, 255 | #[pyo3(get)] 256 | pub logical_size: u64, 257 | #[pyo3(get)] 258 | pub physical_size: u64, 259 | #[pyo3(get)] 260 | pub reparse_value: u32, 261 | #[pyo3(get)] 262 | pub namespace: String, 263 | #[pyo3(get)] 264 | pub name: String, 265 | } 266 | 267 | impl PyMftAttributeX30 { 268 | pub fn from_x30(py: Python, attr: FileNameAttr) -> PyResult> { 269 | Py::new( 270 | py, 271 | PyMftAttributeX30 { 272 | logical_size: attr.logical_size, 273 | physical_size: attr.physical_size, 274 | reparse_value: attr.reparse_value, 275 | namespace: format!("{:?}", &attr.namespace), 276 | parent_entry_id: attr.parent.entry, 277 | parent_entry_sequence: attr.parent.sequence, 278 | name: attr.name.clone(), 279 | inner: attr, 280 | }, 281 | ) 282 | } 283 | } 284 | 285 | #[pymethods] 286 | impl PyMftAttributeX30 { 287 | #[getter] 288 | pub fn created(&self) -> PyResult { 289 | date_to_pyobject(&self.inner.created) 290 | } 291 | 292 | #[getter] 293 | pub fn modified(&self) -> PyResult { 294 | date_to_pyobject(&self.inner.modified) 295 | } 296 | 297 | #[getter] 298 | pub fn mft_modified(&self) -> PyResult { 299 | date_to_pyobject(&self.inner.mft_modified) 300 | } 301 | 302 | #[getter] 303 | pub fn accessed(&self) -> PyResult { 304 | date_to_pyobject(&self.inner.accessed) 305 | } 306 | 307 | #[getter] 308 | pub fn flags(&self) -> PyResult { 309 | Ok(format!("{:?}", self.inner.flags)) 310 | } 311 | } 312 | 313 | #[pyclass] 314 | pub struct PyMftAttributeX40 { 315 | #[pyo3(get)] 316 | /// Unique Id assigned to file 317 | pub object_id: String, 318 | #[pyo3(get)] 319 | /// Volume where file was created 320 | pub birth_volume_id: String, 321 | #[pyo3(get)] 322 | /// Original Object Id of file 323 | pub birth_object_id: String, 324 | #[pyo3(get)] 325 | /// Domain in which object was created 326 | pub domain_id: String, 327 | } 328 | 329 | impl PyMftAttributeX40 { 330 | pub fn from_x40(py: Python, attr: ObjectIdAttr) -> PyResult> { 331 | Py::new( 332 | py, 333 | PyMftAttributeX40 { 334 | object_id: attr.object_id.to_string(), 335 | birth_volume_id: attr 336 | .birth_volume_id 337 | .as_ref() 338 | .map(|a| a.to_string()) 339 | .unwrap_or_default(), 340 | birth_object_id: attr 341 | .birth_object_id 342 | .as_ref() 343 | .map(|a| a.to_string()) 344 | .unwrap_or_default(), 345 | domain_id: attr 346 | .domain_id 347 | .as_ref() 348 | .map(|a| a.to_string()) 349 | .unwrap_or_default(), 350 | }, 351 | ) 352 | } 353 | } 354 | 355 | #[pyclass] 356 | pub struct PyMftAttributeX80 { 357 | inner: DataAttr, 358 | } 359 | 360 | impl PyMftAttributeX80 { 361 | pub fn from_x80(py: Python, attr: DataAttr) -> PyResult> { 362 | Py::new(py, PyMftAttributeX80 { inner: attr }) 363 | } 364 | } 365 | 366 | #[pymethods] 367 | impl PyMftAttributeX80 { 368 | #[getter] 369 | pub fn data(&self) -> &[u8] { 370 | self.inner.data() 371 | } 372 | } 373 | 374 | #[pyclass] 375 | pub struct PyMftAttributeX90 { 376 | #[pyo3(get)] 377 | /// Unique Id assigned to file 378 | pub attribute_type: u32, 379 | #[pyo3(get)] 380 | /// Collation rule used to sort the index entries. 381 | /// If type is $FILENAME, this must be COLLATION_FILENAME 382 | pub collation_rule: u32, 383 | #[pyo3(get)] 384 | /// The index entry size 385 | pub index_entry_size: u32, 386 | #[pyo3(get)] 387 | /// The index entry number of cluster blocks 388 | pub index_entry_number_of_cluster_blocks: u32, 389 | } 390 | 391 | impl PyMftAttributeX90 { 392 | pub fn from_x90(py: Python, attr: IndexRootAttr) -> PyResult> { 393 | Py::new( 394 | py, 395 | PyMftAttributeX90 { 396 | attribute_type: attr.attribute_type, 397 | collation_rule: attr.collation_rule, 398 | index_entry_size: attr.index_entry_size, 399 | index_entry_number_of_cluster_blocks: attr.index_entry_number_of_cluster_blocks, 400 | }, 401 | ) 402 | } 403 | } 404 | 405 | #[pyclass] 406 | pub struct PyMftAttributeOther { 407 | inner: RawAttribute, 408 | } 409 | 410 | impl PyMftAttributeOther { 411 | pub fn from_raw(py: Python, attr: RawAttribute) -> PyResult> { 412 | Py::new(py, PyMftAttributeOther { inner: attr }) 413 | } 414 | } 415 | 416 | #[pymethods] 417 | impl PyMftAttributeOther { 418 | #[getter] 419 | pub fn data(&self) -> &[u8] { 420 | &self.inner.data 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/entry.rs: -------------------------------------------------------------------------------- 1 | use crate::ReadSeek; 2 | use pyo3::prelude::*; 3 | 4 | use crate::attribute::PyMftAttribute; 5 | use crate::err::PyMftError; 6 | use mft_rs::attribute::header::ResidentialHeader; 7 | use mft_rs::attribute::MftAttributeType; 8 | use mft_rs::{MftEntry, MftParser}; 9 | use pyo3::{Py, PyResult, Python}; 10 | use std::path::PathBuf; 11 | 12 | #[pyclass] 13 | pub struct PyMftEntry { 14 | // We need to keep inner entry to access it's attributes. 15 | inner: MftEntry, 16 | #[pyo3(get)] 17 | pub entry_id: u64, 18 | #[pyo3(get)] 19 | pub sequence: u16, 20 | #[pyo3(get)] 21 | pub base_entry_id: u64, 22 | #[pyo3(get)] 23 | pub base_entry_sequence: u16, 24 | #[pyo3(get)] 25 | pub hard_link_count: u16, 26 | #[pyo3(get)] 27 | pub flags: String, 28 | #[pyo3(get)] 29 | pub used_entry_size: u32, 30 | #[pyo3(get)] 31 | pub total_entry_size: u32, 32 | #[pyo3(get)] 33 | pub full_path: String, 34 | #[pyo3(get)] 35 | pub file_size: u64, 36 | } 37 | 38 | #[pymethods] 39 | impl PyMftEntry { 40 | pub fn attributes(&self) -> PyResult> { 41 | Python::with_gil(|py| { 42 | let mut attributes = vec![]; 43 | 44 | for attribute_result in self.inner.iter_attributes() { 45 | match attribute_result { 46 | Ok(attribute) => match PyMftAttribute::from_mft_attribute(py, attribute) 47 | .map(|entry| entry.to_object(py)) 48 | { 49 | Ok(obj) => attributes.push(obj), 50 | Err(e) => attributes.push(e.to_object(py)), 51 | }, 52 | Err(e) => attributes.push(PyErr::from(PyMftError(e)).to_object(py)), 53 | } 54 | } 55 | 56 | Py::new( 57 | py, 58 | PyMftAttributesIter { 59 | inner: Box::new(attributes.into_iter()), 60 | }, 61 | ) 62 | }) 63 | } 64 | } 65 | 66 | impl PyMftEntry { 67 | pub fn from_mft_entry( 68 | py: Python, 69 | entry: MftEntry, 70 | parser: &mut MftParser, 71 | ) -> PyResult> { 72 | let full_path = parser 73 | .get_full_path_for_entry(&entry) 74 | .expect("unreachable") 75 | .unwrap_or_else(|| PathBuf::from("[UNKNOWN]")) 76 | .to_string_lossy() 77 | .to_string(); 78 | 79 | let file_size = entry 80 | .iter_attributes_matching(Some(vec![MftAttributeType::DATA])) 81 | .find_map(Result::ok) 82 | .map_or(0, |attr| match &attr.header.residential_header { 83 | ResidentialHeader::Resident(r) => u64::from(r.data_size), 84 | ResidentialHeader::NonResident(nr) => nr.file_size, 85 | }); 86 | 87 | Py::new( 88 | py, 89 | PyMftEntry { 90 | entry_id: entry.header.record_number, 91 | sequence: entry.header.sequence, 92 | base_entry_id: entry.header.base_reference.entry, 93 | base_entry_sequence: 0, 94 | hard_link_count: 0, 95 | flags: format!("{:?}", entry.header.flags), 96 | used_entry_size: entry.header.used_entry_size, 97 | total_entry_size: entry.header.total_entry_size, 98 | inner: entry, 99 | full_path, 100 | file_size, 101 | }, 102 | ) 103 | } 104 | } 105 | 106 | #[pyclass] 107 | pub struct PyMftAttributesIter { 108 | inner: Box + Send>, 109 | } 110 | 111 | #[pymethods] 112 | impl PyMftAttributesIter { 113 | fn __iter__(slf: PyRefMut) -> PyResult> { 114 | Ok(slf.into()) 115 | } 116 | 117 | fn __next__(mut slf: PyRefMut) -> PyResult> { 118 | slf.next() 119 | } 120 | } 121 | 122 | impl PyMftAttributesIter { 123 | fn next(&mut self) -> PyResult> { 124 | // Extract the result out of the iterator, so iteration will return error, but can continue. 125 | Ok(self.inner.next()) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/err.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{exceptions, PyErr}; 2 | 3 | pub struct PyMftError(pub mft_rs::err::Error); 4 | 5 | impl From for PyErr { 6 | fn from(err: PyMftError) -> Self { 7 | match err.0 { 8 | mft_rs::err::Error::IoError { source } => source.into(), 9 | _ => PyErr::new::(format!("{}", err.0)), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_must_use)] 2 | #![cfg_attr(not(debug_assertions), deny(clippy::dbg_macro))] 3 | 4 | mod attribute; 5 | mod entry; 6 | mod utils; 7 | 8 | pub(crate) mod err; 9 | pub use entry::PyMftEntry; 10 | use mft_rs::csv::FlatMftEntryWithName; 11 | use mft_rs::entry::ZERO_HEADER; 12 | use mft_rs::{MftEntry, MftParser}; 13 | 14 | use std::fs::File; 15 | use std::io; 16 | use std::io::{BufReader, Read, Seek}; 17 | 18 | use pyo3::exceptions; 19 | use pyo3::prelude::*; 20 | 21 | use crate::attribute::{ 22 | PyMftAttribute, PyMftAttributeOther, PyMftAttributeX10, PyMftAttributeX20, PyMftAttributeX30, 23 | PyMftAttributeX40, PyMftAttributeX80, PyMftAttributeX90, 24 | }; 25 | use crate::entry::PyMftAttributesIter; 26 | use crate::err::PyMftError; 27 | use crate::utils::{init_logging, FileOrFileLike}; 28 | use csv::WriterBuilder; 29 | use pyo3::types::{PyBytes, PyString}; 30 | 31 | pub trait ReadSeek: Read + Seek { 32 | fn tell(&mut self) -> io::Result { 33 | self.stream_position() 34 | } 35 | } 36 | 37 | impl ReadSeek for T {} 38 | 39 | pub enum Output { 40 | Python, 41 | CSV, 42 | JSON, 43 | } 44 | 45 | #[pyclass] 46 | /// PyMftParser(self, path_or_file_like, /) 47 | /// -- 48 | /// 49 | /// Returns an instance of the parser. 50 | /// Works on both a path (string), or a file-like object. 51 | pub struct PyMftParser { 52 | inner: Option>>, 53 | } 54 | 55 | #[pymethods] 56 | impl PyMftParser { 57 | #[new] 58 | fn new(path_or_file_like: PyObject) -> PyResult { 59 | let file_or_file_like = FileOrFileLike::from_pyobject(path_or_file_like)?; 60 | 61 | let (boxed_read_seek, size) = match file_or_file_like { 62 | FileOrFileLike::File(s) => { 63 | let file = File::open(s)?; 64 | let size = file.metadata()?.len(); 65 | 66 | let reader = BufReader::with_capacity(4096, file); 67 | 68 | (Box::new(reader) as Box, Some(size)) 69 | } 70 | FileOrFileLike::FileLike(f) => (Box::new(f) as Box, None), 71 | }; 72 | 73 | let parser = MftParser::from_read_seek(boxed_read_seek, size).map_err(PyMftError)?; 74 | 75 | Ok(PyMftParser { 76 | inner: Some(parser), 77 | }) 78 | } 79 | 80 | /// number_of_entries(self, /) 81 | /// -- 82 | /// 83 | /// Returns the total number of entries in the MFT. 84 | fn number_of_entries(&self) -> PyResult { 85 | match self.inner { 86 | Some(ref inner) => Ok(inner.get_entry_count()), 87 | None => Err(PyErr::new::( 88 | "Cannot call this method before object is initialized", 89 | )), 90 | } 91 | } 92 | 93 | /// entries(self, /) 94 | /// -- 95 | /// 96 | /// Returns an iterator that yields the mft entries as python objects. 97 | fn entries(&mut self) -> PyResult> { 98 | self.records_iterator(Output::Python) 99 | } 100 | 101 | /// entries_json(self, /) 102 | /// -- 103 | /// 104 | /// Returns an iterator that yields mft entries as JSON. 105 | fn entries_json(&mut self) -> PyResult> { 106 | self.records_iterator(Output::JSON) 107 | } 108 | 109 | /// entries_csv(self, /) 110 | /// -- 111 | /// 112 | /// Returns an iterator that yields mft entries CSV lines. 113 | fn entries_csv(&mut self) -> PyResult> { 114 | self.records_iterator(Output::CSV) 115 | } 116 | 117 | fn __iter__(mut slf: PyRefMut) -> PyResult> { 118 | slf.entries() 119 | } 120 | fn __next__(_slf: PyRefMut) -> PyResult> { 121 | Err(PyErr::new::("Using `next()` over `PyMftParser` is not supported. Try iterating over `PyMftParser(...).entries()`")) 122 | } 123 | } 124 | 125 | impl PyMftParser { 126 | fn records_iterator(&mut self, output_format: Output) -> PyResult> { 127 | Python::with_gil(|py| { 128 | let inner = match self.inner.take() { 129 | Some(inner) => inner, 130 | None => { 131 | return Err(PyErr::new::( 132 | "PyMftParser can only be used once", 133 | )); 134 | } 135 | }; 136 | 137 | let n_records = inner.get_entry_count(); 138 | 139 | Py::new( 140 | py, 141 | PyMftEntriesIterator { 142 | inner, 143 | total_number_of_records: n_records, 144 | current_record: 0, 145 | output_format, 146 | csv_header_written: false, 147 | }, 148 | ) 149 | }) 150 | } 151 | } 152 | 153 | #[pyclass] 154 | pub struct PyMftEntriesIterator { 155 | inner: MftParser>, 156 | total_number_of_records: u64, 157 | current_record: u64, 158 | output_format: Output, 159 | csv_header_written: bool, 160 | } 161 | 162 | #[pymethods] 163 | impl PyMftEntriesIterator { 164 | fn __iter__(slf: PyRefMut) -> PyResult> { 165 | Ok(slf.into()) 166 | } 167 | fn __next__(mut slf: PyRefMut) -> PyResult> { 168 | slf.next() 169 | } 170 | } 171 | 172 | impl PyMftEntriesIterator { 173 | fn entry_to_pyobject( 174 | &mut self, 175 | entry_result: Result, 176 | py: Python, 177 | ) -> PyObject { 178 | match entry_result { 179 | Ok(entry) => { 180 | match PyMftEntry::from_mft_entry(py, entry, &mut self.inner) 181 | .map(|entry| entry.to_object(py)) 182 | { 183 | Ok(py_mft_entry) => py_mft_entry, 184 | Err(e) => e.to_object(py), 185 | } 186 | } 187 | Err(e) => { 188 | let err = PyErr::from(e); 189 | err.to_object(py) 190 | } 191 | } 192 | } 193 | 194 | fn entry_to_json( 195 | &mut self, 196 | entry_result: Result, 197 | py: Python, 198 | ) -> PyObject { 199 | match entry_result { 200 | Ok(entry) => match serde_json::to_string(&entry) { 201 | Ok(s) => PyString::new(py, &s).to_object(py), 202 | Err(_e) => PyErr::new::("JSON Serialization failed") 203 | .to_object(py), 204 | }, 205 | Err(e) => PyErr::from(e).to_object(py), 206 | } 207 | } 208 | 209 | fn entry_to_csv(&mut self, entry_result: Result, py: Python) -> PyObject { 210 | let mut writer = WriterBuilder::new() 211 | .has_headers(!self.csv_header_written) 212 | .from_writer(Vec::new()); 213 | 214 | if !self.csv_header_written { 215 | self.csv_header_written = true 216 | } 217 | 218 | match entry_result { 219 | Ok(entry) => { 220 | match writer.serialize(FlatMftEntryWithName::from_entry(&entry, &mut self.inner)) { 221 | Ok(()) => {} 222 | Err(_e) => { 223 | return PyErr::new::( 224 | "CSV Serialization failed", 225 | ) 226 | .to_object(py) 227 | } 228 | } 229 | 230 | match writer.into_inner() { 231 | Ok(bytes) => PyBytes::new(py, &bytes).to_object(py), 232 | Err(e) => { 233 | PyErr::new::(e.to_string()).to_object(py) 234 | } 235 | } 236 | } 237 | Err(e) => PyErr::from(e).to_object(py), 238 | } 239 | } 240 | 241 | fn next(&mut self) -> PyResult> { 242 | Python::with_gil(|py| loop { 243 | if self.current_record == self.total_number_of_records { 244 | return Ok(None); 245 | } 246 | 247 | let obj = match self.inner.get_entry(self.current_record) { 248 | Ok(entry) => { 249 | if &entry.header.signature == ZERO_HEADER { 250 | self.current_record += 1; 251 | continue; 252 | } 253 | 254 | let ret = match self.output_format { 255 | Output::Python => self.entry_to_pyobject(Ok(entry), py), 256 | Output::JSON => self.entry_to_json(Ok(entry), py), 257 | Output::CSV => self.entry_to_csv(Ok(entry), py), 258 | }; 259 | 260 | Ok(Some(ret)) 261 | } 262 | Err(error) => Ok(Some(PyErr::from(PyMftError(error)).to_object(py))), 263 | }; 264 | 265 | self.current_record += 1; 266 | return obj; 267 | }) 268 | } 269 | } 270 | 271 | // Don't use double quotes ("") inside this docstring, this will crash pyo3. 272 | /// Parses an mft file. 273 | #[pymodule] 274 | fn mft(py: Python, m: &PyModule) -> PyResult<()> { 275 | init_logging(py).ok(); 276 | 277 | m.add_class::()?; 278 | 279 | // Entry 280 | m.add_class::()?; 281 | m.add_class::()?; 282 | 283 | // Attributes 284 | m.add_class::()?; 285 | m.add_class::()?; 286 | m.add_class::()?; 287 | m.add_class::()?; 288 | m.add_class::()?; 289 | m.add_class::()?; 290 | m.add_class::()?; 291 | m.add_class::()?; 292 | m.add_class::()?; 293 | 294 | Ok(()) 295 | } 296 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use log::{Level, Log, Metadata, Record, SetLoggerError}; 2 | 3 | use chrono::{DateTime, Datelike, Timelike, Utc}; 4 | use pyo3::types::PyString; 5 | 6 | #[cfg(not(feature = "abi3"))] 7 | use pyo3::types::{PyDateTime, PyTzInfo}; 8 | 9 | use pyo3::ToPyObject; 10 | use pyo3::{PyObject, PyResult, Python}; 11 | use pyo3_file::PyFileLikeObject; 12 | 13 | #[derive(Debug)] 14 | pub enum FileOrFileLike { 15 | File(String), 16 | FileLike(PyFileLikeObject), 17 | } 18 | 19 | impl FileOrFileLike { 20 | pub fn from_pyobject(path_or_file_like: PyObject) -> PyResult { 21 | Python::with_gil(|py| { 22 | // is a path 23 | if let Ok(string_ref) = path_or_file_like.downcast::(py) { 24 | return Ok(FileOrFileLike::File( 25 | string_ref.to_string_lossy().to_string(), 26 | )); 27 | } 28 | 29 | // We only need read + seek 30 | match PyFileLikeObject::with_requirements(path_or_file_like, true, false, true) { 31 | Ok(f) => Ok(FileOrFileLike::FileLike(f)), 32 | Err(e) => Err(e), 33 | } 34 | }) 35 | } 36 | } 37 | 38 | /// A logger that prints all messages with a readable output format. 39 | struct PyLogger { 40 | level: Level, 41 | warnings_module: PyObject, 42 | } 43 | 44 | impl Log for PyLogger { 45 | fn enabled(&self, metadata: &Metadata) -> bool { 46 | metadata.level() <= self.level 47 | } 48 | 49 | fn log(&self, record: &Record) { 50 | if self.enabled(record.metadata()) { 51 | if let Level::Warn = self.level { 52 | let level_string = record.level().to_string(); 53 | Python::with_gil(|py| { 54 | let message = format!( 55 | "{:<5} [{}] {}", 56 | level_string, 57 | record.module_path().unwrap_or_default(), 58 | record.args() 59 | ); 60 | 61 | self.warnings_module 62 | .call_method(py, "warn", (message,), None) 63 | .ok(); 64 | }) 65 | } 66 | } 67 | } 68 | 69 | fn flush(&self) {} 70 | } 71 | 72 | pub fn init_logging(py: Python) -> Result<(), SetLoggerError> { 73 | let warnings = py 74 | .import("warnings") 75 | .expect("python to have warning module") 76 | .to_object(py); 77 | 78 | let logger = PyLogger { 79 | level: Level::Warn, 80 | warnings_module: warnings, 81 | }; 82 | 83 | log::set_boxed_logger(Box::new(logger))?; 84 | log::set_max_level(Level::Warn.to_level_filter()); 85 | 86 | Ok(()) 87 | } 88 | 89 | #[cfg(not(feature = "abi3"))] 90 | pub fn date_to_pyobject(date: &DateTime) -> PyResult { 91 | Python::with_gil(|py| { 92 | let utc = get_utc().ok(); 93 | 94 | if utc.is_none() { 95 | log::warn!("UTC module not found, falling back to naive timezone objects") 96 | } 97 | 98 | let tz = utc.as_ref().map(|tz| tz.downcast::(py).unwrap()); 99 | 100 | PyDateTime::new( 101 | py, 102 | date.year(), 103 | date.month() as u8, 104 | date.day() as u8, 105 | date.hour() as u8, 106 | date.minute() as u8, 107 | date.second() as u8, 108 | date.timestamp_subsec_micros(), 109 | // Fallback to naive timestamps (None) if for some reason `datetime.timezone.utc` is not present. 110 | tz, 111 | ) 112 | .map(|dt| dt.to_object(py)) 113 | }) 114 | } 115 | 116 | #[cfg(feature = "abi3")] 117 | pub fn date_to_pyobject(date: &DateTime) -> PyResult { 118 | // Create a UTC string in the format of `YYYY-MM-DDTHH:MM:SS.ssssssZ` 119 | // This is the format that the `datetime` module expects. 120 | // See: https://docs.python.org/3/library/datetime.html#datetime.datetime.strptime 121 | // 122 | 123 | use pyo3::types::PyDict; 124 | let utc_string = format!( 125 | "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z", 126 | date.year(), 127 | date.month(), 128 | date.day(), 129 | date.hour(), 130 | date.minute(), 131 | date.second(), 132 | date.timestamp_subsec_micros() 133 | ); 134 | 135 | Python::with_gil(|py| { 136 | let datetime = py.import("datetime")?; 137 | let datetime: PyObject = datetime.getattr("datetime")?.into(); 138 | let datetime = datetime.getattr(py, "strptime")?; 139 | 140 | let obj = datetime.call1(py, (utc_string, "%Y-%m-%dT%H:%M:%S.%fZ"))?; 141 | // call replace on the datetime object to replace the tzinfo with the UTC tzinfo 142 | 143 | let kwargs: &PyDict = PyDict::from_sequence( 144 | py, 145 | [("tzinfo".to_object(py), get_utc()?.to_object(py))].to_object(py), 146 | ) 147 | .expect("we have GIL"); 148 | 149 | obj.call_method(py, "replace", (), Some(kwargs)) 150 | .map(|dt| dt.to_object(py)) 151 | }) 152 | } 153 | 154 | pub fn get_utc() -> PyResult { 155 | Python::with_gil(|py| { 156 | let datetime = py.import("datetime")?; 157 | let tz: PyObject = datetime.getattr("timezone")?.into(); 158 | let utc = tz.getattr(py, "utc")?; 159 | 160 | Ok(utc) 161 | }) 162 | } 163 | -------------------------------------------------------------------------------- /tests/test_mft.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pytest 4 | 5 | from pathlib import Path 6 | 7 | from mft import PyMftParser, PyMftEntry 8 | 9 | 10 | @pytest.fixture 11 | def sample_mft() -> Path: 12 | p = Path(__file__).parent.parent / "samples" / "MFT" 13 | assert p.exists() 14 | 15 | return p 16 | 17 | 18 | def test_it_works(sample_mft: Path): 19 | with open(sample_mft, "rb") as m: 20 | parser = PyMftParser(m) 21 | 22 | sample_record: PyMftEntry = next(parser.entries()) 23 | 24 | assert sample_record.entry_id == 0 25 | assert sample_record.full_path == "$MFT" 26 | 27 | 28 | def test_iter_attributes(sample_mft: Path): 29 | with open(sample_mft, "rb") as m: 30 | parser = PyMftParser(m) 31 | 32 | sample_record: PyMftEntry = next(parser.entries()) 33 | 34 | l = list(sample_record.attributes()) 35 | assert len(l) == 4 36 | 37 | 38 | def test_datetimes_are_converted_properly(sample_mft: Path): 39 | with open(sample_mft, "rb") as m: 40 | parser = PyMftParser(m) 41 | 42 | sample_record: PyMftEntry = next(parser.entries()) 43 | 44 | attribute = next(sample_record.attributes()) 45 | 46 | content = attribute.attribute_content 47 | 48 | assert content.created.tzinfo == datetime.timezone.utc 49 | 50 | 51 | def test_doesnt_yield_zeroed_entries(sample_mft: Path): 52 | parser = PyMftParser(str(sample_mft)) 53 | 54 | for entry in parser.entries(): 55 | try: 56 | for attribute in entry.attributes(): 57 | print(entry.entry_id) 58 | except RuntimeError as e: 59 | assert False, (e, entry.entry_id) 60 | 61 | 62 | --------------------------------------------------------------------------------