├── 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 | ))
--------------------------------------------------------------------------------