├── LICENSE.md ├── README.md ├── example.img ├── ext4.py └── ext4.py35.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU General Public License 2 | ========================== 3 | 4 | _Version 3, 29 June 2007_ 5 | _Copyright © 2007 Free Software Foundation, Inc. <>_ 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | ## Preamble 11 | 12 | The GNU General Public License is a free, copyleft license for software and other 13 | kinds of works. 14 | 15 | The licenses for most software and other practical works are designed to take away 16 | your freedom to share and change the works. By contrast, the GNU General Public 17 | License is intended to guarantee your freedom to share and change all versions of a 18 | program--to make sure it remains free software for all its users. We, the Free 19 | Software Foundation, use the GNU General Public License for most of our software; it 20 | applies also to any other work released this way by its authors. You can apply it to 21 | your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not price. Our General 24 | Public Licenses are designed to make sure that you have the freedom to distribute 25 | copies of free software (and charge for them if you wish), that you receive source 26 | code or can get it if you want it, that you can change the software or use pieces of 27 | it in new free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you these rights or 30 | asking you to surrender the rights. Therefore, you have certain responsibilities if 31 | you distribute copies of the software, or if you modify it: responsibilities to 32 | respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether gratis or for a fee, 35 | you must pass on to the recipients the same freedoms that you received. You must make 36 | sure that they, too, receive or can get the source code. And you must show them these 37 | terms so they know their rights. 38 | 39 | Developers that use the GNU GPL protect your rights with two steps: **(1)** assert 40 | copyright on the software, and **(2)** offer you this License giving you legal permission 41 | to copy, distribute and/or modify it. 42 | 43 | For the developers' and authors' protection, the GPL clearly explains that there is 44 | no warranty for this free software. For both users' and authors' sake, the GPL 45 | requires that modified versions be marked as changed, so that their problems will not 46 | be attributed erroneously to authors of previous versions. 47 | 48 | Some devices are designed to deny users access to install or run modified versions of 49 | the software inside them, although the manufacturer can do so. This is fundamentally 50 | incompatible with the aim of protecting users' freedom to change the software. The 51 | systematic pattern of such abuse occurs in the area of products for individuals to 52 | use, which is precisely where it is most unacceptable. Therefore, we have designed 53 | this version of the GPL to prohibit the practice for those products. If such problems 54 | arise substantially in other domains, we stand ready to extend this provision to 55 | those domains in future versions of the GPL, as needed to protect the freedom of 56 | users. 57 | 58 | Finally, every program is threatened constantly by software patents. States should 59 | not allow patents to restrict development and use of software on general-purpose 60 | computers, but in those that do, we wish to avoid the special danger that patents 61 | applied to a free program could make it effectively proprietary. To prevent this, the 62 | GPL assures that patents cannot be used to render the program non-free. 63 | 64 | The precise terms and conditions for copying, distribution and modification follow. 65 | 66 | ## TERMS AND CONDITIONS 67 | 68 | ### 0. Definitions 69 | 70 | “This License” refers to version 3 of the GNU General Public License. 71 | 72 | “Copyright” also means copyright-like laws that apply to other kinds of 73 | works, such as semiconductor masks. 74 | 75 | “The Program” refers to any copyrightable work licensed under this 76 | License. Each licensee is addressed as “you”. “Licensees” and 77 | “recipients” may be individuals or organizations. 78 | 79 | To “modify” a work means to copy from or adapt all or part of the work in 80 | a fashion requiring copyright permission, other than the making of an exact copy. The 81 | resulting work is called a “modified version” of the earlier work or a 82 | work “based on” the earlier work. 83 | 84 | A “covered work” means either the unmodified Program or a work based on 85 | the Program. 86 | 87 | To “propagate” a work means to do anything with it that, without 88 | permission, would make you directly or secondarily liable for infringement under 89 | applicable copyright law, except executing it on a computer or modifying a private 90 | copy. Propagation includes copying, distribution (with or without modification), 91 | making available to the public, and in some countries other activities as well. 92 | 93 | To “convey” a work means any kind of propagation that enables other 94 | parties to make or receive copies. Mere interaction with a user through a computer 95 | network, with no transfer of a copy, is not conveying. 96 | 97 | An interactive user interface displays “Appropriate Legal Notices” to the 98 | extent that it includes a convenient and prominently visible feature that **(1)** 99 | displays an appropriate copyright notice, and **(2)** tells the user that there is no 100 | warranty for the work (except to the extent that warranties are provided), that 101 | licensees may convey the work under this License, and how to view a copy of this 102 | License. If the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | ### 1. Source Code 106 | 107 | The “source code” for a work means the preferred form of the work for 108 | making modifications to it. “Object code” means any non-source form of a 109 | work. 110 | 111 | A “Standard Interface” means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of interfaces 113 | specified for a particular programming language, one that is widely used among 114 | developers working in that language. 115 | 116 | The “System Libraries” of an executable work include anything, other than 117 | the work as a whole, that **(a)** is included in the normal form of packaging a Major 118 | Component, but which is not part of that Major Component, and **(b)** serves only to 119 | enable use of the work with that Major Component, or to implement a Standard 120 | Interface for which an implementation is available to the public in source code form. 121 | A “Major Component”, in this context, means a major essential component 122 | (kernel, window system, and so on) of the specific operating system (if any) on which 123 | the executable work runs, or a compiler used to produce the work, or an object code 124 | interpreter used to run it. 125 | 126 | The “Corresponding Source” for a work in object code form means all the 127 | source code needed to generate, install, and (for an executable work) run the object 128 | code and to modify the work, including scripts to control those activities. However, 129 | it does not include the work's System Libraries, or general-purpose tools or 130 | generally available free programs which are used unmodified in performing those 131 | activities but which are not part of the work. For example, Corresponding Source 132 | includes interface definition files associated with source files for the work, and 133 | the source code for shared libraries and dynamically linked subprograms that the work 134 | is specifically designed to require, such as by intimate data communication or 135 | control flow between those subprograms and other parts of the work. 136 | 137 | The Corresponding Source need not include anything that users can regenerate 138 | automatically from other parts of the Corresponding Source. 139 | 140 | The Corresponding Source for a work in source code form is that same work. 141 | 142 | ### 2. Basic Permissions 143 | 144 | All rights granted under this License are granted for the term of copyright on the 145 | Program, and are irrevocable provided the stated conditions are met. This License 146 | explicitly affirms your unlimited permission to run the unmodified Program. The 147 | output from running a covered work is covered by this License only if the output, 148 | given its content, constitutes a covered work. This License acknowledges your rights 149 | of fair use or other equivalent, as provided by copyright law. 150 | 151 | You may make, run and propagate covered works that you do not convey, without 152 | conditions so long as your license otherwise remains in force. You may convey covered 153 | works to others for the sole purpose of having them make modifications exclusively 154 | for you, or provide you with facilities for running those works, provided that you 155 | comply with the terms of this License in conveying all material for which you do not 156 | control copyright. Those thus making or running the covered works for you must do so 157 | exclusively on your behalf, under your direction and control, on terms that prohibit 158 | them from making any copies of your copyrighted material outside their relationship 159 | with you. 160 | 161 | Conveying under any other circumstances is permitted solely under the conditions 162 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 163 | 164 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law 165 | 166 | No covered work shall be deemed part of an effective technological measure under any 167 | applicable law fulfilling obligations under article 11 of the WIPO copyright treaty 168 | adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention 169 | of such measures. 170 | 171 | When you convey a covered work, you waive any legal power to forbid circumvention of 172 | technological measures to the extent such circumvention is effected by exercising 173 | rights under this License with respect to the covered work, and you disclaim any 174 | intention to limit operation or modification of the work as a means of enforcing, 175 | against the work's users, your or third parties' legal rights to forbid circumvention 176 | of technological measures. 177 | 178 | ### 4. Conveying Verbatim Copies 179 | 180 | You may convey verbatim copies of the Program's source code as you receive it, in any 181 | medium, provided that you conspicuously and appropriately publish on each copy an 182 | appropriate copyright notice; keep intact all notices stating that this License and 183 | any non-permissive terms added in accord with section 7 apply to the code; keep 184 | intact all notices of the absence of any warranty; and give all recipients a copy of 185 | this License along with the Program. 186 | 187 | You may charge any price or no price for each copy that you convey, and you may offer 188 | support or warranty protection for a fee. 189 | 190 | ### 5. Conveying Modified Source Versions 191 | 192 | You may convey a work based on the Program, or the modifications to produce it from 193 | the Program, in the form of source code under the terms of section 4, provided that 194 | you also meet all of these conditions: 195 | 196 | * **a)** The work must carry prominent notices stating that you modified it, and giving a 197 | relevant date. 198 | * **b)** The work must carry prominent notices stating that it is released under this 199 | License and any conditions added under section 7. This requirement modifies the 200 | requirement in section 4 to “keep intact all notices”. 201 | * **c)** You must license the entire work, as a whole, under this License to anyone who 202 | comes into possession of a copy. This License will therefore apply, along with any 203 | applicable section 7 additional terms, to the whole of the work, and all its parts, 204 | regardless of how they are packaged. This License gives no permission to license the 205 | work in any other way, but it does not invalidate such permission if you have 206 | separately received it. 207 | * **d)** If the work has interactive user interfaces, each must display Appropriate Legal 208 | Notices; however, if the Program has interactive interfaces that do not display 209 | Appropriate Legal Notices, your work need not make them do so. 210 | 211 | A compilation of a covered work with other separate and independent works, which are 212 | not by their nature extensions of the covered work, and which are not combined with 213 | it such as to form a larger program, in or on a volume of a storage or distribution 214 | medium, is called an “aggregate” if the compilation and its resulting 215 | copyright are not used to limit the access or legal rights of the compilation's users 216 | beyond what the individual works permit. Inclusion of a covered work in an aggregate 217 | does not cause this License to apply to the other parts of the aggregate. 218 | 219 | ### 6. Conveying Non-Source Forms 220 | 221 | You may convey a covered work in object code form under the terms of sections 4 and 222 | 5, provided that you also convey the machine-readable Corresponding Source under the 223 | terms of this License, in one of these ways: 224 | 225 | * **a)** Convey the object code in, or embodied in, a physical product (including a 226 | physical distribution medium), accompanied by the Corresponding Source fixed on a 227 | durable physical medium customarily used for software interchange. 228 | * **b)** Convey the object code in, or embodied in, a physical product (including a 229 | physical distribution medium), accompanied by a written offer, valid for at least 230 | three years and valid for as long as you offer spare parts or customer support for 231 | that product model, to give anyone who possesses the object code either **(1)** a copy of 232 | the Corresponding Source for all the software in the product that is covered by this 233 | License, on a durable physical medium customarily used for software interchange, for 234 | a price no more than your reasonable cost of physically performing this conveying of 235 | source, or **(2)** access to copy the Corresponding Source from a network server at no 236 | charge. 237 | * **c)** Convey individual copies of the object code with a copy of the written offer to 238 | provide the Corresponding Source. This alternative is allowed only occasionally and 239 | noncommercially, and only if you received the object code with such an offer, in 240 | accord with subsection 6b. 241 | * **d)** Convey the object code by offering access from a designated place (gratis or for 242 | a charge), and offer equivalent access to the Corresponding Source in the same way 243 | through the same place at no further charge. You need not require recipients to copy 244 | the Corresponding Source along with the object code. If the place to copy the object 245 | code is a network server, the Corresponding Source may be on a different server 246 | (operated by you or a third party) that supports equivalent copying facilities, 247 | provided you maintain clear directions next to the object code saying where to find 248 | the Corresponding Source. Regardless of what server hosts the Corresponding Source, 249 | you remain obligated to ensure that it is available for as long as needed to satisfy 250 | these requirements. 251 | * **e)** Convey the object code using peer-to-peer transmission, provided you inform 252 | other peers where the object code and Corresponding Source of the work are being 253 | offered to the general public at no charge under subsection 6d. 254 | 255 | A separable portion of the object code, whose source code is excluded from the 256 | Corresponding Source as a System Library, need not be included in conveying the 257 | object code work. 258 | 259 | A “User Product” is either **(1)** a “consumer product”, which 260 | means any tangible personal property which is normally used for personal, family, or 261 | household purposes, or **(2)** anything designed or sold for incorporation into a 262 | dwelling. In determining whether a product is a consumer product, doubtful cases 263 | shall be resolved in favor of coverage. For a particular product received by a 264 | particular user, “normally used” refers to a typical or common use of 265 | that class of product, regardless of the status of the particular user or of the way 266 | in which the particular user actually uses, or expects or is expected to use, the 267 | product. A product is a consumer product regardless of whether the product has 268 | substantial commercial, industrial or non-consumer uses, unless such uses represent 269 | the only significant mode of use of the product. 270 | 271 | “Installation Information” for a User Product means any methods, 272 | procedures, authorization keys, or other information required to install and execute 273 | modified versions of a covered work in that User Product from a modified version of 274 | its Corresponding Source. The information must suffice to ensure that the continued 275 | functioning of the modified object code is in no case prevented or interfered with 276 | solely because modification has been made. 277 | 278 | If you convey an object code work under this section in, or with, or specifically for 279 | use in, a User Product, and the conveying occurs as part of a transaction in which 280 | the right of possession and use of the User Product is transferred to the recipient 281 | in perpetuity or for a fixed term (regardless of how the transaction is 282 | characterized), the Corresponding Source conveyed under this section must be 283 | accompanied by the Installation Information. But this requirement does not apply if 284 | neither you nor any third party retains the ability to install modified object code 285 | on the User Product (for example, the work has been installed in ROM). 286 | 287 | The requirement to provide Installation Information does not include a requirement to 288 | continue to provide support service, warranty, or updates for a work that has been 289 | modified or installed by the recipient, or for the User Product in which it has been 290 | modified or installed. Access to a network may be denied when the modification itself 291 | materially and adversely affects the operation of the network or violates the rules 292 | and protocols for communication across the network. 293 | 294 | Corresponding Source conveyed, and Installation Information provided, in accord with 295 | this section must be in a format that is publicly documented (and with an 296 | implementation available to the public in source code form), and must require no 297 | special password or key for unpacking, reading or copying. 298 | 299 | ### 7. Additional Terms 300 | 301 | “Additional permissions” are terms that supplement the terms of this 302 | License by making exceptions from one or more of its conditions. Additional 303 | permissions that are applicable to the entire Program shall be treated as though they 304 | were included in this License, to the extent that they are valid under applicable 305 | law. If additional permissions apply only to part of the Program, that part may be 306 | used separately under those permissions, but the entire Program remains governed by 307 | this License without regard to the additional permissions. 308 | 309 | When you convey a copy of a covered work, you may at your option remove any 310 | additional permissions from that copy, or from any part of it. (Additional 311 | permissions may be written to require their own removal in certain cases when you 312 | modify the work.) You may place additional permissions on material, added by you to a 313 | covered work, for which you have or can give appropriate copyright permission. 314 | 315 | Notwithstanding any other provision of this License, for material you add to a 316 | covered work, you may (if authorized by the copyright holders of that material) 317 | supplement the terms of this License with terms: 318 | 319 | * **a)** Disclaiming warranty or limiting liability differently from the terms of 320 | sections 15 and 16 of this License; or 321 | * **b)** Requiring preservation of specified reasonable legal notices or author 322 | attributions in that material or in the Appropriate Legal Notices displayed by works 323 | containing it; or 324 | * **c)** Prohibiting misrepresentation of the origin of that material, or requiring that 325 | modified versions of such material be marked in reasonable ways as different from the 326 | original version; or 327 | * **d)** Limiting the use for publicity purposes of names of licensors or authors of the 328 | material; or 329 | * **e)** Declining to grant rights under trademark law for use of some trade names, 330 | trademarks, or service marks; or 331 | * **f)** Requiring indemnification of licensors and authors of that material by anyone 332 | who conveys the material (or modified versions of it) with contractual assumptions of 333 | liability to the recipient, for any liability that these contractual assumptions 334 | directly impose on those licensors and authors. 335 | 336 | All other non-permissive additional terms are considered “further 337 | restrictions” within the meaning of section 10. If the Program as you received 338 | it, or any part of it, contains a notice stating that it is governed by this License 339 | along with a term that is a further restriction, you may remove that term. If a 340 | license document contains a further restriction but permits relicensing or conveying 341 | under this License, you may add to a covered work material governed by the terms of 342 | that license document, provided that the further restriction does not survive such 343 | relicensing or conveying. 344 | 345 | If you add terms to a covered work in accord with this section, you must place, in 346 | the relevant source files, a statement of the additional terms that apply to those 347 | files, or a notice indicating where to find the applicable terms. 348 | 349 | Additional terms, permissive or non-permissive, may be stated in the form of a 350 | separately written license, or stated as exceptions; the above requirements apply 351 | either way. 352 | 353 | ### 8. Termination 354 | 355 | You may not propagate or modify a covered work except as expressly provided under 356 | this License. Any attempt otherwise to propagate or modify it is void, and will 357 | automatically terminate your rights under this License (including any patent licenses 358 | granted under the third paragraph of section 11). 359 | 360 | However, if you cease all violation of this License, then your license from a 361 | particular copyright holder is reinstated **(a)** provisionally, unless and until the 362 | copyright holder explicitly and finally terminates your license, and **(b)** permanently, 363 | if the copyright holder fails to notify you of the violation by some reasonable means 364 | prior to 60 days after the cessation. 365 | 366 | Moreover, your license from a particular copyright holder is reinstated permanently 367 | if the copyright holder notifies you of the violation by some reasonable means, this 368 | is the first time you have received notice of violation of this License (for any 369 | work) from that copyright holder, and you cure the violation prior to 30 days after 370 | your receipt of the notice. 371 | 372 | Termination of your rights under this section does not terminate the licenses of 373 | parties who have received copies or rights from you under this License. If your 374 | rights have been terminated and not permanently reinstated, you do not qualify to 375 | receive new licenses for the same material under section 10. 376 | 377 | ### 9. Acceptance Not Required for Having Copies 378 | 379 | You are not required to accept this License in order to receive or run a copy of the 380 | Program. Ancillary propagation of a covered work occurring solely as a consequence of 381 | using peer-to-peer transmission to receive a copy likewise does not require 382 | acceptance. However, nothing other than this License grants you permission to 383 | propagate or modify any covered work. These actions infringe copyright if you do not 384 | accept this License. Therefore, by modifying or propagating a covered work, you 385 | indicate your acceptance of this License to do so. 386 | 387 | ### 10. Automatic Licensing of Downstream Recipients 388 | 389 | Each time you convey a covered work, the recipient automatically receives a license 390 | from the original licensors, to run, modify and propagate that work, subject to this 391 | License. You are not responsible for enforcing compliance by third parties with this 392 | License. 393 | 394 | An “entity transaction” is a transaction transferring control of an 395 | organization, or substantially all assets of one, or subdividing an organization, or 396 | merging organizations. If propagation of a covered work results from an entity 397 | transaction, each party to that transaction who receives a copy of the work also 398 | receives whatever licenses to the work the party's predecessor in interest had or 399 | could give under the previous paragraph, plus a right to possession of the 400 | Corresponding Source of the work from the predecessor in interest, if the predecessor 401 | has it or can get it with reasonable efforts. 402 | 403 | You may not impose any further restrictions on the exercise of the rights granted or 404 | affirmed under this License. For example, you may not impose a license fee, royalty, 405 | or other charge for exercise of rights granted under this License, and you may not 406 | initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging 407 | that any patent claim is infringed by making, using, selling, offering for sale, or 408 | importing the Program or any portion of it. 409 | 410 | ### 11. Patents 411 | 412 | A “contributor” is a copyright holder who authorizes use under this 413 | License of the Program or a work on which the Program is based. The work thus 414 | licensed is called the contributor's “contributor version”. 415 | 416 | A contributor's “essential patent claims” are all patent claims owned or 417 | controlled by the contributor, whether already acquired or hereafter acquired, that 418 | would be infringed by some manner, permitted by this License, of making, using, or 419 | selling its contributor version, but do not include claims that would be infringed 420 | only as a consequence of further modification of the contributor version. For 421 | purposes of this definition, “control” includes the right to grant patent 422 | sublicenses in a manner consistent with the requirements of this License. 423 | 424 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license 425 | under the contributor's essential patent claims, to make, use, sell, offer for sale, 426 | import and otherwise run, modify and propagate the contents of its contributor 427 | version. 428 | 429 | In the following three paragraphs, a “patent license” is any express 430 | agreement or commitment, however denominated, not to enforce a patent (such as an 431 | express permission to practice a patent or covenant not to sue for patent 432 | infringement). To “grant” such a patent license to a party means to make 433 | such an agreement or commitment not to enforce a patent against the party. 434 | 435 | If you convey a covered work, knowingly relying on a patent license, and the 436 | Corresponding Source of the work is not available for anyone to copy, free of charge 437 | and under the terms of this License, through a publicly available network server or 438 | other readily accessible means, then you must either **(1)** cause the Corresponding 439 | Source to be so available, or **(2)** arrange to deprive yourself of the benefit of the 440 | patent license for this particular work, or **(3)** arrange, in a manner consistent with 441 | the requirements of this License, to extend the patent license to downstream 442 | recipients. “Knowingly relying” means you have actual knowledge that, but 443 | for the patent license, your conveying the covered work in a country, or your 444 | recipient's use of the covered work in a country, would infringe one or more 445 | identifiable patents in that country that you have reason to believe are valid. 446 | 447 | If, pursuant to or in connection with a single transaction or arrangement, you 448 | convey, or propagate by procuring conveyance of, a covered work, and grant a patent 449 | license to some of the parties receiving the covered work authorizing them to use, 450 | propagate, modify or convey a specific copy of the covered work, then the patent 451 | license you grant is automatically extended to all recipients of the covered work and 452 | works based on it. 453 | 454 | A patent license is “discriminatory” if it does not include within the 455 | scope of its coverage, prohibits the exercise of, or is conditioned on the 456 | non-exercise of one or more of the rights that are specifically granted under this 457 | License. You may not convey a covered work if you are a party to an arrangement with 458 | a third party that is in the business of distributing software, under which you make 459 | payment to the third party based on the extent of your activity of conveying the 460 | work, and under which the third party grants, to any of the parties who would receive 461 | the covered work from you, a discriminatory patent license **(a)** in connection with 462 | copies of the covered work conveyed by you (or copies made from those copies), or **(b)** 463 | primarily for and in connection with specific products or compilations that contain 464 | the covered work, unless you entered into that arrangement, or that patent license 465 | was granted, prior to 28 March 2007. 466 | 467 | Nothing in this License shall be construed as excluding or limiting any implied 468 | license or other defenses to infringement that may otherwise be available to you 469 | under applicable patent law. 470 | 471 | ### 12. No Surrender of Others' Freedom 472 | 473 | If conditions are imposed on you (whether by court order, agreement or otherwise) 474 | that contradict the conditions of this License, they do not excuse you from the 475 | conditions of this License. If you cannot convey a covered work so as to satisfy 476 | simultaneously your obligations under this License and any other pertinent 477 | obligations, then as a consequence you may not convey it at all. For example, if you 478 | agree to terms that obligate you to collect a royalty for further conveying from 479 | those to whom you convey the Program, the only way you could satisfy both those terms 480 | and this License would be to refrain entirely from conveying the Program. 481 | 482 | ### 13. Use with the GNU Affero General Public License 483 | 484 | Notwithstanding any other provision of this License, you have permission to link or 485 | combine any covered work with a work licensed under version 3 of the GNU Affero 486 | General Public License into a single combined work, and to convey the resulting work. 487 | The terms of this License will continue to apply to the part which is the covered 488 | work, but the special requirements of the GNU Affero General Public License, section 489 | 13, concerning interaction through a network will apply to the combination as such. 490 | 491 | ### 14. Revised Versions of this License 492 | 493 | The Free Software Foundation may publish revised and/or new versions of the GNU 494 | General Public License from time to time. Such new versions will be similar in spirit 495 | to the present version, but may differ in detail to address new problems or concerns. 496 | 497 | Each version is given a distinguishing version number. If the Program specifies that 498 | a certain numbered version of the GNU General Public License “or any later 499 | version” applies to it, you have the option of following the terms and 500 | conditions either of that numbered version or of any later version published by the 501 | Free Software Foundation. If the Program does not specify a version number of the GNU 502 | General Public License, you may choose any version ever published by the Free 503 | Software Foundation. 504 | 505 | If the Program specifies that a proxy can decide which future versions of the GNU 506 | General Public License can be used, that proxy's public statement of acceptance of a 507 | version permanently authorizes you to choose that version for the Program. 508 | 509 | Later license versions may give you additional or different permissions. However, no 510 | additional obligations are imposed on any author or copyright holder as a result of 511 | your choosing to follow a later version. 512 | 513 | ### 15. Disclaimer of Warranty 514 | 515 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 516 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 517 | PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER 518 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 519 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 520 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 521 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 522 | 523 | ### 16. Limitation of Liability 524 | 525 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 526 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 527 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 528 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 529 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE 530 | OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE 531 | WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 532 | POSSIBILITY OF SUCH DAMAGES. 533 | 534 | ### 17. Interpretation of Sections 15 and 16 535 | 536 | If the disclaimer of warranty and limitation of liability provided above cannot be 537 | given local legal effect according to their terms, reviewing courts shall apply local 538 | law that most closely approximates an absolute waiver of all civil liability in 539 | connection with the Program, unless a warranty or assumption of liability accompanies 540 | a copy of the Program in return for a fee. 541 | 542 | _END OF TERMS AND CONDITIONS_ 543 | 544 | ## How to Apply These Terms to Your New Programs 545 | 546 | If you develop a new program, and you want it to be of the greatest possible use to 547 | the public, the best way to achieve this is to make it free software which everyone 548 | can redistribute and change under these terms. 549 | 550 | To do so, attach the following notices to the program. It is safest to attach them 551 | to the start of each source file to most effectively state the exclusion of warranty; 552 | and each file should have at least the “copyright” line and a pointer to 553 | where the full notice is found. 554 | 555 | 556 | Copyright (C) 557 | 558 | This program is free software: you can redistribute it and/or modify 559 | it under the terms of the GNU General Public License as published by 560 | the Free Software Foundation, either version 3 of the License, or 561 | (at your option) any later version. 562 | 563 | This program is distributed in the hope that it will be useful, 564 | but WITHOUT ANY WARRANTY; without even the implied warranty of 565 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 566 | GNU General Public License for more details. 567 | 568 | You should have received a copy of the GNU General Public License 569 | along with this program. If not, see . 570 | 571 | Also add information on how to contact you by electronic and paper mail. 572 | 573 | If the program does terminal interaction, make it output a short notice like this 574 | when it starts in an interactive mode: 575 | 576 | Copyright (C) 577 | This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. 578 | This is free software, and you are welcome to redistribute it 579 | under certain conditions; type 'show c' for details. 580 | 581 | The hypothetical commands `show w` and `show c` should show the appropriate parts of 582 | the General Public License. Of course, your program's commands might be different; 583 | for a GUI interface, you would use an “about box”. 584 | 585 | You should also get your employer (if you work as a programmer) or school, if any, to 586 | sign a “copyright disclaimer” for the program, if necessary. For more 587 | information on this, and how to apply and follow the GNU GPL, see 588 | <>. 589 | 590 | The GNU General Public License does not permit incorporating your program into 591 | proprietary programs. If your program is a subroutine library, you may consider it 592 | more useful to permit linking proprietary applications with the library. If this is 593 | what you want to do, use the GNU Lesser General Public License instead of this 594 | License. But first, please read 595 | <>. 596 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ext4 2 | Little library for reading ext4 file systems. Most of functions are documented, so simply use Python's help function. Here are some usage examples: 3 | 4 | Opening a volume: 5 | 6 | >>> import ext4 7 | >>> file = open("example.img", "rb") 8 | >>> volume = ext4.Volume(file, offset = 0) 9 | 10 | >>> print(f"Volume {volume.uuid:s} has block size {volume.block_size:d}") 11 | Volume 3C09AE31-A105-45F9-80D0-6062DABDA0EE has block size 1024 12 | 13 | Configure flag and magic checking: 14 | 15 | >>> volume.ignore_flags = False 16 | >>> volume.ignore_magic = False 17 | 18 | Iterating over directory entries: 19 | 20 | >>> example_dir = volume.root.get_inode("example_dir") 21 | 22 | >>> # on-disk order 23 | >>> for file_name, inode_idx, file_type in example_dir.open_dir(): 24 | ... print(file_name) 25 | . 26 | .. 27 | example_file 28 | example_image.jpg 29 | 30 | >>> # sorted 31 | >>> for file_name, inode_idx, file_type in sorted(example_dir.open_dir(), key = ext4.Inode.directory_entry_key): 32 | >>> print(file_name) 33 | 34 | >>> # Fancy and customizable 35 | >>> ext4.Tools.list_dir(volume, example_dir) 36 | drwxr-xr-x 1.00 KiB . 37 | drwxr-xr-x 1.00 KiB .. 38 | -rw-r--r-- 12 bytes example_file 39 | -rw-r--r-- 66.69 KiB example_image.jpg 40 | 41 | Getting an inode by its index: 42 | 43 | >>> root = volume.get_inode(ext4.Volume.ROOT_INODE, ext4.InodeType.DIRECTORY) # == volume.root 44 | 45 | Getting an inode by its path: 46 | 47 | >>> # /example_dir/example_image.jpg 48 | >>> example_image = root.get_inode("example_dir", "example_image.jpg") 49 | >>> # or 50 | >>> example_image = example_dir.get_inode("example_image.jpg") 51 | 52 | Getting information like size or mode: 53 | 54 | >>> print(f"example_img.jpg is {example_image.inode.i_size:d} bytes in size") 55 | example_img.jpg is 68288 bytes in size 56 | >>> print(f"example_img.jpg is {example_image.size_readable:s} in size") 57 | example_img.jpg is 66.69 KiB in size 58 | >>> print(f"The mode of example_img.jpg is {example_image.mode_str:s}") 59 | The mode of example_img.jpg is -rw-r--r-- 60 | 61 | Reading the contents of an inode: 62 | 63 | >>> reader = example_image.open_read() # Either ext4.BlockReader or io.BytesIO 64 | >>> raw = reader.read() 65 | 66 | >>> symbolic_link = root.get_inode("example_symlink") 67 | >>> symbolic_link.open_read().read().decode("utf8") 68 | 'example_dir/example_image.jpg' 69 | 70 | Getting a list of all extended attributes: 71 | 72 | >>> list(example_dir.xattrs()) 73 | [('user.example_attrib', b'some value'), ('security.unsecure', b'maybe')] -------------------------------------------------------------------------------- /example.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubinator/ext4/d838217da5e60277e5430b69f57c17a3a06fd2ea/example.img -------------------------------------------------------------------------------- /ext4.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import functools 3 | import io 4 | import math 5 | import queue 6 | 7 | 8 | 9 | ######################################################################################################################## 10 | ##################################################### HELPERS ###################################################### 11 | ######################################################################################################################## 12 | 13 | def wcscmp (str_a, str_b): 14 | """ 15 | Standard library wcscmp 16 | """ 17 | for a, b in zip(str_a, str_b): 18 | tmp = ord(a) - ord(b) 19 | if tmp != 0: return -1 if tmp < 0 else 1 20 | 21 | tmp = len(str_a) - len(str_b) 22 | return -1 if tmp < 0 else 1 if tmp > 0 else 0 23 | 24 | 25 | 26 | ######################################################################################################################## 27 | #################################################### EXCEPTIONS #################################################### 28 | ######################################################################################################################## 29 | 30 | class Ext4Error (Exception): 31 | """ 32 | Base class for all custom errors 33 | """ 34 | pass 35 | 36 | class BlockMapError (Ext4Error): 37 | """ 38 | Raised, when a requested file_block is not mapped to disk 39 | """ 40 | pass 41 | 42 | class EndOfStreamError (Ext4Error): 43 | """ 44 | Raised, when BlockReader reads beyond the end of the volume's underlying stream 45 | """ 46 | pass 47 | 48 | class MagicError (Ext4Error): 49 | """ 50 | Raised, when a structures magic value is wrong and ignore_magic is False 51 | """ 52 | pass 53 | 54 | 55 | 56 | ######################################################################################################################## 57 | #################################################### LOW LEVEL ##################################################### 58 | ######################################################################################################################## 59 | 60 | class ext4_struct (ctypes.LittleEndianStructure): 61 | """ 62 | Simplifies access to *_lo and *_hi fields 63 | """ 64 | def __getattr__ (self, name): 65 | """ 66 | Enables reading *_lo and *_hi fields together. 67 | """ 68 | try: 69 | # Combining *_lo and *_hi fields 70 | lo_field = ctypes.LittleEndianStructure.__getattribute__(type(self), name + "_lo") 71 | size = lo_field.size 72 | 73 | lo = lo_field.__get__(self) 74 | hi = ctypes.LittleEndianStructure.__getattribute__(self, name + "_hi") 75 | 76 | return (hi << (8 * size)) | lo 77 | except AttributeError: 78 | return ctypes.LittleEndianStructure.__getattribute__(self, name) 79 | 80 | def __setattr__ (self, name, value): 81 | """ 82 | Enables setting *_lo and *_hi fields together. 83 | """ 84 | try: 85 | # Combining *_lo and *_hi fields 86 | lo_field = lo_field = ctypes.LittleEndianStructure.__getattribute__(type(self), name + "_lo") 87 | size = lo_field.size 88 | 89 | lo_field.__set__(self, value & ((1 << (8 * size)) - 1)) 90 | ctypes.LittleEndianStructure.__setattr__(self, name + "_hi", value >> (8 * size)) 91 | except AttributeError: 92 | ctypes.LittleEndianStructure.__setattr__(self, name, value) 93 | 94 | 95 | 96 | class ext4_dir_entry_2 (ext4_struct): 97 | _fields_ = [ 98 | ("inode", ctypes.c_uint), # 0x0 99 | ("rec_len", ctypes.c_ushort), # 0x4 100 | ("name_len", ctypes.c_ubyte), # 0x6 101 | ("file_type", ctypes.c_ubyte) # 0x7 102 | # Variable length field "name" missing at 0x8 103 | ] 104 | 105 | def _from_buffer_copy (raw, offset = 0, platform64 = True): 106 | struct = ext4_dir_entry_2.from_buffer_copy(raw, offset) 107 | struct.name = raw[offset + 0x8 : offset + 0x8 + struct.name_len] 108 | return struct 109 | 110 | 111 | 112 | class ext4_extent (ext4_struct): 113 | _fields_ = [ 114 | ("ee_block", ctypes.c_uint), # 0x0000 115 | ("ee_len", ctypes.c_ushort), # 0x0004 116 | ("ee_start_hi", ctypes.c_ushort), # 0x0006 117 | ("ee_start_lo", ctypes.c_uint) # 0x0008 118 | ] 119 | 120 | 121 | 122 | class ext4_extent_header (ext4_struct): 123 | _fields_ = [ 124 | ("eh_magic", ctypes.c_ushort), # 0x0000, Must be 0xF30A 125 | ("eh_entries", ctypes.c_ushort), # 0x0002 126 | ("eh_max", ctypes.c_ushort), # 0x0004 127 | ("eh_depth", ctypes.c_ushort), # 0x0006 128 | ("eh_generation", ctypes.c_uint) # 0x0008 129 | ] 130 | 131 | 132 | 133 | class ext4_extent_idx (ext4_struct): 134 | _fields_ = [ 135 | ("ei_block", ctypes.c_uint), # 0x0000 136 | ("ei_leaf_lo", ctypes.c_uint), # 0x0004 137 | ("ei_leaf_hi", ctypes.c_ushort), # 0x0008 138 | ("ei_unused", ctypes.c_ushort) # 0x000A 139 | ] 140 | 141 | 142 | 143 | class ext4_group_descriptor (ext4_struct): 144 | _fields_ = [ 145 | ("bg_block_bitmap_lo", ctypes.c_uint), # 0x0000 146 | ("bg_inode_bitmap_lo", ctypes.c_uint), # 0x0004 147 | ("bg_inode_table_lo", ctypes.c_uint), # 0x0008 148 | ("bg_free_blocks_count_lo", ctypes.c_ushort), # 0x000C 149 | ("bg_free_inodes_count_lo", ctypes.c_ushort), # 0x000E 150 | ("bg_used_dirs_count_lo", ctypes.c_ushort), # 0x0010 151 | ("bg_flags", ctypes.c_ushort), # 0x0012 152 | ("bg_exclude_bitmap_lo", ctypes.c_uint), # 0x0014 153 | ("bg_block_bitmap_csum_lo", ctypes.c_ushort), # 0x0018 154 | ("bg_inode_bitmap_csum_lo", ctypes.c_ushort), # 0x001A 155 | ("bg_itable_unused_lo", ctypes.c_ushort), # 0x001C 156 | ("bg_checksum", ctypes.c_ushort), # 0x001E 157 | 158 | # 64-bit fields 159 | ("bg_block_bitmap_hi", ctypes.c_uint), # 0x0020 160 | ("bg_inode_bitmap_hi", ctypes.c_uint), # 0x0024 161 | ("bg_inode_table_hi", ctypes.c_uint), # 0x0028 162 | ("bg_free_blocks_count_hi", ctypes.c_ushort), # 0x002C 163 | ("bg_free_inodes_count_hi", ctypes.c_ushort), # 0x002E 164 | ("bg_used_dirs_count_hi", ctypes.c_ushort), # 0x0030 165 | ("bg_itable_unused_hi", ctypes.c_ushort), # 0x0032 166 | ("bg_exclude_bitmap_hi", ctypes.c_uint), # 0x0034 167 | ("bg_block_bitmap_csum_hi", ctypes.c_ushort), # 0x0038 168 | ("bg_inode_bitmap_csum_hi", ctypes.c_ushort), # 0x003A 169 | ("bg_reserved", ctypes.c_uint), # 0x003C 170 | ] 171 | 172 | def _from_buffer_copy (raw, offset = 0, platform64 = True): 173 | struct = ext4_group_descriptor.from_buffer_copy(raw, offset) 174 | 175 | if not platform64: 176 | struct.bg_block_bitmap_hi = 0 177 | struct.bg_inode_bitmap_hi = 0 178 | struct.bg_inode_table_hi = 0 179 | struct.bg_free_blocks_count_hi = 0 180 | struct.bg_free_inodes_count_hi = 0 181 | struct.bg_used_dirs_count_hi = 0 182 | struct.bg_itable_unused_hi = 0 183 | struct.bg_exclude_bitmap_hi = 0 184 | struct.bg_block_bitmap_csum_hi = 0 185 | struct.bg_inode_bitmap_csum_hi = 0 186 | struct.bg_reserved = 0 187 | 188 | return struct 189 | 190 | 191 | 192 | class ext4_inode (ext4_struct): 193 | EXT2_GOOD_OLD_INODE_SIZE = 128 # Every field passing 128 bytes is "additional data", whose size is specified by i_extra_isize. 194 | 195 | # i_mode 196 | S_IXOTH = 0x1 # Others can execute 197 | S_IWOTH = 0x2 # Others can write 198 | S_IROTH = 0x4 # Others can read 199 | S_IXGRP = 0x8 # Group can execute 200 | S_IWGRP = 0x10 # Group can write 201 | S_IRGRP = 0x20 # Group can read 202 | S_IXUSR = 0x40 # Owner can execute 203 | S_IWUSR = 0x80 # Owner can write 204 | S_IRUSR = 0x100 # Owner can read 205 | S_ISVTX = 0x200 # Sticky bit (only owner can delete) 206 | S_ISGID = 0x400 # Set GID (execute with privileges of group owner of the file's group) 207 | S_ISUID = 0x800 # Set UID (execute with privileges of the file's owner) 208 | S_IFIFO = 0x1000 # FIFO device (named pipe) 209 | S_IFCHR = 0x2000 # Character device (raw, unbuffered, aligned, direct access to hardware storage) 210 | S_IFDIR = 0x4000 # Directory 211 | S_IFBLK = 0x6000 # Block device (buffered, arbitrary access to storage) 212 | S_IFREG = 0x8000 # Regular file 213 | S_IFLNK = 0xA000 # Symbolic link 214 | S_IFSOCK = 0xC000 # Socket 215 | 216 | # i_flags 217 | EXT4_INDEX_FL = 0x1000 # Uses hash trees 218 | EXT4_EXTENTS_FL = 0x80000 # Uses extents 219 | EXT4_EA_INODE_FL = 0x200000 # Inode stores large xattr 220 | EXT4_INLINE_DATA_FL = 0x10000000 # Has inline data 221 | 222 | _fields_ = [ 223 | ("i_mode", ctypes.c_ushort), # 0x0000 224 | ("i_uid_lo", ctypes.c_ushort), # 0x0002, Originally named i_uid 225 | ("i_size_lo", ctypes.c_uint), # 0x0004 226 | ("i_atime", ctypes.c_uint), # 0x0008 227 | ("i_ctime", ctypes.c_uint), # 0x000C 228 | ("i_mtime", ctypes.c_uint), # 0x0010 229 | ("i_dtime", ctypes.c_uint), # 0x0014 230 | ("i_gid_lo", ctypes.c_ushort), # 0x0018, Originally named i_gid 231 | ("i_links_count", ctypes.c_ushort), # 0x001A 232 | ("i_blocks_lo", ctypes.c_uint), # 0x001C 233 | ("i_flags", ctypes.c_uint), # 0x0020 234 | ("osd1", ctypes.c_uint), # 0x0024 235 | ("i_block", ctypes.c_uint * 15), # 0x0028 236 | ("i_generation", ctypes.c_uint), # 0x0064 237 | ("i_file_acl_lo", ctypes.c_uint), # 0x0068 238 | ("i_size_hi", ctypes.c_uint), # 0x006C, Originally named i_size_high 239 | ("i_obso_faddr", ctypes.c_uint), # 0x0070 240 | ("i_osd2_blocks_high", ctypes.c_ushort), # 0x0074, Originally named i_osd2.linux2.l_i_blocks_high 241 | ("i_file_acl_hi", ctypes.c_ushort), # 0x0076, Originally named i_osd2.linux2.l_i_file_acl_high 242 | ("i_uid_hi", ctypes.c_ushort), # 0x0078, Originally named i_osd2.linux2.l_i_uid_high 243 | ("i_gid_hi", ctypes.c_ushort), # 0x007A, Originally named i_osd2.linux2.l_i_gid_high 244 | ("i_osd2_checksum_lo", ctypes.c_ushort), # 0x007C, Originally named i_osd2.linux2.l_i_checksum_lo 245 | ("i_osd2_reserved", ctypes.c_ushort), # 0x007E, Originally named i_osd2.linux2.l_i_reserved 246 | ("i_extra_isize", ctypes.c_ushort), # 0x0080 247 | ("i_checksum_hi", ctypes.c_ushort), # 0x0082 248 | ("i_ctime_extra", ctypes.c_uint), # 0x0084 249 | ("i_mtime_extra", ctypes.c_uint), # 0x0088 250 | ("i_atime_extra", ctypes.c_uint), # 0x008C 251 | ("i_crtime", ctypes.c_uint), # 0x0090 252 | ("i_crtime_extra", ctypes.c_uint), # 0x0094 253 | ("i_version_hi", ctypes.c_uint), # 0x0098 254 | ("i_projid", ctypes.c_uint), # 0x009C 255 | ] 256 | 257 | 258 | 259 | class ext4_superblock (ext4_struct): 260 | EXT2_MIN_DESC_SIZE = 0x20 # Default value for s_desc_size, if INCOMPAT_64BIT is not set (NEEDS CONFIRMATION) 261 | EXT2_MIN_DESC_SIZE_64BIT = 0x40 # Default value for s_desc_size, if INCOMPAT_64BIT is set 262 | 263 | # s_feature_incompat 264 | INCOMPAT_64BIT = 0x80 # Uses 64-bit features (e.g. *_hi structure fields in ext4_group_descriptor) 265 | INCOMPAT_FILETYPE = 0x2 # Directory entries record file type (instead of inode flags) 266 | 267 | _fields_ = [ 268 | ("s_inodes_count", ctypes.c_uint), # 0x0000 269 | ("s_blocks_count_lo", ctypes.c_uint), # 0x0004 270 | ("s_r_blocks_count_lo", ctypes.c_uint), # 0x0008 271 | ("s_free_blocks_count_lo", ctypes.c_uint), # 0x000C 272 | ("s_free_inodes_count", ctypes.c_uint), # 0x0010 273 | ("s_first_data_block", ctypes.c_uint), # 0x0014 274 | ("s_log_block_size", ctypes.c_uint), # 0x0018 275 | ("s_log_cluster_size", ctypes.c_uint), # 0x001C 276 | ("s_blocks_per_group", ctypes.c_uint), # 0x0020 277 | ("s_clusters_per_group", ctypes.c_uint), # 0x0024 278 | ("s_inodes_per_group", ctypes.c_uint), # 0x0028 279 | ("s_mtime", ctypes.c_uint), # 0x002C 280 | ("s_wtime", ctypes.c_uint), # 0x0030 281 | ("s_mnt_count", ctypes.c_ushort), # 0x0034 282 | ("s_max_mnt_count", ctypes.c_ushort), # 0x0036 283 | ("s_magic", ctypes.c_ushort), # 0x0038, Must be 0xEF53 284 | ("s_state", ctypes.c_ushort), # 0x003A 285 | ("s_errors", ctypes.c_ushort), # 0x003C 286 | ("s_minor_rev_level", ctypes.c_ushort), # 0x003E 287 | ("s_lastcheck", ctypes.c_uint), # 0x0040 288 | ("s_checkinterval", ctypes.c_uint), # 0x0044 289 | ("s_creator_os", ctypes.c_uint), # 0x0048 290 | ("s_rev_level", ctypes.c_uint), # 0x004C 291 | ("s_def_resuid", ctypes.c_ushort), # 0x0050 292 | ("s_def_resgid", ctypes.c_ushort), # 0x0052 293 | ("s_first_ino", ctypes.c_uint), # 0x0054 294 | ("s_inode_size", ctypes.c_ushort), # 0x0058 295 | ("s_block_group_nr", ctypes.c_ushort), # 0x005A 296 | ("s_feature_compat", ctypes.c_uint), # 0x005C 297 | ("s_feature_incompat", ctypes.c_uint), # 0x0060 298 | ("s_feature_ro_compat", ctypes.c_uint), # 0x0064 299 | ("s_uuid", ctypes.c_ubyte * 16), # 0x0068 300 | ("s_volume_name", ctypes.c_char * 16), # 0x0078 301 | ("s_last_mounted", ctypes.c_char * 64), # 0x0088 302 | ("s_algorithm_usage_bitmap", ctypes.c_uint), # 0x00C8 303 | ("s_prealloc_blocks", ctypes.c_ubyte), # 0x00CC 304 | ("s_prealloc_dir_blocks", ctypes.c_ubyte), # 0x00CD 305 | ("s_reserved_gdt_blocks", ctypes.c_ushort), # 0x00CE 306 | ("s_journal_uuid", ctypes.c_ubyte * 16), # 0x00D0 307 | ("s_journal_inum", ctypes.c_uint), # 0x00E0 308 | ("s_journal_dev", ctypes.c_uint), # 0x00E4 309 | ("s_last_orphan", ctypes.c_uint), # 0x00E8 310 | ("s_hash_seed", ctypes.c_uint * 4), # 0x00EC 311 | ("s_def_hash_version", ctypes.c_ubyte), # 0x00FC 312 | ("s_jnl_backup_type", ctypes.c_ubyte), # 0x00FD 313 | ("s_desc_size", ctypes.c_ushort), # 0x00FE 314 | ("s_default_mount_opts", ctypes.c_uint), # 0x0100 315 | ("s_first_meta_bg", ctypes.c_uint), # 0x0104 316 | ("s_mkfs_time", ctypes.c_uint), # 0x0108 317 | ("s_jnl_blocks", ctypes.c_uint * 17), # 0x010C 318 | 319 | # 64-bit fields 320 | ("s_blocks_count_hi", ctypes.c_uint), # 0x0150 321 | ("s_r_blocks_count_hi", ctypes.c_uint), # 0x0154 322 | ("s_free_blocks_count_hi", ctypes.c_uint), # 0x0158 323 | ("s_min_extra_isize", ctypes.c_ushort), # 0x015C 324 | ("s_want_extra_isize", ctypes.c_ushort), # 0x015E 325 | ("s_flags", ctypes.c_uint), # 0x0160 326 | ("s_raid_stride", ctypes.c_ushort), # 0x0164 327 | ("s_mmp_interval", ctypes.c_ushort), # 0x0166 328 | ("s_mmp_block", ctypes.c_ulonglong), # 0x0168 329 | ("s_raid_stripe_width", ctypes.c_uint), # 0x0170 330 | ("s_log_groups_per_flex", ctypes.c_ubyte), # 0x0174 331 | ("s_checksum_type", ctypes.c_ubyte), # 0x0175 332 | ("s_reserved_pad", ctypes.c_ushort), # 0x0176 333 | ("s_kbytes_written", ctypes.c_ulonglong), # 0x0178 334 | ("s_snapshot_inum", ctypes.c_uint), # 0x0180 335 | ("s_snapshot_id", ctypes.c_uint), # 0x0184 336 | ("s_snapshot_r_blocks_count", ctypes.c_ulonglong), # 0x0188 337 | ("s_snapshot_list", ctypes.c_uint), # 0x0190 338 | ("s_error_count", ctypes.c_uint), # 0x0194 339 | ("s_first_error_time", ctypes.c_uint), # 0x0198 340 | ("s_first_error_ino", ctypes.c_uint), # 0x019C 341 | ("s_first_error_block", ctypes.c_ulonglong), # 0x01A0 342 | ("s_first_error_func", ctypes.c_ubyte * 32), # 0x01A8 343 | ("s_first_error_line", ctypes.c_uint), # 0x01C8 344 | ("s_last_error_time", ctypes.c_uint), # 0x01CC 345 | ("s_last_error_ino", ctypes.c_uint), # 0x01D0 346 | ("s_last_error_line", ctypes.c_uint), # 0x01D4 347 | ("s_last_error_block", ctypes.c_ulonglong), # 0x01D8 348 | ("s_last_error_func", ctypes.c_ubyte * 32), # 0x01E0 349 | ("s_mount_opts", ctypes.c_ubyte * 64), # 0x0200 350 | ("s_usr_quota_inum", ctypes.c_uint), # 0x0240 351 | ("s_grp_quota_inum", ctypes.c_uint), # 0x0244 352 | ("s_overhead_blocks", ctypes.c_uint), # 0x0248 353 | ("s_backup_bgs", ctypes.c_uint * 2), # 0x024C 354 | ("s_encrypt_algos", ctypes.c_ubyte * 4), # 0x0254 355 | ("s_encrypt_pw_salt", ctypes.c_ubyte * 16), # 0x0258 356 | ("s_lpf_ino", ctypes.c_uint), # 0x0268 357 | ("s_prj_quota_inum", ctypes.c_uint), # 0x026C 358 | ("s_checksum_seed", ctypes.c_uint), # 0x0270 359 | ("s_reserved", ctypes.c_uint * 98), # 0x0274 360 | ("s_checksum", ctypes.c_uint) # 0x03FC 361 | ] 362 | 363 | def _from_buffer_copy (raw, platform64 = True): 364 | struct = ext4_superblock.from_buffer_copy(raw) 365 | 366 | if not platform64: 367 | struct.s_blocks_count_hi = 0 368 | struct.s_r_blocks_count_hi = 0 369 | struct.s_free_blocks_count_hi = 0 370 | struct.s_min_extra_isize = 0 371 | struct.s_want_extra_isize = 0 372 | struct.s_flags = 0 373 | struct.s_raid_stride = 0 374 | struct.s_mmp_interval = 0 375 | struct.s_mmp_block = 0 376 | struct.s_raid_stripe_width = 0 377 | struct.s_log_groups_per_flex = 0 378 | struct.s_checksum_type = 0 379 | struct.s_reserved_pad = 0 380 | struct.s_kbytes_written = 0 381 | struct.s_snapshot_inum = 0 382 | struct.s_snapshot_id = 0 383 | struct.s_snapshot_r_blocks_count = 0 384 | struct.s_snapshot_list = 0 385 | struct.s_error_count = 0 386 | struct.s_first_error_time = 0 387 | struct.s_first_error_ino = 0 388 | struct.s_first_error_block = 0 389 | struct.s_first_error_func = 0 390 | struct.s_first_error_line = 0 391 | struct.s_last_error_time = 0 392 | struct.s_last_error_ino = 0 393 | struct.s_last_error_line = 0 394 | struct.s_last_error_block = 0 395 | struct.s_last_error_func = 0 396 | struct.s_mount_opts = 0 397 | struct.s_usr_quota_inum = 0 398 | struct.s_grp_quota_inum = 0 399 | struct.s_overhead_blocks = 0 400 | struct.s_backup_bgs = 0 401 | struct.s_encrypt_algos = 0 402 | struct.s_encrypt_pw_salt = 0 403 | struct.s_lpf_ino = 0 404 | struct.s_prj_quota_inum = 0 405 | struct.s_checksum_seed = 0 406 | struct.s_reserved = 0 407 | struct.s_checksum = 0 408 | 409 | if struct.s_desc_size == 0: 410 | if (struct.s_feature_incompat & ext4_superblock.INCOMPAT_64BIT) == 0: 411 | struct.s_desc_size = ext4_superblock.EXT2_MIN_DESC_SIZE 412 | else: 413 | struct.s_desc_size = ext4_superblock.EXT2_MIN_DESC_SIZE_64BIT 414 | 415 | return struct 416 | 417 | 418 | 419 | class ext4_xattr_entry (ext4_struct): 420 | _fields_ = [ 421 | ("e_name_len", ctypes.c_ubyte), # 0x00 422 | ("e_name_index", ctypes.c_ubyte), # 0x01 423 | ("e_value_offs", ctypes.c_ushort), # 0x02 424 | ("e_value_inum", ctypes.c_uint), # 0x04 425 | ("e_value_size", ctypes.c_uint), # 0x08 426 | ("e_hash", ctypes.c_uint) # 0x0C 427 | # Variable length field "e_name" missing at 0x10 428 | ] 429 | 430 | def _from_buffer_copy (raw, offset = 0, platform64 = True): 431 | struct = ext4_xattr_entry.from_buffer_copy(raw, offset) 432 | struct.e_name = raw[offset + 0x10 : offset + 0x10 + struct.e_name_len] 433 | return struct 434 | 435 | @property 436 | def _size (self): return 4 * ((ctypes.sizeof(type(self)) + self.e_name_len + 3) // 4) # 4-byte alignment 437 | 438 | 439 | 440 | class ext4_xattr_header (ext4_struct): 441 | _fields_ = [ 442 | ("h_magic", ctypes.c_uint), # 0x0, Must be 0xEA020000 443 | ("h_refcount", ctypes.c_uint), # 0x4 444 | ("h_blocks", ctypes.c_uint), # 0x8 445 | ("h_hash", ctypes.c_uint), # 0xC 446 | ("h_checksum", ctypes.c_uint), # 0x10 447 | ("h_reserved", ctypes.c_uint * 3), # 0x14 448 | ] 449 | 450 | 451 | 452 | class ext4_xattr_ibody_header (ext4_struct): 453 | _fields_ = [ 454 | ("h_magic", ctypes.c_uint) # 0x0, Must be 0xEA020000 455 | ] 456 | 457 | 458 | 459 | class InodeType: 460 | UNKNOWN = 0x0 # Unknown file type 461 | FILE = 0x1 # Regular file 462 | DIRECTORY = 0x2 # Directory 463 | CHARACTER_DEVICE = 0x3 # Character device 464 | BLOCK_DEVICE = 0x4 # Block device 465 | FIFO = 0x5 # FIFO 466 | SOCKET = 0x6 # Socket 467 | SYMBOLIC_LINK = 0x7 # Symbolic link 468 | CHECKSUM = 0xDE # Checksum entry; not really a file type, but a type of directory entry 469 | 470 | 471 | 472 | ######################################################################################################################## 473 | #################################################### HIGH LEVEL #################################################### 474 | ######################################################################################################################## 475 | 476 | class MappingEntry: 477 | """ 478 | Helper class: This class maps block_count file blocks indexed by file_block_idx to the associated disk blocks indexed 479 | by disk_block_idx. 480 | """ 481 | def __init__ (self, file_block_idx, disk_block_idx, block_count = 1): 482 | """ 483 | Initialize a MappingEntry instance with given file_block_idx, disk_block_idx and block_count. 484 | """ 485 | self.file_block_idx = file_block_idx 486 | self.disk_block_idx = disk_block_idx 487 | self.block_count = block_count 488 | 489 | def __iter__ (self): 490 | """ 491 | Can be used to convert an MappingEntry into a tuple (file_block_idx, disk_block_idx, block_count). 492 | """ 493 | yield self.file_block_idx 494 | yield self.disk_block_idx 495 | yield self.block_count 496 | 497 | def __repr__ (self): 498 | return f"{type(self).__name__:s}({self.file_block_idx!r:s}, {self.disk_block_idx!r:s}, {self.block_count!r:s})" 499 | 500 | def copy (self): 501 | return MappingEntry(self.file_block_idx, self.disk_block_idx, self.block_count) 502 | 503 | def create_mapping (*entries): 504 | """ 505 | Converts a list of 2-tuples (disk_block_idx, block_count) into a list of MappingEntry instances 506 | """ 507 | file_block_idx = 0 508 | result = [None] * len(entries) 509 | 510 | for i, entry in enumerate(entries): 511 | disk_block_idx, block_count = entry 512 | result[i] = MappingEntry(file_block_idx, disk_block_idx, block_count) 513 | file_block_idx += block_count 514 | 515 | return result 516 | 517 | def optimize (entries): 518 | """ 519 | Sorts and stiches together a list of MappingEntry instances 520 | """ 521 | entries.sort(key = lambda entry: entry.file_block_idx) 522 | 523 | idx = 0 524 | while idx < len(entries): 525 | while idx + 1 < len(entries) \ 526 | and entries[idx].file_block_idx + entries[idx].block_count == entries[idx + 1].file_block_idx \ 527 | and entries[idx].disk_block_idx + entries[idx].block_count == entries[idx + 1].disk_block_idx: 528 | tmp = entries.pop(idx + 1) 529 | entries[idx].block_count += tmp.block_count 530 | 531 | idx += 1 532 | 533 | # None of the following classes preserve the underlying stream's current seek. 534 | 535 | class Volume: 536 | """ 537 | Provides functionality for reading ext4 volumes 538 | """ 539 | 540 | ROOT_INODE = 2 541 | 542 | def __init__ (self, stream, offset = 0, ignore_flags = False, ignore_magic = False): 543 | """ 544 | Initializes a new ext4 reader at a given offset in stream. If ignore_magic is True, no exception will be thrown, 545 | when a structure with wrong magic number is found. Analogously passing True to ignore_flags suppresses Exception 546 | caused by wrong flags. 547 | """ 548 | self.ignore_flags = ignore_flags 549 | self.ignore_magic = ignore_magic 550 | self.offset = offset 551 | self.platform64 = True # Initial value needed for Volume.read_struct 552 | self.stream = stream 553 | 554 | # Superblock 555 | self.superblock = self.read_struct(ext4_superblock, 0x400) 556 | self.platform64 = (self.superblock.s_feature_incompat & ext4_superblock.INCOMPAT_64BIT) != 0 557 | 558 | if not ignore_magic and self.superblock.s_magic != 0xEF53: 559 | raise MagicError(f"Invalid magic value in superblock: 0x{self.superblock.s_magic:04X} (expected 0xEF53)") 560 | 561 | # Group descriptors 562 | self.group_descriptors = [None] * (self.superblock.s_inodes_count // self.superblock.s_inodes_per_group) 563 | 564 | group_desc_table_offset = (0x400 // self.block_size + 1) * self.block_size # First block after superblock 565 | for group_desc_idx in range(len(self.group_descriptors)): 566 | group_desc_offset = group_desc_table_offset + group_desc_idx * self.superblock.s_desc_size 567 | self.group_descriptors[group_desc_idx] = self.read_struct(ext4_group_descriptor, group_desc_offset) 568 | 569 | def __repr__ (self): 570 | return f"{type(self).__name__:s}(volume_name = {self.superblock.s_volume_name!r:s}, uuid = {self.uuid!r:s}, last_mounted = {self.superblock.s_last_mounted!r:s})" 571 | 572 | @property 573 | def block_size (self): 574 | """ 575 | Returns the volume's block size in bytes. 576 | """ 577 | return 1 << (10 + self.superblock.s_log_block_size) 578 | 579 | def get_inode (self, inode_idx): 580 | """ 581 | Returns an Inode instance representing the inode specified by its index inode_idx. 582 | """ 583 | group_idx, inode_table_entry_idx = self.get_inode_group(inode_idx) 584 | 585 | inode_table_offset = self.group_descriptors[group_idx].bg_inode_table * self.block_size 586 | inode_offset = inode_table_offset + inode_table_entry_idx * self.superblock.s_inode_size 587 | 588 | return Inode(self, inode_offset, inode_idx) 589 | 590 | def get_inode_group (self, inode_idx): 591 | """ 592 | Returns a tuple (group_idx, inode_table_entry_idx) 593 | """ 594 | group_idx = (inode_idx - 1) // self.superblock.s_inodes_per_group 595 | inode_table_entry_idx = (inode_idx - 1) % self.superblock.s_inodes_per_group 596 | return (group_idx, inode_table_entry_idx) 597 | 598 | def read (self, offset, byte_len): 599 | """ 600 | Returns byte_len bytes at offset within this volume. 601 | """ 602 | if self.offset + offset != self.stream.tell(): 603 | self.stream.seek(self.offset + offset, io.SEEK_SET) 604 | 605 | return self.stream.read(byte_len) 606 | 607 | def read_struct (self, structure, offset, platform64 = None): 608 | """ 609 | Interprets the bytes at offset as structure and returns the interpreted instance 610 | """ 611 | raw = self.read(offset, ctypes.sizeof(structure)) 612 | 613 | if hasattr(structure, "_from_buffer_copy"): 614 | return structure._from_buffer_copy(raw, platform64 = platform64 if platform64 != None else self.platform64) 615 | else: 616 | return structure.from_buffer_copy(raw) 617 | 618 | @property 619 | def root (self): 620 | """ 621 | Returns the volume's root inode 622 | """ 623 | return self.get_inode(Volume.ROOT_INODE) 624 | 625 | @property 626 | def uuid (self): 627 | """ 628 | Returns the volume's UUID in the format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX. 629 | """ 630 | uuid = self.superblock.s_uuid 631 | uuid = [uuid[:4], uuid[4 : 6], uuid[6 : 8], uuid[8 : 10], uuid[10:]] 632 | return "-".join("".join(f"{c:02X}" for c in part) for part in uuid) 633 | 634 | 635 | 636 | class Inode: 637 | """ 638 | Provides functionality for parsing inodes and accessing their raw data 639 | """ 640 | 641 | def __init__ (self, volume, offset, inode_idx): 642 | """ 643 | Initializes a new inode parser at the specified offset within the specified volume. file_type is the file type 644 | of the inode as given by the directory entry referring to this inode. 645 | """ 646 | self.inode_idx = inode_idx 647 | self.offset = offset 648 | self.volume = volume 649 | 650 | self.inode = volume.read_struct(ext4_inode, offset) 651 | 652 | def __len__ (self): 653 | """ 654 | Returns the length in bytes of the content referenced by this inode. 655 | """ 656 | return self.inode.i_size 657 | 658 | def __repr__ (self): 659 | if self.inode_idx != None: 660 | return f"{type(self).__name__:s}(inode_idx = {self.inode_idx!r:s}, offset = 0x{self.offset:X}, volume_uuid = {self.volume.uuid!r:s})" 661 | else: 662 | return f"{type(self).__name__:s}(offset = 0x{self.offset:X}, volume_uuid = {self.volume.uuid!r:s})" 663 | 664 | def _parse_xattrs (self, raw_data, offset, prefix_override = {}): 665 | """ 666 | Generator: Parses raw_data (bytes) as ext4_xattr_entry structures and their referenced xattr values and yields 667 | tuples (xattr_name, xattr_value) where xattr_name (str) is the attribute name including its prefix and 668 | xattr_value (bytes) is the raw attribute value. 669 | raw_data must start with the first ext4_xattr_entry structure and offset specifies the offset to the "block start" 670 | for ext4_xattr_entry.e_value_offs. 671 | prefix_overrides allows specifying attributes apart from the default prefixes. The default prefix dictionary is 672 | updated with prefix_overrides. 673 | """ 674 | prefixes = { 675 | 0: "", 676 | 1: "user.", 677 | 2: "system.posix_acl_access", 678 | 3: "system.posix_acl_default", 679 | 4: "trusted.", 680 | 6: "security.", 681 | 7: "system.", 682 | 8: "system.richacl" 683 | } 684 | prefixes.update(prefixes) 685 | 686 | # Iterator over ext4_xattr_entry structures 687 | i = 0 688 | while i < len(raw_data): 689 | xattr_entry = ext4_xattr_entry._from_buffer_copy(raw_data, i, platform64 = self.volume.platform64) 690 | 691 | if (xattr_entry.e_name_len | xattr_entry.e_name_index | xattr_entry.e_value_offs | xattr_entry.e_value_inum) == 0: 692 | # End of ext4_xattr_entry list 693 | break 694 | 695 | if not xattr_entry.e_name_index in prefixes: 696 | raise Ext4Error(f"Unknown attribute prefix {xattr_entry.e_name_index:d} in inode {self.inode_idx:d}") 697 | 698 | xattr_name = prefixes[xattr_entry.e_name_index] + xattr_entry.e_name.decode("iso-8859-2") 699 | 700 | if xattr_entry.e_value_inum != 0: 701 | # external xattr 702 | xattr_inode = self.volume.get_inode(xattr.e_value_inum) 703 | 704 | if not self.volume.ignore_flags and (xattr_inode.inode.i_flags & ext4_inode.EXT4_EA_INODE_FL) != 0: 705 | raise Ext4Error(f"Inode {xattr_inode.inode_idx:d} associated with the extended attribute {xattr_name!r:s} of inode {self.inode_idx:d} is not marked as large extended attribute value.") 706 | 707 | # TODO Use xattr_entry.e_value_size or xattr_inode.inode.i_size? 708 | xattr_value = xattr_inode.open_read().read() 709 | else: 710 | # internal xattr 711 | xattr_value = raw_data[xattr_entry.e_value_offs + offset : xattr_entry.e_value_offs + offset + xattr_entry.e_value_size] 712 | 713 | yield (xattr_name, xattr_value) 714 | 715 | i += xattr_entry._size 716 | 717 | 718 | 719 | def directory_entry_comparator (dir_a, dir_b): 720 | """ 721 | Sort-key for directory entries. It sortes entries in a way that directories come before anything else and within 722 | a group (directory / anything else) entries are sorted by their lower-case name. Entries whose lower-case names 723 | are equal are sorted by their actual names. 724 | """ 725 | file_name_a, _, file_type_a = dir_a 726 | file_name_b, _, file_type_b = dir_b 727 | 728 | if file_type_a == InodeType.DIRECTORY == file_type_b or file_type_a != InodeType.DIRECTORY != file_type_b: 729 | tmp = wcscmp(file_name_a.lower(), file_name_b.lower()) 730 | return tmp if tmp != 0 else wcscmp(file_name_a, file_name_b) 731 | else: 732 | return -1 if file_type_a == InodeType.DIRECTORY else 1 733 | 734 | directory_entry_key = functools.cmp_to_key(directory_entry_comparator) 735 | 736 | def get_inode (self, *relative_path, decode_name = None): 737 | """ 738 | Returns the inode specified by the path relative_path (list of entry names) relative to this inode. "." and ".." 739 | usually are supported too, however in special cases (e.g. manually crafted volumes) they might not be supported 740 | due to them being real on-disk directory entries that might be missing or pointing somewhere else. 741 | decode_name is directly passed to open_dir. 742 | NOTE: Whitespaces will not be trimmed off the path's parts and "\\0" and "\\0\\0" as well as b"\\0" and b"\\0\\0" are 743 | seen as different names (unless decode_name actually trims the name). 744 | NOTE: Along the path file_type != FILETYPE_DIR will be ignored, however i_flags will not be ignored. 745 | """ 746 | if not self.is_dir: 747 | raise Ext4Error(f"Inode {self.inode_idx:d} is not a directory.") 748 | 749 | current_inode = self 750 | 751 | for i, part in enumerate(relative_path): 752 | if not self.volume.ignore_flags and not current_inode.is_dir: 753 | current_path = "/".join(relative_path[:i]) 754 | raise Ext4Error(f"{current_path!r:s} (Inode {inode_idx:d}) is not a directory.") 755 | 756 | file_name, inode_idx, file_type = next(filter(lambda entry: entry[0] == part, current_inode.open_dir(decode_name)), (None, None, None)) 757 | 758 | if inode_idx == None: 759 | current_path = "/".join(relative_path[:i]) 760 | raise FileNotFoundError(f"{part!r:s} not found in {current_path!r:s} (Inode {current_inode.inode_idx:d}).") 761 | 762 | current_inode = current_inode.volume.get_inode(inode_idx) 763 | 764 | 765 | return current_inode 766 | 767 | @property 768 | def is_dir (self): 769 | """ 770 | Indicates whether the inode is marked as a directory. 771 | """ 772 | return (self.inode.i_mode & ext4_inode.S_IFDIR) != 0 773 | 774 | @property 775 | def is_file (self): 776 | """ 777 | Indicates whether the inode is marker as a regular file. 778 | """ 779 | return (self.inode.i_mode & ext4_inode.S_IFREG) != 0 780 | 781 | @property 782 | def is_in_use (self): 783 | """ 784 | Indicates whether the inode's associated bit in the inode bitmap is set. 785 | """ 786 | group_idx, bitmap_bit = self.volume.get_inode_group(self.inode_idx) 787 | 788 | inode_usage_bitmap_offset = self.volume.group_descriptors[group_idx].bg_inode_bitmap * self.volume.block_size 789 | inode_usage_byte = self.volume.read(inode_usage_bitmap_offset + bitmap_bit // 8, 1)[0] 790 | 791 | return ((inode_usage_byte >> (7 - bitmap_bit % 8)) & 1) != 0 792 | 793 | @property 794 | def mode_str (self): 795 | """ 796 | Returns the inode's permissions in form of a unix string (e.g. "-rwxrw-rw" or "drwxr-xr--"). 797 | """ 798 | special_flag = lambda letter, execute, special: { 799 | (False, False): "-", 800 | (False, True): letter.upper(), 801 | (True, False): "x", 802 | (True, True): letter.lower() 803 | }[(execute, special)] 804 | 805 | try: 806 | device_type = { 807 | ext4_inode.S_IFIFO : "p", 808 | ext4_inode.S_IFCHR : "c", 809 | ext4_inode.S_IFDIR : "d", 810 | ext4_inode.S_IFBLK : "b", 811 | ext4_inode.S_IFREG : "-", 812 | ext4_inode.S_IFLNK : "l", 813 | ext4_inode.S_IFSOCK : "s", 814 | }[self.inode.i_mode & 0xF000] 815 | except KeyError: 816 | device_type = "?" 817 | 818 | return "".join([ 819 | device_type, 820 | 821 | "r" if (self.inode.i_mode & ext4_inode.S_IRUSR) != 0 else "-", 822 | "w" if (self.inode.i_mode & ext4_inode.S_IWUSR) != 0 else "-", 823 | special_flag("s", (self.inode.i_mode & ext4_inode.S_IXUSR) != 0, (self.inode.i_mode & ext4_inode.S_ISUID) != 0), 824 | 825 | "r" if (self.inode.i_mode & ext4_inode.S_IRGRP) != 0 else "-", 826 | "w" if (self.inode.i_mode & ext4_inode.S_IWGRP) != 0 else "-", 827 | special_flag("s", (self.inode.i_mode & ext4_inode.S_IXGRP) != 0, (self.inode.i_mode & ext4_inode.S_ISGID) != 0), 828 | 829 | "r" if (self.inode.i_mode & ext4_inode.S_IROTH) != 0 else "-", 830 | "w" if (self.inode.i_mode & ext4_inode.S_IWOTH) != 0 else "-", 831 | special_flag("t", (self.inode.i_mode & ext4_inode.S_IXOTH) != 0, (self.inode.i_mode & ext4_inode.S_ISVTX) != 0), 832 | ]) 833 | 834 | def open_dir (self, decode_name = None): 835 | """ 836 | Generator: Yields the directory entries as tuples (decode_name(name), inode, file_type) in their on-disk order, 837 | where name is the raw on-disk directory entry name (bytes). file_type is one of the Inode.IT_* constants. For 838 | special cases (e.g. invalid utf8 characters in entry names) you can try a different decoder (e.g. 839 | decode_name = lambda raw: raw). 840 | Default of decode_name = lambda raw: raw.decode("utf8") 841 | """ 842 | # Parse args 843 | if decode_name == None: 844 | decode_name = lambda raw: raw.decode("utf8") 845 | 846 | if not self.volume.ignore_flags and not self.is_dir: 847 | raise Ext4Error(f"Inode ({self.inode_idx:d}) is not a directory.") 848 | 849 | # # Hash trees are compatible with linear arrays 850 | if (self.inode.i_flags & ext4_inode.EXT4_INDEX_FL) != 0: 851 | raise NotImplementedError("Hash trees are not implemented yet.") 852 | 853 | # Read raw directory content 854 | raw_data = self.open_read().read() 855 | offset = 0 856 | 857 | while offset < len(raw_data): 858 | dirent = ext4_dir_entry_2._from_buffer_copy(raw_data, offset, platform64 = self.volume.platform64) 859 | 860 | if dirent.file_type != InodeType.CHECKSUM: 861 | yield (decode_name(dirent.name), dirent.inode, dirent.file_type) 862 | 863 | offset += dirent.rec_len 864 | 865 | def open_read (self): 866 | """ 867 | Returns an BlockReader instance for reading this inode's raw content. 868 | """ 869 | if (self.inode.i_flags & ext4_inode.EXT4_EXTENTS_FL) != 0: 870 | # Obtain mapping from extents 871 | mapping = [] # List of MappingEntry instances 872 | 873 | nodes = queue.Queue() 874 | nodes.put_nowait(self.offset + ext4_inode.i_block.offset) 875 | 876 | while nodes.qsize() != 0: 877 | header_offset = nodes.get_nowait() 878 | header = self.volume.read_struct(ext4_extent_header, header_offset) 879 | 880 | if not self.volume.ignore_magic and header.eh_magic != 0xF30A: 881 | raise MagicError(f"Invalid magic value in extent header at offset 0x{header_offset:X} of inode {self.inode_idx:d}: 0x{header.eh_magic:04X} (expected 0xF30A)") 882 | 883 | if header.eh_depth != 0: 884 | indices = self.volume.read_struct(ext4_extent_idx * header.eh_entries, header_offset + ctypes.sizeof(ext4_extent_header)) 885 | for idx in indices: nodes.put_nowait(idx.ei_leaf * self.volume.block_size) 886 | else: 887 | extents = self.volume.read_struct(ext4_extent * header.eh_entries, header_offset + ctypes.sizeof(ext4_extent_header)) 888 | for extent in extents: 889 | mapping.append(MappingEntry(extent.ee_block, extent.ee_start, extent.ee_len)) 890 | 891 | MappingEntry.optimize(mapping) 892 | return BlockReader(self.volume, len(self), mapping) 893 | else: 894 | # Inode uses inline data 895 | i_block = self.volume.read(self.offset + ext4_inode.i_block.offset, ext4_inode.i_block.size) 896 | return io.BytesIO(i_block[:self.inode.i_size]) 897 | 898 | @property 899 | def size_readable (self): 900 | """ 901 | Returns the inode's content length in a readable format (e.g. "123 bytes", "2.03 KiB" or "3.00 GiB"). Possible 902 | units are bytes, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB. 903 | """ 904 | if self.inode.i_size < 1024: 905 | return f"{self.inode.i_size} bytes" if self.inode.i_size != 1 else "1 byte" 906 | else: 907 | units = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] 908 | unit_idx = min(int(math.log(self.inode.i_size, 1024)), len(units)) 909 | 910 | return f"{self.inode.i_size / (1024 ** unit_idx):.2f} {units[unit_idx - 1]:s}" 911 | 912 | def xattrs (self, check_inline = True, check_block = True, force_inline = False, prefix_override = {}): 913 | """ 914 | Generator: Yields the inode's extended attributes as tuples (name, value) in their on-disk order, where name (str) 915 | is the on-disk attribute name including its resolved name prefix and value (bytes) is the raw attribute value. 916 | check_inline and check_block control where to read attributes (the inode's inline data and/or the external data block 917 | pointed to by i_file_acl) and if check_inline as well as force_inline are set to True, the inode's inline data 918 | will not be verified to contain actual extended attributes and instead is just interpreted as such. prefix_overrides 919 | is directly passed to Inode._parse_xattrs. 920 | """ 921 | # Inline xattrs 922 | inline_data_offset = self.offset + ext4_inode.EXT2_GOOD_OLD_INODE_SIZE + self.inode.i_extra_isize 923 | inline_data_length = self.offset + self.volume.superblock.s_inode_size - inline_data_offset 924 | 925 | if check_inline and inline_data_length > ctypes.sizeof(ext4_xattr_ibody_header): 926 | inline_data = self.volume.read(inline_data_offset, inline_data_length) 927 | xattrs_header = ext4_xattr_ibody_header.from_buffer_copy(inline_data) 928 | 929 | # TODO Find way to detect inline xattrs without checking the h_magic field to enable error detection with the h_magic field. 930 | if force_inline or xattrs_header.h_magic == 0xEA020000: 931 | offset = 4 * ((ctypes.sizeof(ext4_xattr_ibody_header) + 3) // 4) # The ext4_xattr_entry following the header is aligned on a 4-byte boundary 932 | for xattr_name, xattr_value in self._parse_xattrs(inline_data[offset:], 0, prefix_override = prefix_override): 933 | yield (xattr_name, xattr_value) 934 | 935 | # xattr block(s) 936 | if check_block and self.inode.i_file_acl != 0: 937 | xattrs_block_start = self.inode.i_file_acl * self.volume.block_size 938 | xattrs_block = self.volume.read(xattrs_block_start, self.volume.block_size) 939 | 940 | xattrs_header = ext4_xattr_header.from_buffer_copy(xattrs_block) 941 | if not self.volume.ignore_magic and xattrs_header.h_magic != 0xEA020000: 942 | raise MagicError(f"Invalid magic value in xattrs block header at offset 0x{xattrs_block_start:X} of inode {self.inode_idx:d}: 0x{xattrs_header.h_magic} (expected 0xEA020000)") 943 | 944 | if xattrs_header.h_blocks != 1: 945 | raise Ext4Error(f"Invalid number of xattr blocks at offset 0x{xattrs_block_start:X} of inode {self.inode_idx:d}: {xattrs_header.h_blocks:d} (expected 1)") 946 | 947 | offset = 4 * ((ctypes.sizeof(ext4_xattr_header) + 3) // 4) # The ext4_xattr_entry following the header is aligned on a 4-byte boundary 948 | for xattr_name, xattr_value in self._parse_xattrs(xattrs_block[offset:], -offset, prefix_override = prefix_override): 949 | yield (xattr_name, xattr_value) 950 | 951 | 952 | 953 | class BlockReader: 954 | """ 955 | Maps disk blocks into a linear byte stream. 956 | NOTE: This class does not implement buffering or caching. 957 | """ 958 | 959 | # OSError 960 | EINVAL = 22 961 | 962 | def __init__ (self, volume, byte_size, block_map): 963 | """ 964 | Initializes a new block reader on the specified volume. mapping must be a list of MappingEntry instances. If 965 | you prefer a way to use 2-tuples (disk_block_idx, block_count) with inferred file_block_index entries, see 966 | MappingEntry.create_mapping. 967 | """ 968 | self.byte_size = byte_size 969 | self.volume = volume 970 | 971 | self.cursor = 0 972 | 973 | block_map = list(map(MappingEntry.copy, block_map)) 974 | 975 | # Optimize mapping (stich together) 976 | MappingEntry.optimize(block_map) 977 | self.block_map = block_map 978 | 979 | def __repr__ (self): 980 | return f"{type(self).__name__:s}(byte_size = {self.byte_size!r:s}, block_map = {self.block_map!r:s}, volume_uuid = {self.volume.uuid!r:s})" 981 | 982 | def get_block_mapping (self, file_block_idx): 983 | """ 984 | Returns the disk block index of the file block specified by file_block_idx. 985 | """ 986 | disk_block_idx = None 987 | 988 | # Find disk block 989 | for entry in self.block_map: 990 | if entry.file_block_idx <= file_block_idx < entry.file_block_idx + entry.block_count: 991 | block_diff = file_block_idx - entry.file_block_idx 992 | disk_block_idx = entry.disk_block_idx + block_diff 993 | break 994 | 995 | return disk_block_idx 996 | 997 | def read (self, byte_len = -1): 998 | """ 999 | Reades up to byte_len bytes from the block device beginning at the cursor's current position. This operation will 1000 | not exceed the inode's size. If -1 is passed for byte_len, the inode is read to the end. 1001 | """ 1002 | # Parse args 1003 | if byte_len < -1: raise ValueError("byte_len must be non-negative or -1") 1004 | 1005 | bytes_remaining = self.byte_size - self.cursor 1006 | byte_len = bytes_remaining if byte_len == -1 else max(0, min(byte_len, bytes_remaining)) 1007 | 1008 | if byte_len == 0: return b"" 1009 | 1010 | # Reading blocks 1011 | start_block_idx = self.cursor // self.volume.block_size 1012 | end_block_idx = (self.cursor + byte_len - 1) // self.volume.block_size 1013 | end_of_stream_check = byte_len 1014 | 1015 | blocks = [self.read_block(i) for i in range(start_block_idx, end_block_idx - start_block_idx + 1)] 1016 | 1017 | start_offset = self.cursor % self.volume.block_size 1018 | if start_offset != 0: blocks[0] = blocks[0][start_offset:] 1019 | byte_len = (byte_len + start_offset - self.volume.block_size - 1) % self.volume.block_size + 1 1020 | blocks[-1] = blocks[-1][:byte_len] 1021 | 1022 | result = b"".join(blocks) 1023 | 1024 | # Check read 1025 | if len(result) != end_of_stream_check: 1026 | raise EndOfStreamError(f"The volume's underlying stream ended {byte_len - len(result):d} bytes before EOF.") 1027 | 1028 | self.cursor += len(result) 1029 | return result 1030 | 1031 | def read_block (self, file_block_idx): 1032 | """ 1033 | Reads one block from disk (return a zero-block if the file block is not mapped) 1034 | """ 1035 | disk_block_idx = self.get_block_mapping(file_block_idx) 1036 | 1037 | if disk_block_idx != None: 1038 | return self.volume.read(disk_block_idx * self.volume.block_size, self.volume.block_size) 1039 | else: 1040 | return bytes([0] * self.volume.block_size) 1041 | 1042 | def seek (self, seek, seek_mode = io.SEEK_SET): 1043 | """ 1044 | Moves the internal cursor along the file (not the disk) and behaves like BufferedReader.seek 1045 | """ 1046 | if seek_mode == io.SEEK_CUR: 1047 | seek += self.cursor 1048 | elif seek_mode == io.SEEK_END: 1049 | seek += self.byte_size 1050 | # elif seek_mode == io.SEEK_SET: 1051 | # seek += 0 1052 | 1053 | if seek < 0: 1054 | raise OSError(BlockReader.EINVAL, "Invalid argument") # Exception behavior copied from IOBase.seek 1055 | 1056 | self.cursor = seek 1057 | return seek 1058 | 1059 | def tell (self): 1060 | """ 1061 | Returns the internal cursor's current file offset. 1062 | """ 1063 | return self.cursor 1064 | 1065 | 1066 | 1067 | class Tools: 1068 | """ 1069 | Provides helpful utility functions 1070 | """ 1071 | 1072 | def list_dir ( 1073 | volume, 1074 | identifier, 1075 | decode_name = None, 1076 | sort_key = Inode.directory_entry_key, 1077 | line_format = None, 1078 | file_types = {0 : "unkn", 1 : "file", 2 : "dir", 3 : "chr", 4 : "blk", 5 : "fifo", 6 : "sock", 7 : "sym"} 1079 | ): 1080 | """ 1081 | Similar to "ls -la" this function lists all entries from a directory of volume. 1082 | 1083 | identifier might be an Inode instance, an integer describing the directory's inode index, a str/bytes describing 1084 | the directory's full path or a list of entry names. decode_name is directly passed to open_dir. See Inode.get_inode 1085 | for more details. 1086 | 1087 | sort_key is the key-function used for sorting the directories entries. If None is passed, the call to sorted is 1088 | omitted. 1089 | 1090 | line_format is a format string specifying each line's format or a function formatting each line. It is used as 1091 | follows: 1092 | 1093 | line_format( 1094 | file_name = file_name, # Entry name 1095 | inode = volume.get_inode(inode_idx), # Referenced inode 1096 | file_type = file_type, # Entry type (int) 1097 | file_type_str = file_types[file_type] if file_type in file_types else "?" # Entry type (str, see next paragraph) 1098 | ) 1099 | 1100 | The default of line_format is the following function: 1101 | 1102 | def line_format (file_name, inode, file_type, file_type_str): 1103 | if file_type == InodeType.SYMBOLIC_LINK: 1104 | link_target = inode.open_read().read().decode("utf8") 1105 | return f"{inode.mode_str:s} {inode.size_readable: >10s} {file_name:s} -> {link_target:s}" 1106 | else: 1107 | return f"{inode.mode_str:s} {inode.size_readable: >10s} {file_name:s}" 1108 | 1109 | file_types is a dictionary specifying the names of the different entry types. 1110 | """ 1111 | # Parse arguments 1112 | if isinstance(identifier, Inode): 1113 | inode = identifier 1114 | elif isinstance(identifier, int): 1115 | inode = volume.get_inode(identifier) 1116 | elif isinstance(identifier, str): 1117 | identifier = identifier.strip(" /").split("/") 1118 | 1119 | if len(identifier) == 1 and identifier[0] == "": 1120 | inode = volume.root 1121 | else: 1122 | inode = volume.root.get_inode(*identifier) 1123 | elif isinstance(identifier, list): 1124 | inode = volume.root.get_inode(*identifier) 1125 | 1126 | if line_format == None: 1127 | def _line_format (file_name, inode, file_type, file_type_str): 1128 | if file_type == InodeType.SYMBOLIC_LINK: 1129 | link_target = inode.open_read().read().decode("utf8") 1130 | return f"{inode.mode_str:s} {inode.size_readable: >10s} {file_name:s} -> {link_target:s}" 1131 | else: 1132 | return f"{inode.mode_str:s} {inode.size_readable: >10s} {file_name:s}" 1133 | 1134 | line_format = _line_format 1135 | elif isinstance(line_format, str): 1136 | line_format = line_format.format 1137 | 1138 | # Print directory 1139 | entries = inode.open_dir(decode_name) if sort_key is None else sorted(inode.open_dir(decode_name), key = sort_key) 1140 | 1141 | for file_name, inode_idx, file_type in entries: 1142 | print(line_format( 1143 | file_name = file_name, 1144 | inode = volume.get_inode(inode_idx), 1145 | file_type = file_type, 1146 | file_type_str = file_types[file_type] if file_type in file_types else "?" 1147 | )) -------------------------------------------------------------------------------- /ext4.py35.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import functools 3 | import io 4 | import math 5 | import queue 6 | 7 | 8 | 9 | ######################################################################################################################## 10 | ##################################################### HELPERS ###################################################### 11 | ######################################################################################################################## 12 | 13 | def wcscmp (str_a, str_b): 14 | """ 15 | Standard library wcscmp 16 | """ 17 | for a, b in zip(str_a, str_b): 18 | tmp = ord(a) - ord(b) 19 | if tmp != 0: return -1 if tmp < 0 else 1 20 | 21 | tmp = len(str_a) - len(str_b) 22 | return -1 if tmp < 0 else 1 if tmp > 0 else 0 23 | 24 | 25 | 26 | ######################################################################################################################## 27 | #################################################### EXCEPTIONS #################################################### 28 | ######################################################################################################################## 29 | 30 | class Ext4Error (Exception): 31 | """ 32 | Base class for all custom errors 33 | """ 34 | pass 35 | 36 | class BlockMapError (Ext4Error): 37 | """ 38 | Raised, when a requested file_block is not mapped to disk 39 | """ 40 | pass 41 | 42 | class EndOfStreamError (Ext4Error): 43 | """ 44 | Raised, when BlockReader reads beyond the end of the volume's underlying stream 45 | """ 46 | pass 47 | 48 | class MagicError (Ext4Error): 49 | """ 50 | Raised, when a structures magic value is wrong and ignore_magic is False 51 | """ 52 | pass 53 | 54 | 55 | 56 | ######################################################################################################################## 57 | #################################################### LOW LEVEL ##################################################### 58 | ######################################################################################################################## 59 | 60 | class ext4_struct (ctypes.LittleEndianStructure): 61 | """ 62 | Simplifies access to *_lo and *_hi fields 63 | """ 64 | def __getattr__ (self, name): 65 | """ 66 | Enables reading *_lo and *_hi fields together. 67 | """ 68 | try: 69 | # Combining *_lo and *_hi fields 70 | lo_field = ctypes.LittleEndianStructure.__getattribute__(type(self), name + "_lo") 71 | size = lo_field.size 72 | 73 | lo = lo_field.__get__(self) 74 | hi = ctypes.LittleEndianStructure.__getattribute__(self, name + "_hi") 75 | 76 | return (hi << (8 * size)) | lo 77 | except AttributeError: 78 | return ctypes.LittleEndianStructure.__getattribute__(self, name) 79 | 80 | def __setattr__ (self, name, value): 81 | """ 82 | Enables setting *_lo and *_hi fields together. 83 | """ 84 | try: 85 | # Combining *_lo and *_hi fields 86 | lo_field = lo_field = ctypes.LittleEndianStructure.__getattribute__(type(self), name + "_lo") 87 | size = lo_field.size 88 | 89 | lo_field.__set__(self, value & ((1 << (8 * size)) - 1)) 90 | ctypes.LittleEndianStructure.__setattr__(self, name + "_hi", value >> (8 * size)) 91 | except AttributeError: 92 | ctypes.LittleEndianStructure.__setattr__(self, name, value) 93 | 94 | 95 | 96 | class ext4_dir_entry_2 (ext4_struct): 97 | _fields_ = [ 98 | ("inode", ctypes.c_uint), # 0x0 99 | ("rec_len", ctypes.c_ushort), # 0x4 100 | ("name_len", ctypes.c_ubyte), # 0x6 101 | ("file_type", ctypes.c_ubyte) # 0x7 102 | # Variable length field "name" missing at 0x8 103 | ] 104 | 105 | def _from_buffer_copy (raw, offset = 0, platform64 = True): 106 | struct = ext4_dir_entry_2.from_buffer_copy(raw, offset) 107 | struct.name = raw[offset + 0x8 : offset + 0x8 + struct.name_len] 108 | return struct 109 | 110 | 111 | 112 | class ext4_extent (ext4_struct): 113 | _fields_ = [ 114 | ("ee_block", ctypes.c_uint), # 0x0000 115 | ("ee_len", ctypes.c_ushort), # 0x0004 116 | ("ee_start_hi", ctypes.c_ushort), # 0x0006 117 | ("ee_start_lo", ctypes.c_uint) # 0x0008 118 | ] 119 | 120 | 121 | 122 | class ext4_extent_header (ext4_struct): 123 | _fields_ = [ 124 | ("eh_magic", ctypes.c_ushort), # 0x0000, Must be 0xF30A 125 | ("eh_entries", ctypes.c_ushort), # 0x0002 126 | ("eh_max", ctypes.c_ushort), # 0x0004 127 | ("eh_depth", ctypes.c_ushort), # 0x0006 128 | ("eh_generation", ctypes.c_uint) # 0x0008 129 | ] 130 | 131 | 132 | 133 | class ext4_extent_idx (ext4_struct): 134 | _fields_ = [ 135 | ("ei_block", ctypes.c_uint), # 0x0000 136 | ("ei_leaf_lo", ctypes.c_uint), # 0x0004 137 | ("ei_leaf_hi", ctypes.c_ushort), # 0x0008 138 | ("ei_unused", ctypes.c_ushort) # 0x000A 139 | ] 140 | 141 | 142 | 143 | class ext4_group_descriptor (ext4_struct): 144 | _fields_ = [ 145 | ("bg_block_bitmap_lo", ctypes.c_uint), # 0x0000 146 | ("bg_inode_bitmap_lo", ctypes.c_uint), # 0x0004 147 | ("bg_inode_table_lo", ctypes.c_uint), # 0x0008 148 | ("bg_free_blocks_count_lo", ctypes.c_ushort), # 0x000C 149 | ("bg_free_inodes_count_lo", ctypes.c_ushort), # 0x000E 150 | ("bg_used_dirs_count_lo", ctypes.c_ushort), # 0x0010 151 | ("bg_flags", ctypes.c_ushort), # 0x0012 152 | ("bg_exclude_bitmap_lo", ctypes.c_uint), # 0x0014 153 | ("bg_block_bitmap_csum_lo", ctypes.c_ushort), # 0x0018 154 | ("bg_inode_bitmap_csum_lo", ctypes.c_ushort), # 0x001A 155 | ("bg_itable_unused_lo", ctypes.c_ushort), # 0x001C 156 | ("bg_checksum", ctypes.c_ushort), # 0x001E 157 | 158 | # 64-bit fields 159 | ("bg_block_bitmap_hi", ctypes.c_uint), # 0x0020 160 | ("bg_inode_bitmap_hi", ctypes.c_uint), # 0x0024 161 | ("bg_inode_table_hi", ctypes.c_uint), # 0x0028 162 | ("bg_free_blocks_count_hi", ctypes.c_ushort), # 0x002C 163 | ("bg_free_inodes_count_hi", ctypes.c_ushort), # 0x002E 164 | ("bg_used_dirs_count_hi", ctypes.c_ushort), # 0x0030 165 | ("bg_itable_unused_hi", ctypes.c_ushort), # 0x0032 166 | ("bg_exclude_bitmap_hi", ctypes.c_uint), # 0x0034 167 | ("bg_block_bitmap_csum_hi", ctypes.c_ushort), # 0x0038 168 | ("bg_inode_bitmap_csum_hi", ctypes.c_ushort), # 0x003A 169 | ("bg_reserved", ctypes.c_uint), # 0x003C 170 | ] 171 | 172 | def _from_buffer_copy (raw, offset = 0, platform64 = True): 173 | struct = ext4_group_descriptor.from_buffer_copy(raw, offset) 174 | 175 | if not platform64: 176 | struct.bg_block_bitmap_hi = 0 177 | struct.bg_inode_bitmap_hi = 0 178 | struct.bg_inode_table_hi = 0 179 | struct.bg_free_blocks_count_hi = 0 180 | struct.bg_free_inodes_count_hi = 0 181 | struct.bg_used_dirs_count_hi = 0 182 | struct.bg_itable_unused_hi = 0 183 | struct.bg_exclude_bitmap_hi = 0 184 | struct.bg_block_bitmap_csum_hi = 0 185 | struct.bg_inode_bitmap_csum_hi = 0 186 | struct.bg_reserved = 0 187 | 188 | return struct 189 | 190 | 191 | 192 | class ext4_inode (ext4_struct): 193 | EXT2_GOOD_OLD_INODE_SIZE = 128 # Every field passing 128 bytes is "additional data", whose size is specified by i_extra_isize. 194 | 195 | # i_mode 196 | S_IXOTH = 0x1 # Others can execute 197 | S_IWOTH = 0x2 # Others can write 198 | S_IROTH = 0x4 # Others can read 199 | S_IXGRP = 0x8 # Group can execute 200 | S_IWGRP = 0x10 # Group can write 201 | S_IRGRP = 0x20 # Group can read 202 | S_IXUSR = 0x40 # Owner can execute 203 | S_IWUSR = 0x80 # Owner can write 204 | S_IRUSR = 0x100 # Owner can read 205 | S_ISVTX = 0x200 # Sticky bit (only owner can delete) 206 | S_ISGID = 0x400 # Set GID (execute with privileges of group owner of the file's group) 207 | S_ISUID = 0x800 # Set UID (execute with privileges of the file's owner) 208 | S_IFIFO = 0x1000 # FIFO device (named pipe) 209 | S_IFCHR = 0x2000 # Character device (raw, unbuffered, aligned, direct access to hardware storage) 210 | S_IFDIR = 0x4000 # Directory 211 | S_IFBLK = 0x6000 # Block device (buffered, arbitrary access to storage) 212 | S_IFREG = 0x8000 # Regular file 213 | S_IFLNK = 0xA000 # Symbolic link 214 | S_IFSOCK = 0xC000 # Socket 215 | 216 | # i_flags 217 | EXT4_INDEX_FL = 0x1000 # Uses hash trees 218 | EXT4_EXTENTS_FL = 0x80000 # Uses extents 219 | EXT4_EA_INODE_FL = 0x200000 # Inode stores large xattr 220 | EXT4_INLINE_DATA_FL = 0x10000000 # Has inline data 221 | 222 | _fields_ = [ 223 | ("i_mode", ctypes.c_ushort), # 0x0000 224 | ("i_uid_lo", ctypes.c_ushort), # 0x0002, Originally named i_uid 225 | ("i_size_lo", ctypes.c_uint), # 0x0004 226 | ("i_atime", ctypes.c_uint), # 0x0008 227 | ("i_ctime", ctypes.c_uint), # 0x000C 228 | ("i_mtime", ctypes.c_uint), # 0x0010 229 | ("i_dtime", ctypes.c_uint), # 0x0014 230 | ("i_gid_lo", ctypes.c_ushort), # 0x0018, Originally named i_gid 231 | ("i_links_count", ctypes.c_ushort), # 0x001A 232 | ("i_blocks_lo", ctypes.c_uint), # 0x001C 233 | ("i_flags", ctypes.c_uint), # 0x0020 234 | ("osd1", ctypes.c_uint), # 0x0024 235 | ("i_block", ctypes.c_uint * 15), # 0x0028 236 | ("i_generation", ctypes.c_uint), # 0x0064 237 | ("i_file_acl_lo", ctypes.c_uint), # 0x0068 238 | ("i_size_hi", ctypes.c_uint), # 0x006C, Originally named i_size_high 239 | ("i_obso_faddr", ctypes.c_uint), # 0x0070 240 | ("i_osd2_blocks_high", ctypes.c_ushort), # 0x0074, Originally named i_osd2.linux2.l_i_blocks_high 241 | ("i_file_acl_hi", ctypes.c_ushort), # 0x0076, Originally named i_osd2.linux2.l_i_file_acl_high 242 | ("i_uid_hi", ctypes.c_ushort), # 0x0078, Originally named i_osd2.linux2.l_i_uid_high 243 | ("i_gid_hi", ctypes.c_ushort), # 0x007A, Originally named i_osd2.linux2.l_i_gid_high 244 | ("i_osd2_checksum_lo", ctypes.c_ushort), # 0x007C, Originally named i_osd2.linux2.l_i_checksum_lo 245 | ("i_osd2_reserved", ctypes.c_ushort), # 0x007E, Originally named i_osd2.linux2.l_i_reserved 246 | ("i_extra_isize", ctypes.c_ushort), # 0x0080 247 | ("i_checksum_hi", ctypes.c_ushort), # 0x0082 248 | ("i_ctime_extra", ctypes.c_uint), # 0x0084 249 | ("i_mtime_extra", ctypes.c_uint), # 0x0088 250 | ("i_atime_extra", ctypes.c_uint), # 0x008C 251 | ("i_crtime", ctypes.c_uint), # 0x0090 252 | ("i_crtime_extra", ctypes.c_uint), # 0x0094 253 | ("i_version_hi", ctypes.c_uint), # 0x0098 254 | ("i_projid", ctypes.c_uint), # 0x009C 255 | ] 256 | 257 | 258 | 259 | class ext4_superblock (ext4_struct): 260 | EXT2_MIN_DESC_SIZE = 0x20 # Default value for s_desc_size, if INCOMPAT_64BIT is not set (NEEDS CONFIRMATION) 261 | EXT2_MIN_DESC_SIZE_64BIT = 0x40 # Default value for s_desc_size, if INCOMPAT_64BIT is set 262 | 263 | # s_feature_incompat 264 | INCOMPAT_64BIT = 0x80 # Uses 64-bit features (e.g. *_hi structure fields in ext4_group_descriptor) 265 | INCOMPAT_FILETYPE = 0x2 # Directory entries record file type (instead of inode flags) 266 | 267 | _fields_ = [ 268 | ("s_inodes_count", ctypes.c_uint), # 0x0000 269 | ("s_blocks_count_lo", ctypes.c_uint), # 0x0004 270 | ("s_r_blocks_count_lo", ctypes.c_uint), # 0x0008 271 | ("s_free_blocks_count_lo", ctypes.c_uint), # 0x000C 272 | ("s_free_inodes_count", ctypes.c_uint), # 0x0010 273 | ("s_first_data_block", ctypes.c_uint), # 0x0014 274 | ("s_log_block_size", ctypes.c_uint), # 0x0018 275 | ("s_log_cluster_size", ctypes.c_uint), # 0x001C 276 | ("s_blocks_per_group", ctypes.c_uint), # 0x0020 277 | ("s_clusters_per_group", ctypes.c_uint), # 0x0024 278 | ("s_inodes_per_group", ctypes.c_uint), # 0x0028 279 | ("s_mtime", ctypes.c_uint), # 0x002C 280 | ("s_wtime", ctypes.c_uint), # 0x0030 281 | ("s_mnt_count", ctypes.c_ushort), # 0x0034 282 | ("s_max_mnt_count", ctypes.c_ushort), # 0x0036 283 | ("s_magic", ctypes.c_ushort), # 0x0038, Must be 0xEF53 284 | ("s_state", ctypes.c_ushort), # 0x003A 285 | ("s_errors", ctypes.c_ushort), # 0x003C 286 | ("s_minor_rev_level", ctypes.c_ushort), # 0x003E 287 | ("s_lastcheck", ctypes.c_uint), # 0x0040 288 | ("s_checkinterval", ctypes.c_uint), # 0x0044 289 | ("s_creator_os", ctypes.c_uint), # 0x0048 290 | ("s_rev_level", ctypes.c_uint), # 0x004C 291 | ("s_def_resuid", ctypes.c_ushort), # 0x0050 292 | ("s_def_resgid", ctypes.c_ushort), # 0x0052 293 | ("s_first_ino", ctypes.c_uint), # 0x0054 294 | ("s_inode_size", ctypes.c_ushort), # 0x0058 295 | ("s_block_group_nr", ctypes.c_ushort), # 0x005A 296 | ("s_feature_compat", ctypes.c_uint), # 0x005C 297 | ("s_feature_incompat", ctypes.c_uint), # 0x0060 298 | ("s_feature_ro_compat", ctypes.c_uint), # 0x0064 299 | ("s_uuid", ctypes.c_ubyte * 16), # 0x0068 300 | ("s_volume_name", ctypes.c_char * 16), # 0x0078 301 | ("s_last_mounted", ctypes.c_char * 64), # 0x0088 302 | ("s_algorithm_usage_bitmap", ctypes.c_uint), # 0x00C8 303 | ("s_prealloc_blocks", ctypes.c_ubyte), # 0x00CC 304 | ("s_prealloc_dir_blocks", ctypes.c_ubyte), # 0x00CD 305 | ("s_reserved_gdt_blocks", ctypes.c_ushort), # 0x00CE 306 | ("s_journal_uuid", ctypes.c_ubyte * 16), # 0x00D0 307 | ("s_journal_inum", ctypes.c_uint), # 0x00E0 308 | ("s_journal_dev", ctypes.c_uint), # 0x00E4 309 | ("s_last_orphan", ctypes.c_uint), # 0x00E8 310 | ("s_hash_seed", ctypes.c_uint * 4), # 0x00EC 311 | ("s_def_hash_version", ctypes.c_ubyte), # 0x00FC 312 | ("s_jnl_backup_type", ctypes.c_ubyte), # 0x00FD 313 | ("s_desc_size", ctypes.c_ushort), # 0x00FE 314 | ("s_default_mount_opts", ctypes.c_uint), # 0x0100 315 | ("s_first_meta_bg", ctypes.c_uint), # 0x0104 316 | ("s_mkfs_time", ctypes.c_uint), # 0x0108 317 | ("s_jnl_blocks", ctypes.c_uint * 17), # 0x010C 318 | 319 | # 64-bit fields 320 | ("s_blocks_count_hi", ctypes.c_uint), # 0x0150 321 | ("s_r_blocks_count_hi", ctypes.c_uint), # 0x0154 322 | ("s_free_blocks_count_hi", ctypes.c_uint), # 0x0158 323 | ("s_min_extra_isize", ctypes.c_ushort), # 0x015C 324 | ("s_want_extra_isize", ctypes.c_ushort), # 0x015E 325 | ("s_flags", ctypes.c_uint), # 0x0160 326 | ("s_raid_stride", ctypes.c_ushort), # 0x0164 327 | ("s_mmp_interval", ctypes.c_ushort), # 0x0166 328 | ("s_mmp_block", ctypes.c_ulonglong), # 0x0168 329 | ("s_raid_stripe_width", ctypes.c_uint), # 0x0170 330 | ("s_log_groups_per_flex", ctypes.c_ubyte), # 0x0174 331 | ("s_checksum_type", ctypes.c_ubyte), # 0x0175 332 | ("s_reserved_pad", ctypes.c_ushort), # 0x0176 333 | ("s_kbytes_written", ctypes.c_ulonglong), # 0x0178 334 | ("s_snapshot_inum", ctypes.c_uint), # 0x0180 335 | ("s_snapshot_id", ctypes.c_uint), # 0x0184 336 | ("s_snapshot_r_blocks_count", ctypes.c_ulonglong), # 0x0188 337 | ("s_snapshot_list", ctypes.c_uint), # 0x0190 338 | ("s_error_count", ctypes.c_uint), # 0x0194 339 | ("s_first_error_time", ctypes.c_uint), # 0x0198 340 | ("s_first_error_ino", ctypes.c_uint), # 0x019C 341 | ("s_first_error_block", ctypes.c_ulonglong), # 0x01A0 342 | ("s_first_error_func", ctypes.c_ubyte * 32), # 0x01A8 343 | ("s_first_error_line", ctypes.c_uint), # 0x01C8 344 | ("s_last_error_time", ctypes.c_uint), # 0x01CC 345 | ("s_last_error_ino", ctypes.c_uint), # 0x01D0 346 | ("s_last_error_line", ctypes.c_uint), # 0x01D4 347 | ("s_last_error_block", ctypes.c_ulonglong), # 0x01D8 348 | ("s_last_error_func", ctypes.c_ubyte * 32), # 0x01E0 349 | ("s_mount_opts", ctypes.c_ubyte * 64), # 0x0200 350 | ("s_usr_quota_inum", ctypes.c_uint), # 0x0240 351 | ("s_grp_quota_inum", ctypes.c_uint), # 0x0244 352 | ("s_overhead_blocks", ctypes.c_uint), # 0x0248 353 | ("s_backup_bgs", ctypes.c_uint * 2), # 0x024C 354 | ("s_encrypt_algos", ctypes.c_ubyte * 4), # 0x0254 355 | ("s_encrypt_pw_salt", ctypes.c_ubyte * 16), # 0x0258 356 | ("s_lpf_ino", ctypes.c_uint), # 0x0268 357 | ("s_prj_quota_inum", ctypes.c_uint), # 0x026C 358 | ("s_checksum_seed", ctypes.c_uint), # 0x0270 359 | ("s_reserved", ctypes.c_uint * 98), # 0x0274 360 | ("s_checksum", ctypes.c_uint) # 0x03FC 361 | ] 362 | 363 | def _from_buffer_copy (raw, platform64 = True): 364 | struct = ext4_superblock.from_buffer_copy(raw) 365 | 366 | if not platform64: 367 | struct.s_blocks_count_hi = 0 368 | struct.s_r_blocks_count_hi = 0 369 | struct.s_free_blocks_count_hi = 0 370 | struct.s_min_extra_isize = 0 371 | struct.s_want_extra_isize = 0 372 | struct.s_flags = 0 373 | struct.s_raid_stride = 0 374 | struct.s_mmp_interval = 0 375 | struct.s_mmp_block = 0 376 | struct.s_raid_stripe_width = 0 377 | struct.s_log_groups_per_flex = 0 378 | struct.s_checksum_type = 0 379 | struct.s_reserved_pad = 0 380 | struct.s_kbytes_written = 0 381 | struct.s_snapshot_inum = 0 382 | struct.s_snapshot_id = 0 383 | struct.s_snapshot_r_blocks_count = 0 384 | struct.s_snapshot_list = 0 385 | struct.s_error_count = 0 386 | struct.s_first_error_time = 0 387 | struct.s_first_error_ino = 0 388 | struct.s_first_error_block = 0 389 | struct.s_first_error_func = 0 390 | struct.s_first_error_line = 0 391 | struct.s_last_error_time = 0 392 | struct.s_last_error_ino = 0 393 | struct.s_last_error_line = 0 394 | struct.s_last_error_block = 0 395 | struct.s_last_error_func = 0 396 | struct.s_mount_opts = 0 397 | struct.s_usr_quota_inum = 0 398 | struct.s_grp_quota_inum = 0 399 | struct.s_overhead_blocks = 0 400 | struct.s_backup_bgs = 0 401 | struct.s_encrypt_algos = 0 402 | struct.s_encrypt_pw_salt = 0 403 | struct.s_lpf_ino = 0 404 | struct.s_prj_quota_inum = 0 405 | struct.s_checksum_seed = 0 406 | struct.s_reserved = 0 407 | struct.s_checksum = 0 408 | 409 | if struct.s_desc_size == 0: 410 | if (struct.s_feature_incompat & ext4_superblock.INCOMPAT_64BIT) == 0: 411 | struct.s_desc_size = ext4_superblock.EXT2_MIN_DESC_SIZE 412 | else: 413 | struct.s_desc_size = ext4_superblock.EXT2_MIN_DESC_SIZE_64BIT 414 | 415 | return struct 416 | 417 | 418 | 419 | class ext4_xattr_entry (ext4_struct): 420 | _fields_ = [ 421 | ("e_name_len", ctypes.c_ubyte), # 0x00 422 | ("e_name_index", ctypes.c_ubyte), # 0x01 423 | ("e_value_offs", ctypes.c_ushort), # 0x02 424 | ("e_value_inum", ctypes.c_uint), # 0x04 425 | ("e_value_size", ctypes.c_uint), # 0x08 426 | ("e_hash", ctypes.c_uint) # 0x0C 427 | # Variable length field "e_name" missing at 0x10 428 | ] 429 | 430 | def _from_buffer_copy (raw, offset = 0, platform64 = True): 431 | struct = ext4_xattr_entry.from_buffer_copy(raw, offset) 432 | struct.e_name = raw[offset + 0x10 : offset + 0x10 + struct.e_name_len] 433 | return struct 434 | 435 | @property 436 | def _size (self): return 4 * ((ctypes.sizeof(type(self)) + self.e_name_len + 3) // 4) # 4-byte alignment 437 | 438 | 439 | 440 | class ext4_xattr_header (ext4_struct): 441 | _fields_ = [ 442 | ("h_magic", ctypes.c_uint), # 0x0, Must be 0xEA020000 443 | ("h_refcount", ctypes.c_uint), # 0x4 444 | ("h_blocks", ctypes.c_uint), # 0x8 445 | ("h_hash", ctypes.c_uint), # 0xC 446 | ("h_checksum", ctypes.c_uint), # 0x10 447 | ("h_reserved", ctypes.c_uint * 3), # 0x14 448 | ] 449 | 450 | 451 | 452 | class ext4_xattr_ibody_header (ext4_struct): 453 | _fields_ = [ 454 | ("h_magic", ctypes.c_uint) # 0x0, Must be 0xEA020000 455 | ] 456 | 457 | 458 | 459 | class InodeType: 460 | UNKNOWN = 0x0 # Unknown file type 461 | FILE = 0x1 # Regular file 462 | DIRECTORY = 0x2 # Directory 463 | CHARACTER_DEVICE = 0x3 # Character device 464 | BLOCK_DEVICE = 0x4 # Block device 465 | FIFO = 0x5 # FIFO 466 | SOCKET = 0x6 # Socket 467 | SYMBOLIC_LINK = 0x7 # Symbolic link 468 | CHECKSUM = 0xDE # Checksum entry; not really a file type, but a type of directory entry 469 | 470 | 471 | 472 | ######################################################################################################################## 473 | #################################################### HIGH LEVEL #################################################### 474 | ######################################################################################################################## 475 | 476 | class MappingEntry: 477 | """ 478 | Helper class: This class maps block_count file blocks indexed by file_block_idx to the associated disk blocks indexed 479 | by disk_block_idx. 480 | """ 481 | def __init__ (self, file_block_idx, disk_block_idx, block_count = 1): 482 | """ 483 | Initialize a MappingEntry instance with given file_block_idx, disk_block_idx and block_count. 484 | """ 485 | self.file_block_idx = file_block_idx 486 | self.disk_block_idx = disk_block_idx 487 | self.block_count = block_count 488 | 489 | def __iter__ (self): 490 | """ 491 | Can be used to convert an MappingEntry into a tuple (file_block_idx, disk_block_idx, block_count). 492 | """ 493 | yield self.file_block_idx 494 | yield self.disk_block_idx 495 | yield self.block_count 496 | 497 | def __repr__ (self): 498 | return "{type:s}({file_block_idx!r:s}, {disk_block_idx!r:s}, {blocK_count!r:s})".format( 499 | blocK_count = self.block_count, 500 | disk_block_idx = self.disk_block_idx, 501 | file_block_idx = self.file_block_idx, 502 | type = type(self).__name__ 503 | ) 504 | 505 | def copy (self): 506 | return MappingEntry(self.file_block_idx, self.disk_block_idx, self.block_count) 507 | 508 | def create_mapping (*entries): 509 | """ 510 | Converts a list of 2-tuples (disk_block_idx, block_count) into a list of MappingEntry instances 511 | """ 512 | file_block_idx = 0 513 | result = [None] * len(entries) 514 | 515 | for i, entry in enumerate(entries): 516 | disk_block_idx, block_count = entry 517 | result[i] = MappingEntry(file_block_idx, disk_block_idx, block_count) 518 | file_block_idx += block_count 519 | 520 | return result 521 | 522 | def optimize (entries): 523 | """ 524 | Sorts and stiches together a list of MappingEntry instances 525 | """ 526 | entries.sort(key = lambda entry: entry.file_block_idx) 527 | 528 | idx = 0 529 | while idx < len(entries): 530 | while idx + 1 < len(entries) \ 531 | and entries[idx].file_block_idx + entries[idx].block_count == entries[idx + 1].file_block_idx \ 532 | and entries[idx].disk_block_idx + entries[idx].block_count == entries[idx + 1].disk_block_idx: 533 | tmp = entries.pop(idx + 1) 534 | entries[idx].block_count += tmp.block_count 535 | 536 | idx += 1 537 | 538 | # None of the following classes preserve the underlying stream's current seek. 539 | 540 | class Volume: 541 | """ 542 | Provides functionality for reading ext4 volumes 543 | """ 544 | 545 | ROOT_INODE = 2 546 | 547 | def __init__ (self, stream, offset = 0, ignore_flags = False, ignore_magic = False): 548 | """ 549 | Initializes a new ext4 reader at a given offset in stream. If ignore_magic is True, no exception will be thrown, 550 | when a structure with wrong magic number is found. Analogously passing True to ignore_flags suppresses Exception 551 | caused by wrong flags. 552 | """ 553 | self.ignore_flags = ignore_flags 554 | self.ignore_magic = ignore_magic 555 | self.offset = offset 556 | self.platform64 = True # Initial value needed for Volume.read_struct 557 | self.stream = stream 558 | 559 | # Superblock 560 | self.superblock = self.read_struct(ext4_superblock, 0x400) 561 | self.platform64 = (self.superblock.s_feature_incompat & ext4_superblock.INCOMPAT_64BIT) != 0 562 | 563 | if not ignore_magic and self.superblock.s_magic != 0xEF53: 564 | raise MagicError("Invalid magic value in superblock: 0x{magic:04X} (expected 0xEF53)".format(magic = self.superblock.s_magic)) 565 | 566 | # Group descriptors 567 | self.group_descriptors = [None] * (self.superblock.s_inodes_count // self.superblock.s_inodes_per_group) 568 | 569 | group_desc_table_offset = (0x400 // self.block_size + 1) * self.block_size # First block after superblock 570 | for group_desc_idx in range(len(self.group_descriptors)): 571 | group_desc_offset = group_desc_table_offset + group_desc_idx * self.superblock.s_desc_size 572 | self.group_descriptors[group_desc_idx] = self.read_struct(ext4_group_descriptor, group_desc_offset) 573 | 574 | def __repr__ (self): 575 | return "{type_name:s}(volume_name = {volume_name!r:s}, uuid = {uuid!r:s}, last_mounted = {last_mounted!r:s})".format( 576 | last_mounted = self.superblock.s_last_mounted, 577 | type_name = type(self).__name__, 578 | uuid = self.uuid, 579 | volume_name = self.superblock.s_volume_name 580 | ) 581 | 582 | @property 583 | def block_size (self): 584 | """ 585 | Returns the volume's block size in bytes. 586 | """ 587 | return 1 << (10 + self.superblock.s_log_block_size) 588 | 589 | def get_inode (self, inode_idx): 590 | """ 591 | Returns an Inode instance representing the inode specified by its index inode_idx. 592 | """ 593 | group_idx, inode_table_entry_idx = self.get_inode_group(inode_idx) 594 | 595 | inode_table_offset = self.group_descriptors[group_idx].bg_inode_table * self.block_size 596 | inode_offset = inode_table_offset + inode_table_entry_idx * self.superblock.s_inode_size 597 | 598 | return Inode(self, inode_offset, inode_idx) 599 | 600 | def get_inode_group (self, inode_idx): 601 | """ 602 | Returns a tuple (group_idx, inode_table_entry_idx) 603 | """ 604 | group_idx = (inode_idx - 1) // self.superblock.s_inodes_per_group 605 | inode_table_entry_idx = (inode_idx - 1) % self.superblock.s_inodes_per_group 606 | return (group_idx, inode_table_entry_idx) 607 | 608 | def read (self, offset, byte_len): 609 | """ 610 | Returns byte_len bytes at offset within this volume. 611 | """ 612 | if self.offset + offset != self.stream.tell(): 613 | self.stream.seek(self.offset + offset, io.SEEK_SET) 614 | 615 | return self.stream.read(byte_len) 616 | 617 | def read_struct (self, structure, offset, platform64 = None): 618 | """ 619 | Interprets the bytes at offset as structure and returns the interpreted instance 620 | """ 621 | raw = self.read(offset, ctypes.sizeof(structure)) 622 | 623 | if hasattr(structure, "_from_buffer_copy"): 624 | return structure._from_buffer_copy(raw, platform64 = platform64 if platform64 != None else self.platform64) 625 | else: 626 | return structure.from_buffer_copy(raw) 627 | 628 | @property 629 | def root (self): 630 | """ 631 | Returns the volume's root inode 632 | """ 633 | return self.get_inode(Volume.ROOT_INODE) 634 | 635 | @property 636 | def uuid (self): 637 | """ 638 | Returns the volume's UUID in the format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX. 639 | """ 640 | uuid = self.superblock.s_uuid 641 | uuid = [uuid[:4], uuid[4 : 6], uuid[6 : 8], uuid[8 : 10], uuid[10:]] 642 | return "-".join("".join("{0:02X}".format(c) for c in part) for part in uuid) 643 | 644 | 645 | 646 | class Inode: 647 | """ 648 | Provides functionality for parsing inodes and accessing their raw data 649 | """ 650 | 651 | def __init__ (self, volume, offset, inode_idx): 652 | """ 653 | Initializes a new inode parser at the specified offset within the specified volume. file_type is the file type 654 | of the inode as given by the directory entry referring to this inode. 655 | """ 656 | self.inode_idx = inode_idx 657 | self.offset = offset 658 | self.volume = volume 659 | 660 | self.inode = volume.read_struct(ext4_inode, offset) 661 | 662 | def __len__ (self): 663 | """ 664 | Returns the length in bytes of the content referenced by this inode. 665 | """ 666 | return self.inode.i_size 667 | 668 | def __repr__ (self): 669 | if self.inode_idx != None: 670 | return "{type_name:s}(inode_idx = {inode!r:s}, offset = 0x{offset:X}, volume_uuid = {uuid!r:s})".format( 671 | inode = self.inode_idx, 672 | offset = self.offset, 673 | type_name = type(self).__name__, 674 | uuid = self.volume.uuid 675 | ) 676 | else: 677 | return "{type_name:s}(offset = 0x{offset:X}, volume_uuid = {uuid!r:s})".format( 678 | offset = self.offset, 679 | type_name = type(self).__name__, 680 | uuid = self.volume.uuid 681 | ) 682 | 683 | def _parse_xattrs (self, raw_data, offset, prefix_override = {}): 684 | """ 685 | Generator: Parses raw_data (bytes) as ext4_xattr_entry structures and their referenced xattr values and yields 686 | tuples (xattr_name, xattr_value) where xattr_name (str) is the attribute name including its prefix and 687 | xattr_value (bytes) is the raw attribute value. 688 | raw_data must start with the first ext4_xattr_entry structure and offset specifies the offset to the "block start" 689 | for ext4_xattr_entry.e_value_offs. 690 | prefix_overrides allows specifying attributes apart from the default prefixes. The default prefix dictionary is 691 | updated with prefix_overrides. 692 | """ 693 | prefixes = { 694 | 0: "", 695 | 1: "user.", 696 | 2: "system.posix_acl_access", 697 | 3: "system.posix_acl_default", 698 | 4: "trusted.", 699 | 6: "security.", 700 | 7: "system.", 701 | 8: "system.richacl" 702 | } 703 | prefixes.update(prefixes) 704 | 705 | # Iterator over ext4_xattr_entry structures 706 | i = 0 707 | while i < len(raw_data): 708 | xattr_entry = ext4_xattr_entry._from_buffer_copy(raw_data, i, platform64 = self.volume.platform64) 709 | 710 | if (xattr_entry.e_name_len | xattr_entry.e_name_index | xattr_entry.e_value_offs | xattr_entry.e_value_inum) == 0: 711 | # End of ext4_xattr_entry list 712 | break 713 | 714 | if not xattr_entry.e_name_index in prefixes: 715 | raise Ext4Error("Unknown attribute prefix {prefix:d} in inode {inode:d}".format( 716 | inode = self.inode_idx, 717 | prefix = xattr_entry.e_name_index 718 | )) 719 | 720 | xattr_name = prefixes[xattr_entry.e_name_index] + xattr_entry.e_name.decode("iso-8859-2") 721 | 722 | if xattr_entry.e_value_inum != 0: 723 | # external xattr 724 | xattr_inode = self.volume.get_inode(xattr.e_value_inum, InodeType.FILE) 725 | 726 | if not self.volume.ignore_flags and (xattr_inode.inode.i_flags & ext4_inode.EXT4_EA_INODE_FL) != 0: 727 | raise Ext4Error("Inode {value_indoe:d} associated with the extended attribute {xattr_name!r:s} of inode {inode:d} is not marked as large extended attribute value.".format( 728 | inode = self.inode_idx, 729 | value_inode = xattr_inode.inode_idx, 730 | xattr_name = xattr_name 731 | )) 732 | 733 | # TODO Use xattr_entry.e_value_size or xattr_inode.inode.i_size? 734 | xattr_value = xattr_inode.open_read().read() 735 | else: 736 | # internal xattr 737 | xattr_value = raw_data[xattr_entry.e_value_offs + offset : xattr_entry.e_value_offs + offset + xattr_entry.e_value_size] 738 | 739 | yield (xattr_name, xattr_value) 740 | 741 | i += xattr_entry._size 742 | 743 | 744 | 745 | def directory_entry_comparator (dir_a, dir_b): 746 | """ 747 | Sort-key for directory entries. It sortes entries in a way that directories come before anything else and within 748 | a group (directory / anything else) entries are sorted by their lower-case name. Entries whose lower-case names 749 | are equal are sorted by their actual names. 750 | """ 751 | file_name_a, _, file_type_a = dir_a 752 | file_name_b, _, file_type_b = dir_b 753 | 754 | if file_type_a == InodeType.DIRECTORY == file_type_b or file_type_a != InodeType.DIRECTORY != file_type_b: 755 | tmp = wcscmp(file_name_a.lower(), file_name_b.lower()) 756 | return tmp if tmp != 0 else wcscmp(file_name_a, file_name_b) 757 | else: 758 | return -1 if file_type_a == InodeType.DIRECTORY else 1 759 | 760 | directory_entry_key = functools.cmp_to_key(directory_entry_comparator) 761 | 762 | def get_inode (self, *relative_path, decode_name = None): 763 | """ 764 | Returns the inode specified by the path relative_path (list of entry names) relative to this inode. "." and ".." 765 | usually are supported too, however in special cases (e.g. manually crafted volumes) they might not be supported 766 | due to them being real on-disk directory entries that might be missing or pointing somewhere else. 767 | decode_name is directly passed to open_dir. 768 | NOTE: Whitespaces will not be trimmed off the path's parts and "\\0" and "\\0\\0" as well as b"\\0" and b"\\0\\0" are 769 | seen as different names (unless decode_name actually trims the name). 770 | NOTE: Along the path file_type != FILETYPE_DIR will be ignored, however i_flags will not be ignored. 771 | """ 772 | if not self.is_dir: 773 | raise Ext4Error("Inode {inode:d} is not a directory.".format(inode = self.inode_idx)) 774 | 775 | current_inode = self 776 | 777 | for i, part in enumerate(relative_path): 778 | if not self.volume.ignore_flags and not current_inode.is_dir: 779 | current_path = "/".join(relative_path[:i]) 780 | raise Ext4Error("{current_path!r:s} (Inode {inode:d}) is not a directory.".format( 781 | current_path = current_path, 782 | inode = inode_idx 783 | )) 784 | 785 | file_name, inode_idx, file_type = next(filter(lambda entry: entry[0] == part, current_inode.open_dir(decode_name)), (None, None, None)) 786 | 787 | if inode_idx == None: 788 | current_path = "/".join(relative_path[:i]) 789 | raise FileNotFoundError("{part!r:s} not found in {current_path!r:s} (Inode {inode:d}).".format( 790 | current_path = current_path, 791 | inode = current_inode.inode_idx, 792 | part = part 793 | )) 794 | 795 | current_inode = current_inode.volume.get_inode(inode_idx, file_type) 796 | 797 | 798 | return current_inode 799 | 800 | @property 801 | def is_dir (self): 802 | """ 803 | Indicates whether the inode is marked as a directory. 804 | """ 805 | return (self.inode.i_mode & ext4_inode.S_IFDIR) != 0 806 | 807 | @property 808 | def is_file (self): 809 | """ 810 | Indicates whether the inode is marker as a regular file. 811 | """ 812 | return (self.inode.i_mode & ext4_inode.S_IFREG) != 0 813 | 814 | @property 815 | def is_in_use (self): 816 | """ 817 | Indicates whether the inode's associated bit in the inode bitmap is set. 818 | """ 819 | group_idx, bitmap_bit = self.volume.get_inode_group(self.inode_idx) 820 | 821 | inode_usage_bitmap_offset = self.volume.group_descriptors[group_idx].bg_inode_bitmap * self.volume.block_size 822 | inode_usage_byte = self.volume.read(inode_usage_bitmap_offset + bitmap_bit // 8, 1)[0] 823 | 824 | return ((inode_usage_byte >> (7 - bitmap_bit % 8)) & 1) != 0 825 | 826 | @property 827 | def mode_str (self): 828 | """ 829 | Returns the inode's permissions in form of a unix string (e.g. "-rwxrw-rw" or "drwxr-xr--"). 830 | """ 831 | special_flag = lambda letter, execute, special: { 832 | (False, False): "-", 833 | (False, True): letter.upper(), 834 | (True, False): "x", 835 | (True, True): letter.lower() 836 | }[(execute, special)] 837 | 838 | try: 839 | device_type = { 840 | ext4_inode.S_IFIFO : "p", 841 | ext4_inode.S_IFCHR : "c", 842 | ext4_inode.S_IFDIR : "d", 843 | ext4_inode.S_IFBLK : "b", 844 | ext4_inode.S_IFREG : "-", 845 | ext4_inode.S_IFLNK : "l", 846 | ext4_inode.S_IFSOCK : "s", 847 | }[self.inode.i_mode & 0xF000] 848 | except KeyError: 849 | device_type = "?" 850 | 851 | return "".join([ 852 | device_type, 853 | 854 | "r" if (self.inode.i_mode & ext4_inode.S_IRUSR) != 0 else "-", 855 | "w" if (self.inode.i_mode & ext4_inode.S_IWUSR) != 0 else "-", 856 | special_flag("s", (self.inode.i_mode & ext4_inode.S_IXUSR) != 0, (self.inode.i_mode & ext4_inode.S_ISUID) != 0), 857 | 858 | "r" if (self.inode.i_mode & ext4_inode.S_IRGRP) != 0 else "-", 859 | "w" if (self.inode.i_mode & ext4_inode.S_IWGRP) != 0 else "-", 860 | special_flag("s", (self.inode.i_mode & ext4_inode.S_IXGRP) != 0, (self.inode.i_mode & ext4_inode.S_ISGID) != 0), 861 | 862 | "r" if (self.inode.i_mode & ext4_inode.S_IROTH) != 0 else "-", 863 | "w" if (self.inode.i_mode & ext4_inode.S_IWOTH) != 0 else "-", 864 | special_flag("t", (self.inode.i_mode & ext4_inode.S_IXOTH) != 0, (self.inode.i_mode & ext4_inode.S_ISVTX) != 0), 865 | ]) 866 | 867 | def open_dir (self, decode_name = None): 868 | """ 869 | Generator: Yields the directory entries as tuples (decode_name(name), inode, file_type) in their on-disk order, 870 | where name is the raw on-disk directory entry name (bytes). file_type is one of the Inode.IT_* constants. For 871 | special cases (e.g. invalid utf8 characters in entry names) you can try a different decoder (e.g. 872 | decode_name = lambda raw: raw). 873 | Default of decode_name = lambda raw: raw.decode("utf8") 874 | """ 875 | # Parse args 876 | if decode_name == None: 877 | decode_name = lambda raw: raw.decode("utf8") 878 | 879 | if not self.volume.ignore_flags and not self.is_dir: 880 | raise Ext4Error("Inode ({inode:d}) is not a directory.".format(inode = self.inode_idx)) 881 | 882 | # # Hash trees are compatible with linear arrays 883 | if (self.inode.i_flags & ext4_inode.EXT4_INDEX_FL) != 0: 884 | raise NotImplementedError("Hash trees are not implemented yet.") 885 | 886 | # Read raw directory content 887 | raw_data = self.open_read().read() 888 | offset = 0 889 | 890 | while offset < len(raw_data): 891 | dirent = ext4_dir_entry_2._from_buffer_copy(raw_data, offset, platform64 = self.volume.platform64) 892 | 893 | if dirent.file_type != InodeType.CHECKSUM: 894 | yield (decode_name(dirent.name), dirent.inode, dirent.file_type) 895 | 896 | offset += dirent.rec_len 897 | 898 | def open_read (self): 899 | """ 900 | Returns an BlockReader instance for reading this inode's raw content. 901 | """ 902 | if (self.inode.i_flags & ext4_inode.EXT4_EXTENTS_FL) != 0: 903 | # Obtain mapping from extents 904 | mapping = [] # List of MappingEntry instances 905 | 906 | nodes = queue.Queue() 907 | nodes.put_nowait(self.offset + ext4_inode.i_block.offset) 908 | 909 | while nodes.qsize() != 0: 910 | header_offset = nodes.get_nowait() 911 | header = self.volume.read_struct(ext4_extent_header, header_offset) 912 | 913 | if not self.volume.ignore_magic and header.eh_magic != 0xF30A: 914 | raise MagicError("Invalid magic value in extent header at offset 0x{header_offset:X} of inode {inode:d}: 0x{header_magic:04X} (expected 0xF30A)".format( 915 | header_magic = header.eh_magic, 916 | header_offset = self.inode_idx, 917 | inode = self.inode_idx 918 | )) 919 | 920 | if header.eh_depth != 0: 921 | indices = self.volume.read_struct(ext4_extent_idx * header.eh_entries, header_offset + ctypes.sizeof(ext4_extent_header)) 922 | for idx in indices: nodes.put_nowait(idx.ei_leaf * self.volume.block_size) 923 | else: 924 | extents = self.volume.read_struct(ext4_extent * header.eh_entries, header_offset + ctypes.sizeof(ext4_extent_header)) 925 | for extent in extents: 926 | mapping.append(MappingEntry(extent.ee_block, extent.ee_start, extent.ee_len)) 927 | 928 | MappingEntry.optimize(mapping) 929 | return BlockReader(self.volume, len(self), mapping) 930 | else: 931 | # Inode uses inline data 932 | i_block = self.volume.read(self.offset + ext4_inode.i_block.offset, ext4_inode.i_block.size) 933 | return io.BytesIO(i_block[:self.inode.i_size]) 934 | 935 | @property 936 | def size_readable (self): 937 | """ 938 | Returns the inode's content length in a readable format (e.g. "123 bytes", "2.03 KiB" or "3.00 GiB"). Possible 939 | units are bytes, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB. 940 | """ 941 | if self.inode.i_size < 1024: 942 | return "{0:d} bytes".format(self.inode.i_size) if self.inode.i_size != 1 else "1 byte" 943 | else: 944 | units = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] 945 | unit_idx = min(int(math.log(self.inode.i_size, 1024)), len(units)) 946 | 947 | return "{size:.2f} {unit:s}".format( 948 | size = self.inode.i_size / (1024 ** unit_idx), 949 | unit = units[unit_idx - 1] 950 | ) 951 | 952 | def xattrs (self, check_inline = True, check_block = True, force_inline = False, prefix_override = {}): 953 | """ 954 | Generator: Yields the inode's extended attributes as tuples (name, value) in their on-disk order, where name (str) 955 | is the on-disk attribute name including its resolved name prefix and value (bytes) is the raw attribute value. 956 | check_inline and check_block control where to read attributes (the inode's inline data and/or the external data block 957 | pointed to by i_file_acl) and if check_inline as well as force_inline are set to True, the inode's inline data 958 | will not be verified to contain actual extended attributes and instead is just interpreted as such. prefix_overrides 959 | is directly passed to Inode._parse_xattrs. 960 | """ 961 | # Inline xattrs 962 | inline_data_offset = self.offset + ext4_inode.EXT2_GOOD_OLD_INODE_SIZE + self.inode.i_extra_isize 963 | inline_data_length = self.offset + self.volume.superblock.s_inode_size - inline_data_offset 964 | 965 | if check_inline and inline_data_length > ctypes.sizeof(ext4_xattr_ibody_header): 966 | inline_data = self.volume.read(inline_data_offset, inline_data_length) 967 | xattrs_header = ext4_xattr_ibody_header.from_buffer_copy(inline_data) 968 | 969 | # TODO Find way to detect inline xattrs without checking the h_magic field to enable error detection with the h_magic field. 970 | if force_inline or xattrs_header.h_magic == 0xEA020000: 971 | offset = 4 * ((ctypes.sizeof(ext4_xattr_ibody_header) + 3) // 4) # The ext4_xattr_entry following the header is aligned on a 4-byte boundary 972 | for xattr_name, xattr_value in self._parse_xattrs(inline_data[offset:], 0, prefix_override = prefix_override): 973 | yield (xattr_name, xattr_value) 974 | 975 | # xattr block(s) 976 | if check_block and self.inode.i_file_acl != 0: 977 | xattrs_block_start = self.inode.i_file_acl * self.volume.block_size 978 | xattrs_block = self.volume.read(xattrs_block_start, self.volume.block_size) 979 | 980 | xattrs_header = ext4_xattr_header.from_buffer_copy(xattrs_block) 981 | if not self.volume.ignore_magic and xattrs_header.h_magic != 0xEA020000: 982 | raise MagicError("Invalid magic value in xattrs block header at offset 0x{xattrs_block_start:X} of inode {inode:d}: 0x{xattrs_header} (expected 0xEA020000)".format( 983 | inode = self.inode_idx, 984 | xattrs_block_start = xattrs_block_start, 985 | xattrs_header = xattrs_header.h_magic 986 | )) 987 | 988 | if xattrs_header.h_blocks != 1: 989 | raise Ext4Error("Invalid number of xattr blocks at offset 0x{xattrs_block_start:X} of inode {inode:d}: {xattrs_header:d} (expected 1)".format( 990 | inode = self.inode_idx, 991 | xattrs_header = xattrs_header.h_blocks, 992 | xattrs_block_start = xattrs_block_start 993 | )) 994 | 995 | offset = 4 * ((ctypes.sizeof(ext4_xattr_header) + 3) // 4) # The ext4_xattr_entry following the header is aligned on a 4-byte boundary 996 | for xattr_name, xattr_value in self._parse_xattrs(xattrs_block[offset:], -offset, prefix_override = prefix_override): 997 | yield (xattr_name, xattr_value) 998 | 999 | 1000 | 1001 | class BlockReader: 1002 | """ 1003 | Maps disk blocks into a linear byte stream. 1004 | NOTE: This class does not implement buffering or caching. 1005 | """ 1006 | 1007 | # OSError 1008 | EINVAL = 22 1009 | 1010 | def __init__ (self, volume, byte_size, block_map): 1011 | """ 1012 | Initializes a new block reader on the specified volume. mapping must be a list of MappingEntry instances. If 1013 | you prefer a way to use 2-tuples (disk_block_idx, block_count) with inferred file_block_index entries, see 1014 | MappingEntry.create_mapping. 1015 | """ 1016 | self.byte_size = byte_size 1017 | self.volume = volume 1018 | 1019 | self.cursor = 0 1020 | 1021 | block_map = list(map(MappingEntry.copy, block_map)) 1022 | 1023 | # Optimize mapping (stich together) 1024 | MappingEntry.optimize(block_map) 1025 | self.block_map = block_map 1026 | 1027 | def __repr__ (self): 1028 | return "{type_name:s}(byte_size = {size!r:s}, block_map = {block_map!r:s}, volume_uuid = {uuid!r:s})".format( 1029 | block_map = self.block_map, 1030 | size = self.byte_size, 1031 | type_name = type(self).__name__, 1032 | uuid = self.volume.uuid 1033 | ) 1034 | 1035 | def get_block_mapping (self, file_block_idx): 1036 | """ 1037 | Returns the disk block index of the file block specified by file_block_idx. 1038 | """ 1039 | disk_block_idx = None 1040 | 1041 | # Find disk block 1042 | for entry in self.block_map: 1043 | if entry.file_block_idx <= file_block_idx < entry.file_block_idx + entry.block_count: 1044 | block_diff = file_block_idx - entry.file_block_idx 1045 | disk_block_idx = entry.disk_block_idx + block_diff 1046 | break 1047 | 1048 | return disk_block_idx 1049 | 1050 | def read (self, byte_len = -1): 1051 | """ 1052 | Reades up to byte_len bytes from the block device beginning at the cursor's current position. This operation will 1053 | not exceed the inode's size. If -1 is passed for byte_len, the inode is read to the end. 1054 | """ 1055 | # Parse args 1056 | if byte_len < -1: raise ValueError("byte_len must be non-negative or -1") 1057 | 1058 | bytes_remaining = self.byte_size - self.cursor 1059 | byte_len = bytes_remaining if byte_len == -1 else max(0, min(byte_len, bytes_remaining)) 1060 | 1061 | if byte_len == 0: return b"" 1062 | 1063 | # Reading blocks 1064 | start_block_idx = self.cursor // self.volume.block_size 1065 | end_block_idx = (self.cursor + byte_len - 1) // self.volume.block_size 1066 | end_of_stream_check = byte_len 1067 | 1068 | blocks = [self.read_block(i) for i in range(start_block_idx, end_block_idx - start_block_idx + 1)] 1069 | 1070 | start_offset = self.cursor % self.volume.block_size 1071 | if start_offset != 0: blocks[0] = blocks[0][start_offset:] 1072 | byte_len = (byte_len + start_offset - self.volume.block_size - 1) % self.volume.block_size + 1 1073 | blocks[-1] = blocks[-1][:byte_len] 1074 | 1075 | result = b"".join(blocks) 1076 | 1077 | # Check read 1078 | if len(result) != end_of_stream_check: 1079 | raise EndOfStreamError("The volume's underlying stream ended {0:d} bytes before EOF.".format(byte_len - len(result))) 1080 | 1081 | self.cursor += len(result) 1082 | return result 1083 | 1084 | def read_block (self, file_block_idx): 1085 | """ 1086 | Reads one block from disk (return a zero-block if the file block is not mapped) 1087 | """ 1088 | disk_block_idx = self.get_block_mapping(file_block_idx) 1089 | 1090 | if disk_block_idx != None: 1091 | return self.volume.read(disk_block_idx * self.volume.block_size, self.volume.block_size) 1092 | else: 1093 | return bytes([0] * self.volume.block_size) 1094 | 1095 | def seek (self, seek, seek_mode = io.SEEK_SET): 1096 | """ 1097 | Moves the internal cursor along the file (not the disk) and behaves like BufferedReader.seek 1098 | """ 1099 | if seek_mode == io.SEEK_CUR: 1100 | seek += self.cursor 1101 | elif seek_mode == io.SEEK_END: 1102 | seek += self.byte_size 1103 | # elif seek_mode == io.SEEK_SET: 1104 | # seek += 0 1105 | 1106 | if seek < 0: 1107 | raise OSError(BlockReader.EINVAL, "Invalid argument") # Exception behavior copied from IOBase.seek 1108 | 1109 | self.cursor = seek 1110 | return seek 1111 | 1112 | def tell (self): 1113 | """ 1114 | Returns the internal cursor's current file offset. 1115 | """ 1116 | return self.cursor 1117 | 1118 | 1119 | 1120 | class Tools: 1121 | """ 1122 | Provides helpful utility functions 1123 | """ 1124 | 1125 | def list_dir ( 1126 | volume, 1127 | identifier, 1128 | decode_name = None, 1129 | sort_key = Inode.directory_entry_key, 1130 | line_format = None, 1131 | file_types = {0 : "unkn", 1 : "file", 2 : "dir", 3 : "chr", 4 : "blk", 5 : "fifo", 6 : "sock", 7 : "sym"} 1132 | ): 1133 | """ 1134 | Similar to "ls -la" this function lists all entries from a directory of volume. 1135 | 1136 | identifier might be an Inode instance, an integer describing the directory's inode index, a str/bytes describing 1137 | the directory's full path or a list of entry names. decode_name is directly passed to open_dir. See Inode.get_inode 1138 | for more details. 1139 | 1140 | sort_key is the key-function used for sorting the directories entries. If None is passed, the call to sorted is 1141 | omitted. 1142 | 1143 | line_format is a format string specifying each line's format or a function formatting each line. It is used as 1144 | follows: 1145 | 1146 | line_format( 1147 | file_name = file_name, # Entry name 1148 | inode = volume.get_inode(inode_idx), # Referenced inode 1149 | file_type = file_type, # Entry type (int) 1150 | file_type_str = file_types[file_type] if file_type in file_types else "?" # Entry type (str, see next paragraph) 1151 | ) 1152 | 1153 | The default of line_format is the following function: 1154 | 1155 | def line_format (file_name, inode, file_type, file_type_str): 1156 | if file_type == InodeType.SYMBOLIC_LINK: 1157 | link_target = inode.open_read().read().decode("utf8") 1158 | return "{mode:s} {size: >10s} {file_name:s} -> {link_target:s}".format(file_name = file_name, link_target = link_target, mode = inode.mode_str, size = inode.size_readable) 1159 | else: 1160 | return "{mode:s} {size: >10s} {file_name:s}".format(file_name = file_name, mode = inode.mode_str, size = inode.size_readable) 1161 | 1162 | file_types is a dictionary specifying the names of the different entry types. 1163 | """ 1164 | # Parse arguments 1165 | if isinstance(identifier, Inode): 1166 | inode = identifier 1167 | elif isinstance(identifier, int): 1168 | inode = volume.get_inode(identifier) 1169 | elif isinstance(identifier, str): 1170 | identifier = identifier.strip(" /").split("/") 1171 | 1172 | if len(identifier) == 1 and identifier[0] == "": 1173 | inode = volume.root 1174 | else: 1175 | inode = volume.root.get_inode(*identifier) 1176 | elif isinstance(identifier, list): 1177 | inode = volume.root.get_inode(*identifier) 1178 | 1179 | if line_format == None: 1180 | def _line_format (file_name, inode, file_type, file_type_str): 1181 | if file_type == InodeType.SYMBOLIC_LINK: 1182 | link_target = inode.open_read().read().decode("utf8") 1183 | return "{mode:s} {size: >10s} {file_name:s} -> {link_target:s}".format(file_name = file_name, link_target = link_target, mode = inode.mode_str, size = inode.size_readable) 1184 | else: 1185 | return "{mode:s} {size: >10s} {file_name:s}".format(file_name = file_name, mode = inode.mode_str, size = inode.size_readable) 1186 | 1187 | line_format = _line_format 1188 | elif isinstance(line_format, str): 1189 | line_format = line_format.format 1190 | 1191 | # Print directory 1192 | entries = inode.open_dir(decode_name) if sort_key is None else sorted(inode.open_dir(decode_name), key = sort_key) 1193 | 1194 | for file_name, inode_idx, file_type in entries: 1195 | print(line_format( 1196 | file_name = file_name, 1197 | inode = volume.get_inode(inode_idx), 1198 | file_type = file_type, 1199 | file_type_str = file_types[file_type] if file_type in file_types else "?" 1200 | )) --------------------------------------------------------------------------------