Compare commits

...

164 Commits

Author SHA1 Message Date
Caleb Gardner ebbbc9e87e Merge pull request #40 from CalebQ42/go-unsquashfs-hardlinks
go-unsquashfs hardlinks & squashfslow access
2025-05-25 13:53:38 -05:00
Caleb Gardner 155999a8e3 Fixed always showing hardlink info 2025-05-25 13:45:51 -05:00
Caleb Gardner 7930f4402b Added -show-hard-links to go-unsquashfs
Exposed the underlying squashfslow values for File and FS
2025-05-25 13:35:40 -05:00
Caleb Gardner ada61a391c Added OpenFile to get a *squashfs.File instead of fs.File
Added -e to extract only specific files/folders
Only require the filename for -l, -ll, and -lln
2025-05-21 01:11:24 -05:00
Caleb Gardner f32cb520dc Zstd re-use 2025-04-16 18:02:19 -05:00
Caleb Gardner f991ddb1d4 Don't ignore permissions in benchmark tests 2025-04-16 16:22:32 -05:00
Caleb Gardner 4c8c9f0b47 Re-use zstd and zlib readers 2025-04-16 05:49:42 -05:00
Caleb Gardner 8a2556ea0d Remove test file 2025-04-16 05:16:58 -05:00
Caleb Gardner 33156751ca Merge pull request #39 from CalebQ42/perfExp
Performance
2025-04-10 11:26:24 -05:00
Caleb Gardner 6224c4be41 Further performance improvements
Further removed multiple pointer instances
Re-use decompression readers (except zstd due to bugs)
2025-04-10 11:20:55 -05:00
Caleb Gardner 6b0e9ef2c6 Reduce use of binary.Read and, by extention, reflection 2025-04-10 06:26:41 -05:00
Caleb Gardner 4490fc3873 Removed all the pointers 2025-04-10 02:15:24 -05:00
Caleb Gardner f242de2710 Better disabling of compression types 2025-03-17 06:53:29 -05:00
Caleb Gardner 88315ee384 Fix build flags 2025-03-17 06:28:31 -05:00
Caleb Gardner 1e2a8f4b75 go mod tidy 2025-03-17 06:22:24 -05:00
Caleb Gardner 863b03fb19 Updated README 2025-03-17 06:21:19 -05:00
Caleb Gardner d3f84344d1 Fix build flags in lzma.go & xz.go 2025-03-17 06:19:45 -05:00
Caleb Gardner ad24995b7b Change no_lzma and no_lzo to no_obsolete and no_gpl
Added build tags section to README
2025-03-17 06:16:25 -05:00
Caleb Gardner 638355ab71 Merge pull request #37 from afbjorklund/comp-none
Allow disabling lzo and lzma
2025-03-17 05:45:26 -05:00
Anders F Björklund 04d914d403 Allow disabling lzo and lzma
By setting the buildtags "no_lzo" and/or "no_lzma",
one can drop the library dependency on lzo and lzma.

The same could be done for xz as well, but there are
still lots of archives using xz compression out there.
2025-03-16 13:56:04 +01:00
Caleb Gardner 7323fe56f6 Merge pull request #35 from afbjorklund/list
Add list option to unsquashfs
2025-03-15 15:48:41 -05:00
Caleb Gardner 6286da31e1 Merge branch 'main' into list 2025-03-15 15:48:06 -05:00
Caleb Gardner 77c87a9653 Merge pull request #34 from afbjorklund/unsquashfs-offset
Allow mounting with an offset
2025-03-15 15:34:04 -05:00
Anders F Björklund e6b0b83dcb Add support for uid/gid 2025-03-15 17:50:02 +01:00
Anders F Björklund cef9090210 Add support for symlinks 2025-03-15 17:46:02 +01:00
Anders F Björklund 24a9457c6b Refactor: export FileInfo 2025-03-15 17:35:40 +01:00
Anders F Björklund e0c1309ed4 Add list option to unsquashfs 2025-03-15 17:32:27 +01:00
Anders F Björklund 8b475b6cc4 Allow mounting with an offset
When mounting squashfs images embedded in apptainer image,
using offset means we don't need to use a temporary copy.
2025-03-15 17:26:59 +01:00
Caleb Gardner 3a48a0bcdc Remove t.Fatal at end of single file test 2025-03-12 00:11:29 -05:00
Caleb Gardner f11416493e Apply FileMode fixes to Inode.Mode() 2025-03-12 00:09:02 -05:00
Caleb Gardner 619bb023b1 Fix missing fileInfo.Mode() types 2025-03-12 00:03:58 -05:00
Caleb Gardner 38e4761d21 Merge pull request #33 from afbjorklund/fileinfo-symlink
Properly show symlinks in Mode
2025-03-11 23:51:01 -05:00
Anders F Björklund 06d2ef3056 Properly show symlinks in Mode
Previously they were extracted OK (as symlinks), but shown
as regular files with length 0 when getting the file info.
2025-03-11 18:46:31 +01:00
Caleb Gardner 446f29df70 Empty io.Reader buffer on EOF 2025-03-04 04:33:47 -06:00
Caleb Gardner d6c8efcfe6 Removed writeToWriteAt
Didn't seem to have any performance advantage
2025-03-04 04:08:13 -06:00
Caleb Gardner d890932d5c Use WriterAt if it's available for FullReader 2025-02-27 07:19:04 -06:00
Caleb Gardner 87b5ac7f5d gopls modernize 2025-02-27 02:46:22 -06:00
Caleb Gardner e9fdd89c67 Merge pull request #31 from willmurphyscode/main
fix: remove stray println
2024-12-10 16:09:45 -06:00
Will Murphy c80d150fdc fix: remove stray println 2024-12-10 17:00:57 -05:00
Caleb Gardner 03266d0560 Fix frag, id, inode table values on block boundries
Fixes bug mention in #30
2024-11-26 17:09:39 -06:00
Caleb Gardner 0f8a4e0027 Re-added NewReaderAtOffset 2024-09-20 20:10:33 -05:00
Caleb Gardner 2a33cad709 PERFORMANCE
Changed some struct values from pointers to normal values for improved performance.
2024-07-17 09:30:16 -05:00
Caleb Gardner e9de9e6ad4 Merge pull request #28 from CalebQ42/exp1
Separation
2023-12-28 00:03:14 -06:00
Caleb Gardner ef72408cd0 Added inode number to directory.Entry 2023-12-27 23:55:57 -06:00
Caleb Gardner 144805e747 Rename squashfslow.Base to squashfslow.FileBase 2023-12-27 23:50:27 -06:00
Caleb Gardner bfba5d5b60 Rename squashfs/squashfs to squashfs/low
squashfs/low library name is now squashfslow
2023-12-27 23:25:49 -06:00
Caleb Gardner 17d45eea50 Finishing touches
Added FastOptions as an alternative to DefaultOptions
A few performance improvements
A few bug fixes
2023-12-27 21:35:40 -06:00
Caleb Gardner d9132ab6a4 Finished. Now for bug fixes 2023-12-24 18:20:05 -06:00
Caleb Gardner 5de59627df Started working on the main library (nearly complete) 2023-12-24 08:05:56 -06:00
Caleb Gardner b2a3920c1f Finished?
Everything seems to extract fine (though more testing is needed)
2023-12-24 06:02:11 -06:00
Caleb Gardner 0449a03428 Added data.FullReader and moved to low level library
Added ability to get readers from Base
2023-12-24 02:55:31 -06:00
Caleb Gardner 0574bbed39 Inode parsing and directory decoding 2023-12-23 05:47:21 -06:00
Caleb Gardner 707391baba Initial work
Create Reader
Pulled back in Inode decoding and superblock
New Data and Metadata readers
Added getting of id, fragment, and export table data lazily
Added README to squashfs/squashfs
2023-12-23 02:48:54 -06:00
Caleb Gardner d4d1b2c2b2 Reset to zero 2023-12-19 03:23:24 -06:00
Caleb Gardner fcd8c4c85b use filepath.Join instead of concatenation 2023-12-08 00:28:17 -06:00
Caleb Gardner 54d193a3df Possible fix for #22
Add sparse support for fragments (undocumented)
2023-08-12 13:30:15 -05:00
Caleb Gardner a129b259be Apply sparse file fix to reader 2023-08-11 18:15:11 -05:00
Caleb Gardner 87f7533a17 Fix Error decompressing files with lots of NULLs #24 2023-08-11 15:32:52 -05:00
Caleb Gardner 7e1a584e8f Fixed an error not being reported in full reader 2023-04-17 11:40:15 -05:00
Caleb Gardner 942e0f770f Set main folder permission 2023-04-17 10:38:44 -05:00
Caleb Gardner 7d16990277 Updated README 2023-04-17 10:33:27 -05:00
Caleb Gardner d2c72f9464 Limit number of simultaneous file extractions to prevent hardlock
Added helper extraction functions
chmod & chown is now set after a folder's extraction to prevent permission issues
2023-04-17 10:22:10 -05:00
Caleb Gardner 2ba4551fb9 Fixed stupid errors 2023-04-17 08:01:20 -05:00
Caleb Gardner 6931075e7e Testing better large file support 2023-04-17 07:51:08 -05:00
Caleb Gardner 55a25c9d45 Updated README 2023-04-12 08:44:48 -05:00
Caleb Gardner 94b45c8402 Added IgnorePerm to ExtractionOptions 2023-04-12 07:57:57 -05:00
Caleb Gardner 01de43a5ae Added ErrReadNotFile to ReatAt, WriteTo 2023-04-11 00:34:43 -05:00
Caleb Gardner 5b29f4d029 Updated README 2023-04-09 21:09:53 -05:00
Caleb Gardner 6c7e926649 Updated deps 2023-04-09 20:41:45 -05:00
Caleb Gardner 72d85d7810 Added (expiremental) support for device files and Fifo 2023-04-09 20:41:00 -05:00
Caleb Gardner 67df5f40c6 Fix #21 and update deps 2023-03-29 12:45:21 -05:00
Caleb Gardner 1ae5593e6c Merge pull request #20 from x1unix/main
fix: use correct ENODATA value on different platforms
2023-03-18 06:57:45 -05:00
ds.hiveon 653c4a167b fix: remove redundant build tag 2023-03-18 07:46:29 +01:00
ds.hiveon 9fe17650b8 fix: use correct ENODATA value on different platforms 2023-03-18 07:44:18 +01:00
Caleb Gardner e9e967f085 Fuse2 fix 2023-01-18 02:31:08 -06:00
Caleb Gardner 187da99dd6 Added fuse2 mounting 2023-01-17 10:20:11 -06:00
Caleb Gardner 75d2a29319 Export SquashFuse 2023-01-17 09:56:34 -06:00
Caleb Gardner ce2e45ceec Fixed issues with decompress.Decoder 2023-01-05 01:29:23 -06:00
Caleb Gardner 089ef53c8c Revert changes to fullreader 2023-01-04 06:40:57 -06:00
Caleb Gardner 658e5c9e0b Mount is non-blocking again 2023-01-04 06:01:12 -06:00
Caleb Gardner f2d86aff96 Fixed a race condition with mounts that caused them to fail 2023-01-04 05:41:43 -06:00
Caleb Gardner f61237a1f0 Added ReaderAtOffset 2022-12-22 02:00:42 -06:00
Caleb Gardner 820e06e792 fuse Serve in goroutine 2022-12-17 17:06:28 -06:00
Caleb Gardner 4f8f5f6928 Tweaks to decode interface 2022-12-17 16:47:33 -06:00
Caleb Gardner 1b5078c7bd Messing around with optimizations. 2022-12-14 13:48:22 -06:00
Caleb Gardner 56fdba2f28 Merge pull request #17 from CalebQ42/fuseBraz
Fuse SUCCESS
2022-12-03 02:50:17 -06:00
Caleb Gardner ffbf4ebc64 Fuse SUCCESS 2022-12-03 02:45:58 -06:00
Belac Darkstorm a015b16293 Clean path before checking if valid. 2022-10-24 03:17:55 -05:00
Caleb Gardner 327781d86e Fixed issues with fragments 2022-08-26 12:11:27 -05:00
Caleb Gardner 4efd2ee49d Merge pull request #16 from tri-adam/0.6.0-fixes
v0.6.0 fixes
2022-08-26 11:44:16 -05:00
Caleb Gardner 392193993c Added single file test 2022-08-26 11:43:46 -05:00
Adam Hughes 2230a449ec fix: use fs interfaces in type assertions
Previous code would panic due to invalid type assertions (presumably due
to change of type returned by func Sub). Switching to relevant fs
interface types fixes the issue and should work going forward, even if
the type is changed.

Signed-off-by: Adam Hughes <9903835+tri-adam@users.noreply.github.com>
2022-08-26 15:10:51 +00:00
Adam Hughes 0e50efea64 fix: use correct count when reading fragments
Signed-off-by: Adam Hughes <9903835+tri-adam@users.noreply.github.com>
2022-08-26 15:00:00 +00:00
Caleb Gardner 7a22538623 Finishing touches? 2022-08-26 05:01:17 -05:00
Caleb Gardner 3bf851852f Updated README (limitations) 2022-06-21 01:25:00 -05:00
Caleb Gardner ac89ff7275 Updated README. Performance may not be very good... 2022-06-21 01:23:41 -05:00
Caleb Gardner 83dfa77b7d Potential workaround for poor zstd performance
Performance is still not great, but better
2022-06-21 01:09:33 -05:00
Caleb Gardner 1b934de04d Messing with stuff 2022-06-19 16:25:50 -05:00
Caleb Gardner 981f1697ab Added benchmark race to proper tests 2022-06-19 02:02:31 -05:00
Caleb Gardner 214419b5c3 IT WORKS (again) 2022-06-19 00:32:33 -05:00
Caleb Gardner 8f5e1fef96 NEW ISSUES 2022-06-18 14:40:33 -05:00
Caleb Gardner 49595de3f2 Re-wrote metadata reader. Seems to work now.
Need to work on test now.
2022-06-18 14:31:17 -05:00
Caleb Gardner 96b38935a6 Found the problem file.
NOW TO DEBUG
2022-06-18 06:46:00 -05:00
Caleb Gardner 9ac8fef3b2 Fixing issues 2022-06-18 06:30:04 -05:00
Caleb Gardner cde6a265a1 Started work on proper tests.
STILL HAVING STUPID UNEXPLAINABLE NIL POINTERS.
2022-06-18 01:32:51 -05:00
Caleb Gardner 8613e35221 Fixed some bugs
THINGS ARE BROKEN FOR NO REASON
2022-05-10 01:40:32 -05:00
Caleb Gardner 16ef5838c3 Move changes from exp2 to main
This is largely a move to simplify a lot of the readers
Also further breaks out functions.
2022-05-10 01:12:13 -05:00
Caleb Gardner 0a2ced9072 Merge pull request #11 from tri-adam/path-fix
fix: handle paths with special characters
2022-04-22 04:25:42 -05:00
Adam Hughes a908d69987 fix: handle paths with special characters
Use direct comparison of filenames rather than path.Match, which gives
characters such as '[' special meaning, resulting in unexpected failures
when calling Open, ReadDir, Stat, or Sub.
2022-04-22 05:02:57 +00:00
Caleb Gardner 6ada4f3b49 Create FUNDING.yml 2021-12-31 00:35:04 -06:00
Caleb Gardner 89f28cec6e Merge pull request #6 from stffabi/feature/support-reading-dot
Support reading "." for fs.FS
2021-12-02 07:40:27 -06:00
stffabi c988309edc Support reading "." for fs.FS 2021-12-02 13:43:18 +01:00
Caleb Gardner e8a8c531a9 Tweaks to make FromReader work 2021-09-27 01:27:58 -05:00
Caleb Gardner 80ff4466ae Added new test 2021-09-26 22:36:30 -05:00
Caleb Gardner 64055a8a63 Improved testing 2021-09-26 19:10:43 -05:00
Caleb Gardner 0402b0a2ee Bringing rawreader from expiremental branch.
Now allows creation of a squashfs.Reader from an io.Reader
2021-09-26 18:30:08 -05:00
Caleb Gardner 305f261d10 Add Lzo decompressor and Xz decompressor with filters 2021-09-12 05:26:47 -05:00
Caleb Gardner 70e3d81427 Some musings on what to do. 2021-04-30 03:32:31 -05:00
Caleb Gardner 6ad6857d8d Renamed files to make them more clear
Trying to figure out how to write. Might have to keep tables uncompressed for now.
2021-04-30 02:52:27 -05:00
Caleb Gardner 7cf15d48c7 Getting back into it. Maybe. 2021-04-04 09:39:28 -05:00
Caleb Gardner 28f39cf315 Updated libraries.
Replaced builtin zlib with faster library.
2021-04-03 10:14:12 -05:00
Caleb Gardner 7f5fa3ba1f Some quick fixes for "correctness" 2021-04-03 01:17:09 -05:00
Caleb Gardner 2a8310a724 Updated README 2021-02-27 01:58:01 -06:00
Caleb Gardner 8ad4d30bc7 Merge branch 'main' of https://github.com/CalebQ42/squashfs 2021-02-27 00:50:40 -06:00
Caleb Gardner b6fbd63ba4 Makes sure folders are created when files are added
Added some comments
2021-02-27 00:49:40 -06:00
Caleb Gardner 9913b848c6 Added some data writing logic. 2021-02-26 01:53:16 -06:00
Caleb Gardner 65bc4a5d78 More work on fragments. 2021-02-25 06:22:57 -06:00
Caleb Gardner c9d451e24c Added fragment calculations (untested). 2021-02-25 03:17:20 -06:00
Caleb Gardner ae5ade0683 Merge pull request #3 from CalebQ42/go1.16_fs3
Added 1.16 io/fs interfaces
2021-02-24 23:40:20 -06:00
Caleb Gardner a7b6801d2b Merge branch 'main' into go1.16_fs3 2021-02-24 23:39:45 -06:00
Caleb Gardner a123935250 Added more necessary parts to compression. 2021-02-24 05:58:18 -06:00
Caleb Gardner 8a91e1a1c1 Getting back into it... 2021-02-23 22:26:15 -06:00
Caleb Gardner 07962426b2 Minor work on the Writer 2021-02-03 14:03:21 -06:00
Caleb Gardner d89153c3e2 Finished io/FS interface 2021-01-30 06:30:00 -06:00
Caleb Gardner 3f1b2a8d1e Restructure for 1.16 io/fs interface 2021-01-29 12:55:57 -06:00
Caleb Gardner 3691e1f486 Merge pull request #2 from srevinsaju/patch-1
fix: typo in readme
2021-01-27 08:35:41 -06:00
Srevin Saju 8ab566521d fix: typo in readme 2021-01-27 12:54:29 +03:00
Caleb Gardner dd08d3516d Reader will now check BlockLog 2021-01-20 14:10:14 -06:00
Caleb Gardner 8dba30e24f Some setup for 1.16 2021-01-20 12:55:39 -06:00
Caleb Gardner d4e2577075 More work on writing 2021-01-17 02:09:13 -06:00
Caleb Gardner 23371163c0 Merge branch 'main' of https://github.com/CalebQ42/squashfs 2021-01-16 15:43:04 -06:00
Caleb Gardner 69f56d6951 Working on how to actually write the archive.
Made SuperblockFlags public so you can set options when writing
2021-01-16 15:42:55 -06:00
Caleb Gardner 17e1d65488 Fixed Extended Files 2021-01-16 03:09:48 -06:00
Caleb Gardner 80946f58e7 Fixed issue with Extended Symlinks
Removed some shadowed err's
2021-01-16 01:32:00 -06:00
Caleb Gardner 4187598783 A couple of fixes.
GetChildrenRecursively is no longer threaded so it's more consistent
Fixed GetFileAtPath, specifically when getting the root dir
2021-01-15 10:57:03 -06:00
Caleb Gardner 9cf92c4916 Removed some shadowed values (thanks gopls) 2021-01-13 11:45:10 -06:00
Caleb Gardner 407d649b3d Finished FixSymlinks (in theory) 2021-01-13 06:02:15 -06:00
Caleb Gardner dcf59a4261 The root inode is now only initialized once.
Privated File.Reader because it really shouldn't be public.
2021-01-12 01:43:03 -06:00
Caleb Gardner 1506ca0ac3 updated dependencies 2021-01-10 04:39:19 -06:00
Caleb Gardner fe9344b633 Fixed issue with not all data being extracted with ExtractTo 2021-01-10 04:25:09 -06:00
Caleb Gardner 76649fde7f Implemented WriteTo which halves decompress times.
Added a drag race benchmark (for the fun of it)
2021-01-10 03:33:33 -06:00
Caleb Gardner ee9406513c Added a new test for fun to compare vs unsquashfs 2021-01-09 10:15:56 -06:00
Caleb Gardner 18092c63aa Some cleanup, no change in functionality 2021-01-09 09:53:58 -06:00
Caleb Gardner b2d6ff56f6 Added uid/guid support.
Added permission support.
2021-01-09 02:30:04 -06:00
Caleb Gardner 4b3d5d12f8 Setup for differnet types of compression for Writer
Added some TODOs
2021-01-08 14:35:32 -06:00
Caleb Gardner 3f71404a2a Re-Write of the Writer to make it simpler 2021-01-08 05:09:38 -06:00
Caleb Gardner 35d22b4bd0 Removed close function from squashfs.File
This seems to cause issues in ver specific circumstances and ultimately, isn't needed.
2021-01-06 12:56:09 -06:00
Caleb Gardner 97b12090c6 Trying to figure out the best way to convert files 2021-01-05 05:53:16 -06:00
Caleb Gardner 43fe4f91a2 More work on Writer
Adding files to the Writer should work properly now (except symlinks)
Threaded the adding of files.
Added ability to ignore errors when adding files.
2021-01-03 04:39:39 -06:00
Caleb Gardner 9524a2c192 More work on Writer 2021-01-02 17:10:21 -06:00
Caleb Gardner 162b228881 Some beginning work on squashfs.Writer
Just laying out some of the functions and what they do.
2021-01-01 10:54:09 -06:00
Caleb Gardner 7f87999a8f Merge branch 'main' of https://github.com/CalebQ42/squashfs into main 2020-12-28 11:51:08 -06:00
Caleb Gardner 47c28baf87 Improved data structure for structs.
Thanks gopls with VS Code
2020-12-28 11:50:56 -06:00
Caleb Gardner a298a3d7b5 Fixed first byte of data blocks being cut off 2020-12-27 02:10:51 -06:00
56 changed files with 3183 additions and 2309 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: CalebQ42
+2 -1
View File
@@ -1 +1,2 @@
testing
testing
/go-unsquashfs
+36 -6
View File
@@ -1,14 +1,44 @@
# squashfs (WIP)
# squashfs
[![PkgGoDev](https://pkg.go.dev/badge/github.com/CalebQ42/squashfs)](https://pkg.go.dev/github.com/CalebQ42/squashfs) [![Go Report Card](https://goreportcard.com/badge/github.com/CalebQ42/squashfs)](https://goreportcard.com/report/github.com/CalebQ42/squashfs)
A PURE Go library to read and write squashfs.
A PURE Go library to read squashfs. There is currently no plans to add archive creation support as it will almost always be better to just call `mksquashfs`. I could see some possible use cases, but probably won't spend time on it unless it's requested (open a discussion if you want this feature).
Currently has support for reading squashfs files and extracting files and folders. Supports all compression types except LZO, but additional compression options are hit or miss.
The library has two parts with this `github.com/CalebQ42/squashfs` being easy to use as it implements `io/fs` interfaces and doesn't expose unnecessary information. 95% this is the library you want. If you need lower level access to the information, use `github.com/CalebQ42/squashfs/low` where far more information is exposed.
The only major thing missing from squashfs reading is Xattr parsing.
Currently has support for reading squashfs files and extracting files and folders.
Special thanks to https://dr-emann.github.io/squashfs/ for some VERY important information in an easy to understand format.
Special thanks to <https://dr-emann.github.io/squashfs/> for some VERY important information in an easy to understand format.
Thanks also to [distri's squashfs library](https://github.com/distr1/distri/tree/master/internal/squashfs) as I referenced it to figure some things out (and double check others).
# [TODO](https://github.com/CalebQ42/squashfs/projects/1?fullscreen=true)
## Build tags
As of `v1.1.0` this library has two optional build tags: `no_gpl` and `no_obsolete`. `no_gpl` disables the ability to read archives with lzo compression due to the library's gpl license. `no_obsolete` removes "obsolete" compression types for a reduced compilation size; currently this only disable lzma compression since it's superseded by xz.
## FUSE
As of `v1.0`, FUSE capabilities has been moved to [a separate library](https://github.com/CalebQ42/squashfuse).
## Limitations
* No Xattr parsing.
* Socket files are not extracted.
* From my research, it seems like a socket file would be useless if it could be created.
* Fifo files are ignored on `darwin`
## Issues
* Noticably slower then `unsquashfs` for extraction, especially on larger images.
* This seems to be related to above along with the general optimization of `unsquashfs` and it's compression libraries.
* Times seem to be largely dependent on file tree size and compression type.
* My main testing image (~100MB) using Zstd takes ~2x longer.
* An Arch Linux airootfs image (~780MB) using XZ compression with LZMA filters takes ~28x longer.
* A Tensorflow docker image (~3.3GB) using Zstd takes ~3x longer.
Note: These numbers are using `FastOptions()`. `DefaultOptions()` takes ~2x longer.
## Recommendations on Usage
Due to the above performance consideration, this library should only be used to access files within the archive without extraction, or to mount it via Fuse.
* Neither of these use cases are largely effected by the issue above.
+148
View File
@@ -0,0 +1,148 @@
package main
import (
"flag"
"fmt"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/CalebQ42/squashfs"
squashfslow "github.com/CalebQ42/squashfs/low"
)
func userName(uid int, numeric bool) string {
us := strconv.Itoa(uid)
if numeric {
return us
}
if u, err := user.LookupId(us); err == nil {
return u.Username
}
return us
}
func groupName(gid int, numeric bool) string {
gs := strconv.Itoa(gid)
if numeric {
return gs
}
if g, err := user.LookupGroupId(gs); err == nil {
return g.Name
}
return gs
}
var hardLinks = make(map[uint32]string)
func printFile(rdr *squashfs.Reader, path string, f *squashfs.File) {
path = filepath.Join(path, f.Low.Name)
fi, _ := f.Stat()
sfi := fi.(squashfs.FileInfo)
owner := fmt.Sprintf("%s/%s",
userName(sfi.Uid(), *numeric),
groupName(sfi.Gid(), *numeric))
var link string
var isHardLink bool
if *showHardLinks {
link, isHardLink = hardLinks[f.Low.Inode.Num]
if !isHardLink {
hardLinks[f.Low.Inode.Num] = path
}
}
var size int64
if isHardLink {
size = 0
} else {
size = fi.Size()
}
if sfi.IsSymlink() {
link = " -> " + sfi.SymlinkPath()
} else if isHardLink {
link = " link to " + link
}
fmt.Printf("%s %s %*d %s %s%s\n",
strings.ToLower(fi.Mode().String()),
owner, 26-len(owner), size,
fi.ModTime().Format("2006-01-02 15:04"),
path, link)
if f.IsDir() {
fs, _ := f.FS()
printDir(rdr, path, fs)
}
}
func printDir(rdr *squashfs.Reader, path string, f squashfs.FS) {
var base squashfslow.FileBase
var fil squashfs.File
var err error
for _, e := range f.LowDir.Entries {
base, err = rdr.Low.BaseFromEntry(e)
if err != nil {
panic(err)
}
fil = rdr.FileFromBase(base, f)
printFile(rdr, path, &fil)
}
}
var (
verbose *bool
list *bool
long *bool
numeric *bool
offset *int64
ignore *bool
file *string
showHardLinks *bool
)
func main() {
verbose = flag.Bool("v", false, "Verbose")
list = flag.Bool("l", false, "List")
long = flag.Bool("ll", false, "List with attributes")
numeric = flag.Bool("lln", false, "List with attributes and numeric ids")
showHardLinks = flag.Bool("show-hard-links", false, "When used with ll or lln, shows hard links")
offset = flag.Int64("o", 0, "Offset")
ignore = flag.Bool("ip", false, "Ignore Permissions and extract all files/folders with 0755")
file = flag.String("e", "", "File or folder to extract")
flag.Parse()
if (*list || *long || *numeric) && flag.NArg() < 1 {
fmt.Println("Please provide a file name")
os.Exit(0)
} else if (!*list && !*long && !*numeric) && flag.NArg() < 2 {
fmt.Println("Please provide a file name and extraction path")
os.Exit(0)
}
f, err := os.Open(flag.Arg(0))
if err != nil {
panic(err)
}
r, err := squashfs.NewReaderAtOffset(f, *offset)
if err != nil {
panic(err)
}
extractFil := r.File()
if *file != "" {
extractFil, err = r.OpenFile(*file)
if err != nil {
panic(err)
}
}
if *list || *long || *numeric {
printFile(&r, "", extractFil)
return
}
op := squashfs.DefaultOptions()
op.Verbose = *verbose
op.IgnorePerm = *ignore
n := time.Now()
err = extractFil.ExtractWithOptions(flag.Arg(1), op)
if err != nil {
panic(err)
}
fmt.Println("Took:", time.Since(n))
}
-190
View File
@@ -1,190 +0,0 @@
package squashfs
import (
"bytes"
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/inode"
)
var (
//ErrInodeNotFile is given when giving an inode, but the function requires a file inode.
errInodeNotFile = errors.New("Given inode is NOT a file type")
//ErrInodeOnlyFragment is given when trying to make a DataReader from an inode, but the inode only had data in a fragment
errInodeOnlyFragment = errors.New("Given inode ONLY has fragment data")
)
//DataReader reads data from data blocks.
type dataReader struct {
r *Reader
offset int64 //offset relative to the beginning of the squash file
blocks []dataBlock
curBlock int //Which block in sizes is currently cached
curData []byte
curReadOffset int //offset relative to the currently cached data
}
//DataBlock holds info about a given data block from it's size
type dataBlock struct {
begOffset int64 //The offset relative to the beginning of the squash file. Makes it easier to seek to it.
size uint32
compressed bool
uncompressedSize uint32
}
//NewDataBlock creates a new squashfs.datablock from a given size.
func newDataBlock(raw uint32) (dbs dataBlock) {
dbs.compressed = raw&(1<<24) != (1 << 24)
dbs.size = raw &^ (1 << 24)
if !dbs.compressed {
dbs.uncompressedSize = dbs.size
}
return
}
//NewDataReader creates a new data reader at the given offset, with the blocks defined by sizes
func (r *Reader) newDataReader(offset int64, sizes []uint32) (*dataReader, error) {
var dr dataReader
dr.r = r
dr.offset = offset
for _, size := range sizes {
dr.blocks = append(dr.blocks, newDataBlock(size))
}
err := dr.readCurBlock()
if err != nil {
return nil, err
}
return &dr, nil
}
//NewDataReaderFromInode creates a new DataReader from a given inode. Inode must be of BasicFile or ExtendedFile types
func (r *Reader) newDataReaderFromInode(i *inode.Inode) (*dataReader, error) {
var rdr dataReader
rdr.r = r
switch i.Type {
case inode.BasicFileType:
fil := i.Info.(inode.BasicFile)
if fil.Init.BlockStart == 0 {
return nil, errInodeOnlyFragment
}
rdr.offset = int64(fil.Init.BlockStart)
for _, sizes := range fil.BlockSizes {
rdr.blocks = append(rdr.blocks, newDataBlock(sizes))
}
if fil.Fragmented {
rdr.blocks = rdr.blocks[:len(rdr.blocks)-1]
}
case inode.ExtFileType:
fil := i.Info.(inode.ExtendedFile)
if fil.Init.BlockStart == 0 {
return nil, errInodeOnlyFragment
}
rdr.offset = int64(fil.Init.BlockStart)
for _, sizes := range fil.BlockSizes {
rdr.blocks = append(rdr.blocks, newDataBlock(sizes))
}
if fil.Fragmented {
rdr.blocks = rdr.blocks[:len(rdr.blocks)-1]
}
default:
return nil, errInodeNotFile
}
err := rdr.readCurBlock()
if err != nil {
return nil, err
}
return &rdr, nil
}
func (d *dataReader) readNextBlock() error {
d.curBlock++
if d.curBlock >= len(d.blocks) {
d.curBlock--
return io.EOF
}
err := d.readCurBlock()
if err != nil {
d.curBlock--
d.readCurBlock()
return err
}
return nil
}
func (d *dataReader) readCurBlock() error {
if d.curBlock >= len(d.blocks) {
return io.EOF
}
if d.blocks[d.curBlock].size == 0 {
d.curData = make([]byte, d.r.super.BlockSize)
d.blocks[d.curBlock].uncompressedSize = d.r.super.BlockSize
d.blocks[d.curBlock].begOffset = d.offset
return nil
}
sec := io.NewSectionReader(d.r.r, d.offset, int64(d.blocks[d.curBlock].size))
if d.blocks[d.curBlock].compressed {
btys, err := d.r.decompressor.Decompress(sec)
if err != nil {
return err
}
d.blocks[d.curBlock].uncompressedSize = uint32(len(btys))
d.curData = btys
d.blocks[d.curBlock].begOffset = d.offset
d.offset += int64(d.blocks[d.curBlock].size)
return nil
}
var buf bytes.Buffer
_, err := io.Copy(&buf, sec)
if err != nil {
return err
}
d.curData = buf.Bytes()
d.blocks[d.curBlock].begOffset = d.offset
d.offset += int64(d.blocks[d.curBlock].size)
return err
}
//Close frees up the curData from memory
func (d *dataReader) Close() error {
d.curData = nil
return nil
}
func (d *dataReader) Read(p []byte) (int, error) {
if d.curData == nil {
err := d.readCurBlock()
if err != nil {
return 0, err
}
}
if d.curReadOffset+len(p) <= len(d.curData) {
for i := 0; i < len(p); i++ {
p[i] = d.curData[d.curReadOffset+i]
}
d.curReadOffset += len(p)
return len(p), nil
}
read := 0
for read < len(p) {
if d.curReadOffset == len(d.curData) {
err := d.readNextBlock()
if err != nil {
return read, err
}
d.curReadOffset = 0
}
for ; read < len(p); read++ {
d.curReadOffset++
if d.curReadOffset < len(d.curData) {
p[read] = d.curData[d.curReadOffset]
} else {
break
}
}
}
if read != len(p) {
return read, errors.New("Didn't read enough data")
}
return read, nil
}
+49
View File
@@ -0,0 +1,49 @@
package squashfs
import (
"io"
"io/fs"
"runtime"
"github.com/CalebQ42/squashfs/internal/routinemanager"
)
type ExtractionOptions struct {
manager *routinemanager.Manager
LogOutput io.Writer //Where the verbose log should write.
DereferenceSymlink bool //Replace symlinks with the target file.
UnbreakSymlink bool //Try to make sure symlinks remain unbroken when extracted, without changing the symlink.
Verbose bool //Prints extra info to log on an error.
IgnorePerm bool //Ignore file's permissions and instead use Perm.
Perm fs.FileMode //Permission to use when IgnorePerm. Defaults to 0777.
SimultaneousFiles uint16 //Number of files to process in parallel. Default set based on runtime.NumCPU().
ExtractionRoutines uint16 //Number of goroutines to use for each file's extraction. Only applies to regular files. Default set based on runtime.NumCPU().
}
// The default extraction options.
func DefaultOptions() *ExtractionOptions {
cores := uint16(runtime.NumCPU() / 2)
var files, routines uint16
if cores <= 4 {
files = 1
routines = cores
} else {
files = cores - 4
routines = 4
}
return &ExtractionOptions{
Perm: 0777,
SimultaneousFiles: files,
ExtractionRoutines: routines,
}
}
// Less limited default options. Can run up 2x faster than DefaultOptions.
// Tends to use all available CPU resources.
func FastOptions() *ExtractionOptions {
return &ExtractionOptions{
Perm: 0777,
SimultaneousFiles: uint16(runtime.NumCPU()),
ExtractionRoutines: uint16(runtime.NumCPU()),
}
}
+403 -521
View File
@@ -2,559 +2,441 @@ package squashfs
import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"path"
"strings"
"time"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/routinemanager"
squashfslow "github.com/CalebQ42/squashfs/low"
"github.com/CalebQ42/squashfs/low/data"
"github.com/CalebQ42/squashfs/low/inode"
)
var (
//ErrNotDirectory is returned when you're trying to do directory things with a non-directory
errNotDirectory = errors.New("File is not a directory")
//ErrNotFile is returned when you're trying to do file things with a directory
errNotFile = errors.New("File is not a file")
//ErrNotReading is returned when running functions that are only meant to be used when reading a squashfs
errNotReading = errors.New("Function only supported when reading a squashfs")
//ErrBrokenSymlink is returned when using ExtractWithOptions with the unbreakSymlink set to true, but the symlink's file cannot be extracted.
ErrBrokenSymlink = errors.New("Extracted symlink is probably broken")
)
//File is the main way to interact with files within squashfs, or when putting files into a squashfs.
//File can be either a file or folder. When reading from a squashfs, it reads from the datablocks.
//When writing, this holds the information on WHERE the file will be placed inside the archive.
//
//Implements os.FileInfo and io.ReadCloser
// File represents a file inside a squashfs archive.
type File struct {
Parent *File //The parent directory. Should ALWAYS be a folder. If it's the root directory, will be nil
Reader io.Reader //Underlying reader. When writing, will probably be an os.File. When reading this is kept nil UNTIL reading to save memory.
name string //The name of the file or folder. Root folder will not have a name ("")
path string //The path to the folder the File is located in.
r *Reader //The squashfs.Reader where this file is contained.
in *inode.Inode //Underlyting inode when reading.
filType int //The file's type, using inode types.
full data.FullReader
rdr data.Reader
rdrInit bool
parent FS
r *Reader
Low squashfslow.FileBase
dirsRead int
}
//get a File from a directory.entry
func (r *Reader) newFileFromDirEntry(entry *directory.Entry) (fil *File, err error) {
fil = new(File)
fil.in, err = r.getInodeFromEntry(entry)
if err != nil {
return nil, err
}
fil.name = entry.Name
fil.r = r
fil.filType = fil.in.Type
return
}
//Name is the file's name
func (f *File) Name() string {
return f.name
}
//Size is the complete size of the file. Zero if it's not a file.
func (f *File) Size() int64 {
switch f.filType {
case inode.BasicFileType:
return int64(f.in.Info.(inode.BasicFile).Init.Size)
case inode.ExtFileType:
return int64(f.in.Info.(inode.ExtendedFile).Init.Size)
default:
return 0
// Creates a new *File from the given *squashfs.Base
func (r *Reader) FileFromBase(b squashfslow.FileBase, parent FS) File {
return File{
Low: b,
parent: parent,
r: r,
}
}
//ModTime is the time of last modification.
func (f *File) ModTime() time.Time {
return time.Unix(int64(f.in.Header.ModifiedTime), 0)
}
//Sys returns the underlying reader, file.Reader. If the reader isn't initialized, it will initialize it.
//If called on something other then a file, returns nil.
func (f *File) Sys() interface{} {
if f.IsFile() {
if f.Reader == nil && f.r != nil {
var err error
f.Reader, err = f.r.newFileReader(f.in)
if err != nil {
return nil
}
}
return f.Reader
}
return nil
}
//GetChildren returns a *squashfs.File slice of every direct child of the directory. If the File is not a directory, will return ErrNotDirectory
func (f *File) GetChildren() (children []*File, err error) {
children = make([]*File, 0)
if f.r == nil {
return nil, errNotReading
}
func (f File) FS() (FS, error) {
if !f.IsDir() {
return nil, errNotDirectory
return FS{}, errors.New("not a directory")
}
dir, err := f.r.readDirFromInode(f.in)
d, err := f.Low.ToDir(f.r.Low)
if err != nil {
return
return FS{}, err
}
var fil *File
for _, entry := range dir.Entries {
fil, err = f.r.newFileFromDirEntry(&entry)
if err != nil {
return
}
fil.Parent = f
if f.name != "" {
fil.path = f.Path()
}
children = append(children, fil)
}
return
return FS{LowDir: d, parent: &f.parent, r: f.r}, nil
}
//GetChildrenRecursively returns ALL children. Goes down ALL folder paths.
func (f *File) GetChildrenRecursively() (children []*File, err error) {
children = make([]*File, 0)
if f.r == nil {
return nil, errNotReading
}
if !f.IsDir() {
return nil, errNotDirectory
}
children, err = f.GetChildren()
if err != nil {
return
}
var childFolders []*File
for _, child := range children {
if child.IsDir() {
childFolders = append(childFolders, child)
}
}
foldChil := make(chan []*File)
errChan := make(chan error)
for _, folds := range childFolders {
go func(fil *File) {
childs, err := fil.GetChildrenRecursively()
errChan <- err
foldChil <- childs
}(folds)
}
for range childFolders {
err = <-errChan
if err != nil {
return
}
children = append(children, <-foldChil...)
}
return
}
//Path returns the path of the file within the archive.
func (f *File) Path() string {
if f.name == "" {
return f.path
}
return f.path + "/" + f.name
}
//GetFileAtPath tries to return the File at the given path, relative to the file.
//Returns nil if called on something other then a folder, OR if the path goes oustide the archive.
//Allows wildcards supported by path.Match (namely * and ?).
func (f *File) GetFileAtPath(dirPath string) *File {
if dirPath == "" {
return f
}
dirPath = strings.TrimSuffix(strings.TrimPrefix(dirPath, "/"), "/")
if dirPath != "" && !f.IsDir() {
return nil
}
for strings.HasSuffix(dirPath, "./") {
//since you can TECHNICALLY have an infinite amount of ./ and it would still be valid.
dirPath = strings.TrimPrefix(dirPath, "./")
}
split := strings.Split(dirPath, "/")
if split[0] == ".." && f.name == "" {
return nil
} else if split[0] == ".." {
if f.Parent != nil {
return f.Parent.GetFileAtPath(strings.Join(split[1:], "/"))
}
return nil
}
children, err := f.GetChildren()
if err != nil {
return nil
}
for _, child := range children {
eq, _ := path.Match(split[0], child.name)
if eq {
return child.GetFileAtPath(strings.Join(split[1:], "/"))
}
}
return nil
}
//IsDir returns if the file is a directory.
func (f *File) IsDir() bool {
return f.filType == inode.BasicDirectoryType || f.filType == inode.ExtDirType
}
//IsSymlink returns if the file is a symlink.
func (f *File) IsSymlink() bool {
return f.filType == inode.BasicSymlinkType || f.filType == inode.ExtSymlinkType
}
//IsFile returns if the file is a file.
func (f *File) IsFile() bool {
return f.filType == inode.BasicFileType || f.filType == inode.ExtFileType
}
//SymlinkPath returns the path the symlink is pointing to. If the file ISN'T a symlink, will return an empty string.
//If a path begins with "/" then the symlink is pointing to an absolute path (starting from root, and not a file inside the archive)
func (f *File) SymlinkPath() string {
switch f.filType {
case inode.BasicSymlinkType:
return f.in.Info.(inode.BasicSymlink).Path
case inode.ExtSymlinkType:
return f.in.Info.(inode.ExtendedSymlink).Path
default:
return ""
}
}
//GetSymlinkFile tries to return the squashfs.File associated with the symlink. If the file isn't a symlink
//or the symlink points to a location outside the archive, nil is returned.
func (f *File) GetSymlinkFile() *File {
if !f.IsSymlink() {
return nil
}
if strings.HasSuffix(f.SymlinkPath(), "/") {
return nil
}
return f.Parent.GetFileAtPath(f.SymlinkPath())
}
//GetSymlinkFileRecursive tries to return the squasfs.File associated with the symlink. It will recursively
//try to get the symlink's file. This will return either a non-symlink File, or nil.
func (f *File) GetSymlinkFileRecursive() *File {
if !f.IsSymlink() {
return nil
}
if strings.HasSuffix(f.SymlinkPath(), "/") {
return nil
}
sym := f
for {
sym = sym.GetSymlinkFile()
if sym == nil {
return nil
}
if !sym.IsSymlink() {
return sym
}
}
}
//Mode returns the os.FileMode of the File. Sets mode bits for directories and symlinks.
func (f *File) Mode() os.FileMode {
mode := os.FileMode(f.in.Header.Permissions)
switch {
case f.IsDir():
mode = mode | os.ModeDir
case f.IsSymlink():
mode = mode | os.ModeSymlink
}
return mode
}
//ExtractTo extracts the file to the given path. This is the same as ExtractWithOptions(path, false, false, os.ModePerm, false).
//Will NOT try to keep symlinks valid, folders extracted will have the permissions set by the squashfs, but the folder to make path will have full permissions (777).
//
//Will try it's best to extract all files, and if any errors come up, they will be appended to the error slice that's returned.
func (f *File) ExtractTo(path string) []error {
return f.ExtractWithOptions(path, false, false, os.ModePerm, false)
}
//ExtractSymlink is similar to ExtractTo, but when it extracts a symlink, it instead extracts the file associated with the symlink in it's place.
//This is the same as ExtractWithOptions(path, true, false, os.ModePerm, false)
func (f *File) ExtractSymlink(path string) []error {
return f.ExtractWithOptions(path, true, false, os.ModePerm, false)
}
//ExtractWithOptions will extract the file to the given path, while allowing customization on how it works. ExtractTo is the "default" options.
//Will try it's best to extract all files, and if any errors come up, they will be appended to the error slice that's returned.
//Should only return multiple errors if extracting a folder.
//
//If dereferenceSymlink is set, instead of extracting a symlink, it will extract the file the symlink is pointed to in it's place.
//If both dereferenceSymlink and unbreakSymlink is set, dereferenceSymlink takes precendence.
//
//If unbreakSymlink is set, it will also try to extract the symlink's associated file. WARNING: the symlink's file may have to go up the directory to work.
//If unbreakSymlink is set and the file cannot be extracted, a ErrBrokenSymlink will be appended to the returned error slice.
//
//folderPerm only applies to the folders created to get to path. Folders from the archive are given the correct permissions defined by the archive.
func (f *File) ExtractWithOptions(path string, dereferenceSymlink, unbreakSymlink bool, folderPerm os.FileMode, verbose bool) (errs []error) {
errs = make([]error, 0)
err := os.MkdirAll(path, folderPerm)
if err != nil {
return []error{err}
}
switch {
case f.IsDir():
if f.name != "" {
//TODO: check if folder is present, and if so, try to set it's permission
err = os.Mkdir(path+"/"+f.name, os.ModePerm)
if err != nil {
if verbose {
fmt.Println("Error while making: ", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil, err := os.Open(path + "/" + f.name)
if err != nil {
if verbose {
fmt.Println("Error while opening:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil.Chown(int(f.r.idTable[f.in.Header.UID]), int(f.r.idTable[f.in.Header.GID]))
//don't mention anything when it fails. Because it fails often. Probably has something to do about uid & gid 0
// if err != nil {
// if verbose {
// fmt.Println("Error while changing owner:", path+"/"+f.Name)
// fmt.Println(err)
// }
// errs = append(errs, err)
// }
err = fil.Chmod(f.Mode())
if err != nil {
if verbose {
fmt.Println("Error while changing owner:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
}
}
children, err := f.GetChildren()
if err != nil {
if verbose {
fmt.Println("Error getting children for:", f.Path())
fmt.Println(err)
}
errs = append(errs, err)
return
}
finishChan := make(chan []error)
defer close(finishChan)
for _, child := range children {
go func(child *File) {
if f.name == "" {
finishChan <- child.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
} else {
finishChan <- child.ExtractWithOptions(path+"/"+f.name, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
}
}(child)
}
for range children {
errs = append(errs, (<-finishChan)...)
}
return
case f.IsFile():
fil, err := os.Create(path + "/" + f.name)
if os.IsExist(err) {
err = os.Remove(path + "/" + f.name)
if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
fil, err = os.Create(path + "/" + f.name)
if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
} else if err != nil {
if verbose {
fmt.Println("Error while making:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
} //Since we will be reading from the file
_, err = io.Copy(fil, f)
if err != nil {
if verbose {
fmt.Println("Error while Copying data to:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
return
}
f.Close()
fil.Chown(int(f.r.idTable[f.in.Header.UID]), int(f.r.idTable[f.in.Header.GID]))
//don't mention anything when it fails. Because it fails often. Probably has something to do about uid & gid 0
// if err != nil {
// if verbose {
// fmt.Println("Error while changing owner:", path+"/"+f.Name)
// fmt.Println(err)
// }
// errs = append(errs, err)
// return
// }
err = fil.Chmod(f.Mode())
if err != nil {
if verbose {
fmt.Println("Error while setting permissions for:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
}
return
case f.IsSymlink():
symPath := f.SymlinkPath()
if dereferenceSymlink {
fil := f.GetSymlinkFile()
if fil == nil {
if verbose {
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.name)
}
return
}
fil.name = f.name
extracSymErrs := fil.ExtractWithOptions(path, dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
if len(extracSymErrs) > 0 {
if verbose {
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.name)
fmt.Println(extracSymErrs)
}
errs = append(errs, extracSymErrs...)
}
return
} else if unbreakSymlink {
fil := f.GetSymlinkFile()
if fil != nil {
symPath = path + "/" + symPath
paths := strings.Split(symPath, "/")
extracSymErrs := fil.ExtractWithOptions(strings.Join(paths[:len(paths)-1], "/"), dereferenceSymlink, unbreakSymlink, folderPerm, verbose)
if len(extracSymErrs) > 0 {
if verbose {
fmt.Println("Error(s) while extracting the symlink's file:", path+"/"+f.name)
fmt.Println(extracSymErrs)
}
errs = append(errs, extracSymErrs...)
}
} else {
if verbose {
fmt.Println("Symlink path(", symPath, ") is outside the archive:"+path+"/"+f.name)
}
return
}
}
err = os.Symlink(f.SymlinkPath(), path+"/"+f.name)
if err != nil {
if verbose {
fmt.Println("Error while making symlink:", path+"/"+f.name)
fmt.Println(err)
}
errs = append(errs, err)
}
}
return
}
//Close frees up the memory held up by the underlying reader. Should NOT be called when writing.
//When reading, Close is safe to use, but any subsequent Read calls resets to the beginning of the file.
// Closes the underlying readers.
// Further calls to Read and WriteTo will re-create the readers.
// Never returns an error.
func (f *File) Close() error {
if f.IsDir() {
return errNotFile
}
if f.Reader != nil {
if closer, is := f.Reader.(io.Closer); is {
closer.Close()
}
f.Reader = nil
}
f.rdr.Close()
f.full.Close()
return nil
}
//Read from the file. Doesn't do anything fancy, just pases it to the underlying io.Reader. If a directory, return io.EOF.
func (f *File) Read(p []byte) (int, error) {
if f.IsDir() {
return 0, io.EOF
// Returns the file the symlink points to.
// If the file isn't a symlink, or points to a file outside the archive, returns nil.
func (f File) GetSymlinkFile() fs.File {
if !f.IsSymlink() {
return nil
}
var err error
if f.Reader == nil && f.r != nil {
f.Reader, err = f.r.newFileReader(f.in)
if filepath.IsAbs(f.SymlinkPath()) {
return nil
}
fil, err := f.parent.Open(f.SymlinkPath())
if err != nil {
return nil
}
return fil
}
// Returns whether the file is a directory.
func (f File) IsDir() bool {
return f.Low.IsDir()
}
// Returns whether the file is a regular file.
func (f File) IsRegular() bool {
return f.Low.IsRegular()
}
// Returns whether the file is a symlink.
func (f File) IsSymlink() bool {
return f.Low.Inode.Type == inode.Sym || f.Low.Inode.Type == inode.ESym
}
func (f File) Mode() fs.FileMode {
return f.Low.Inode.Mode()
}
// Read reads the data from the file. Only works if file is a normal file.
func (f *File) Read(b []byte) (int, error) {
if !f.IsRegular() {
return 0, errors.New("file is not a regular file")
}
if !f.rdrInit {
err := f.initializeReaders()
if err != nil {
return 0, err
}
}
return f.Reader.Read(p)
return f.rdr.Read(b)
}
//ReadDirFromInode returns a fully populated Directory from a given Inode.
//If the given inode is not a directory it returns an error.
func (r *Reader) readDirFromInode(i *inode.Inode) (*directory.Directory, error) {
var offset uint32
var metaOffset uint16
var size uint32
switch i.Type {
case inode.BasicDirectoryType:
offset = i.Info.(inode.BasicDirectory).DirectoryIndex
metaOffset = i.Info.(inode.BasicDirectory).DirectoryOffset
size = uint32(i.Info.(inode.BasicDirectory).DirectorySize)
case inode.ExtDirType:
offset = i.Info.(inode.ExtendedDirectory).Init.DirectoryIndex
metaOffset = i.Info.(inode.ExtendedDirectory).Init.DirectoryOffset
size = i.Info.(inode.ExtendedDirectory).Init.DirectorySize
// ReadDir returns n fs.DirEntry's that's contained in the File (if it's a directory).
// If n <= 0 all fs.DirEntry's are returned.
func (f *File) ReadDir(n int) ([]fs.DirEntry, error) {
if !f.IsDir() {
return nil, errors.New("file is not a directory")
}
d, err := f.Low.ToDir(f.r.Low)
if err != nil {
return nil, err
}
start, end := 0, len(d.Entries)
if n > 0 {
start, end = f.dirsRead, f.dirsRead+n
if end > len(d.Entries) {
end = len(d.Entries)
err = io.EOF
}
}
var out []fs.DirEntry
var fi FileInfo
for _, e := range d.Entries[start:end] {
fi, err = f.r.newFileInfo(e)
if err != nil {
f.dirsRead += len(out)
return out, err
}
out = append(out, fs.FileInfoToDirEntry(fi))
}
f.dirsRead += len(out)
return out, err
}
// Returns the file's fs.FileInfo
func (f File) Stat() (fs.FileInfo, error) {
uid, err := f.Low.Uid(&f.r.Low)
if err != nil {
return nil, err
}
gid, err := f.Low.Gid(&f.r.Low)
if err != nil {
return nil, err
}
return newFileInfo(f.Low.Name, uid, gid, &f.Low.Inode), nil
}
// SymlinkPath returns the symlink's target path. Is the File isn't a symlink, returns an empty string.
func (f File) SymlinkPath() string {
switch f.Low.Inode.Type {
case inode.Sym:
return string(f.Low.Inode.Data.(inode.Symlink).Target)
case inode.ESym:
return string(f.Low.Inode.Data.(inode.ESymlink).Target)
}
return ""
}
// Writes all data from the file to the given writer in a multi-threaded manner.
// The underlying reader is separate
func (f *File) WriteTo(w io.Writer) (int64, error) {
if !f.IsRegular() {
return 0, errors.New("file is not a regular file")
}
if !f.rdrInit {
err := f.initializeReaders()
if err != nil {
return 0, err
}
}
return f.full.WriteTo(w)
}
func (f *File) initializeReaders() error {
var err error
f.rdr, f.full, err = f.Low.GetRegFileReaders(f.r.Low)
if err == nil {
f.rdrInit = true
} else {
f.rdr.Close()
f.full.Close()
}
return err
}
func (f File) deviceDevices() (maj uint32, min uint32) {
var dev uint32
switch f.Low.Inode.Type {
case inode.Char, inode.Block:
dev = f.Low.Inode.Data.(inode.Device).Dev
case inode.EChar, inode.EBlock:
dev = f.Low.Inode.Data.(inode.EDevice).Dev
}
return dev >> 8, dev & 0x000FF
}
func (f File) path() string {
if f.parent.LowDir.Name == "" {
return f.Low.Name
}
return filepath.Join(f.parent.path(), f.Low.Name)
}
// Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder.
// Uses default extraction options.
func (f File) Extract(folder string) error {
return f.ExtractWithOptions(folder, DefaultOptions())
}
// Extract the file to the given folder. If the file is a folder, the folder's contents will be extracted to the folder.
// Allows setting various extraction options via ExtractionOptions.
func (f File) ExtractWithOptions(path string, op *ExtractionOptions) error {
if op.manager == nil {
op.manager = routinemanager.NewManager(op.SimultaneousFiles)
if op.LogOutput != nil {
log.SetOutput(op.LogOutput)
}
err := os.MkdirAll(path, 0777)
if err != nil {
if op.Verbose {
log.Println("Failed to create initial directory", path)
}
return err
}
}
switch f.Low.Inode.Type {
case inode.Dir, inode.EDir:
d, err := f.Low.ToDir(f.r.Low)
if err != nil {
if op.Verbose {
log.Println("Failed to create squashfs.Directory for", path)
}
return errors.Join(errors.New("failed to create squashfs.Directory: "+path), err)
}
errChan := make(chan error, len(d.Entries))
for i := range d.Entries {
b, err := f.r.Low.BaseFromEntry(d.Entries[i])
if err != nil {
if op.Verbose {
log.Println("Failed to get squashfs.Base from entry for", path)
}
return errors.Join(errors.New("failed to get base from entry: "+path), err)
}
go func(b squashfslow.FileBase, path string) {
i := op.manager.Lock()
if b.IsDir() {
extDir := filepath.Join(path, b.Name)
err = os.Mkdir(extDir, 0777)
op.manager.Unlock(i)
if err != nil {
if op.Verbose {
log.Println("Failed to create directory", path)
}
errChan <- errors.Join(errors.New("failed to create directory: "+path), err)
return
}
err = f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent)).ExtractWithOptions(extDir, op)
if err != nil {
if op.Verbose {
log.Println("Failed to extract directory", path)
}
errChan <- errors.Join(errors.New("failed to extract directory: "+path), err)
return
}
errChan <- nil
} else {
fil := f.r.FileFromBase(b, f.r.FSFromDirectory(d, f.parent))
err = fil.ExtractWithOptions(path, op)
op.manager.Unlock(i)
fil.Close()
errChan <- err
}
}(b, path)
}
var errCache []error
for range d.Entries {
err := <-errChan
if err != nil {
errCache = append(errCache, err)
}
}
if len(errCache) > 0 {
return errors.Join(errors.New("failed to extract folder: "+path), errors.Join(errCache...))
}
case inode.Fil, inode.EFil:
path = filepath.Join(path, f.Low.Name)
outFil, err := os.Create(path)
if err != nil {
if op.Verbose {
log.Println("Failed to create file", path)
}
return errors.Join(errors.New("failed to create file: "+path), err)
}
defer outFil.Close()
full, err := f.Low.GetFullReader(&f.r.Low)
if err != nil {
if op.Verbose {
log.Println("Failed to create full reader for", path)
}
return errors.Join(errors.New("failed to create full reader: "+path), err)
}
full.SetGoroutineLimit(op.ExtractionRoutines)
_, err = full.WriteTo(outFil)
if err != nil {
if op.Verbose {
log.Println("Failed to write file", path)
}
return errors.Join(errors.New("failed to write file: "+path), err)
}
case inode.Sym, inode.ESym:
symPath := f.SymlinkPath()
if op.DereferenceSymlink {
filTmp := f.GetSymlinkFile()
if filTmp == nil {
if op.Verbose {
log.Println("Failed to get symlink's file:", f.path())
}
return errors.New("failed to get symlink's file")
}
fil := filTmp.(*File)
fil.Low.Name = f.Low.Name
err := fil.ExtractWithOptions(path, op)
if err != nil {
if op.Verbose {
log.Println("Failed to extract symlink's file:", filepath.Join(path, f.Low.Name))
}
return errors.Join(errors.New("failed to extract symlink's file: "+path), err)
}
} else {
if op.UnbreakSymlink {
filTmp := f.GetSymlinkFile()
if filTmp == nil {
if op.Verbose {
log.Println("Failed to get symlink's file:", f.path())
}
return errors.New("failed to get symlink's file")
}
extractLoc := filepath.Join(path, filepath.Dir(symPath))
fil := filTmp.(*File)
err := fil.ExtractWithOptions(extractLoc, op)
if err != nil {
if op.Verbose {
log.Println("Error while extracting", fil.path(), "to make sure symlink at", f.path(), "is unbroken")
}
return errors.Join(errors.New("failed to extract symlink's file: "+extractLoc), err)
}
}
path = filepath.Join(path, f.Low.Name)
err := os.Symlink(f.SymlinkPath(), path)
if err != nil {
if op.Verbose {
log.Println("Failed to create symlink:", path)
}
return errors.Join(errors.New("failed to create symlink: "+path), err)
}
}
case inode.Char, inode.EChar, inode.Block, inode.EBlock, inode.Fifo, inode.EFifo:
if runtime.GOOS == "windows" {
if op.Verbose {
log.Println(f.path(), "ignored. A device link and can't be created on Windows.")
}
return nil
}
_, err := exec.LookPath("mknod")
if err != nil {
if op.Verbose {
log.Println("mknot command not found, cannot create device link for", f.path())
}
return errors.Join(errors.New("mknot command not found"), err)
}
path = filepath.Join(path, f.Low.Name)
var typ string
switch f.Low.Inode.Type {
case inode.Char, inode.EChar:
typ = "c"
case inode.Block, inode.EBlock:
typ = "b"
default: //Fifo IPC
if runtime.GOOS == "darwin" {
if op.Verbose {
log.Println(f.path(), "ignored. A Fifo file and can't be created on Darwin.")
}
return nil
}
typ = "p"
}
cmd := exec.Command("mknod", path, typ)
if typ != "p" {
maj, min := f.deviceDevices()
cmd.Args = append(cmd.Args, strconv.Itoa(int(maj)), strconv.Itoa(int(min)))
}
if op.Verbose {
cmd.Stdout = op.LogOutput
cmd.Stderr = op.LogOutput
}
err = cmd.Run()
if err != nil {
if op.Verbose {
log.Println("Error while running mknod for", path)
}
return errors.Join(errors.New("error while running mknod for "+path), err)
}
case inode.Sock, inode.ESock:
if op.Verbose {
log.Println(f.path(), "ignored since it's a socket file.")
}
return nil
default:
return nil, errors.New("Not a directory inode")
return errors.New("Unsupported file type. Inode type: " + strconv.Itoa(int(f.Low.Inode.Type)))
}
br, err := r.newMetadataReader(int64(r.super.DirTableStart + uint64(offset)))
if op.Verbose {
log.Println(f.path(), "extracted to", path)
}
if op.IgnorePerm {
return nil
}
uid, err := f.Low.Uid(&f.r.Low)
if err != nil {
return nil, err
if op.Verbose {
log.Println("Failed to get uid for", path)
log.Println(err)
}
return nil
}
_, err = br.Seek(int64(metaOffset), io.SeekStart)
gid, err := f.Low.Gid(&f.r.Low)
if err != nil {
return nil, err
if op.Verbose {
log.Println("Failed to get gid for", path)
log.Println(err)
}
return nil
}
dir, err := directory.NewDirectory(br, size)
if err != nil {
return dir, err
}
return dir, nil
}
//GetInodeFromEntry returns the inode associated with a given directory.Entry
func (r *Reader) getInodeFromEntry(en *directory.Entry) (*inode.Inode, error) {
br, err := r.newMetadataReader(int64(r.super.InodeTableStart + uint64(en.Header.InodeOffset)))
if err != nil {
return nil, err
}
_, err = br.Seek(int64(en.Init.Offset), io.SeekStart)
if err != nil {
return nil, err
}
i, err := inode.ProcessInode(br, r.super.BlockSize)
if err != nil {
return nil, err
}
return i, nil
os.Chmod(path, f.Mode())
os.Chown(path, int(uid), int(gid))
return nil
}
+126
View File
@@ -0,0 +1,126 @@
package squashfs
import (
"io/fs"
"time"
"github.com/CalebQ42/squashfs/low/directory"
"github.com/CalebQ42/squashfs/low/inode"
)
type FileInfo struct {
name string
uid uint32
gid uint32
size int64
target string
perm uint32
modTime uint32
fileType uint16
}
func (r Reader) newFileInfo(e directory.Entry) (FileInfo, error) {
b, err := r.Low.BaseFromEntry(e)
if err != nil {
return FileInfo{}, err
}
uid, err := b.Uid(&r.Low)
if err != nil {
return FileInfo{}, err
}
gid, err := b.Gid(&r.Low)
if err != nil {
return FileInfo{}, err
}
return newFileInfo(e.Name, uid, gid, &b.Inode), nil
}
func newFileInfo(name string, uid, gid uint32, i *inode.Inode) FileInfo {
var size int64
var target string
switch i.Type {
case inode.Fil:
size = int64(i.Data.(inode.File).Size)
case inode.EFil:
size = int64(i.Data.(inode.EFile).Size)
case inode.Sym:
target = string(i.Data.(inode.Symlink).Target)
case inode.ESym:
target = string(i.Data.(inode.ESymlink).Target)
}
return FileInfo{
name: name,
uid: uid,
gid: gid,
size: size,
target: target,
perm: uint32(i.Perm),
modTime: i.ModTime,
fileType: i.Type,
}
}
func (f FileInfo) Name() string {
return f.name
}
func (f FileInfo) Uid() int {
return int(f.uid)
}
func (f FileInfo) Gid() int {
return int(f.gid)
}
func (f FileInfo) Size() int64 {
return f.size
}
func (f FileInfo) SymlinkPath() string {
return f.target
}
func (f FileInfo) Mode() fs.FileMode {
switch f.fileType {
case inode.Dir, inode.EDir:
return fs.FileMode(f.perm | uint32(fs.ModeDir))
case inode.Sym, inode.ESym:
return fs.FileMode(f.perm | uint32(fs.ModeSymlink))
case inode.Char, inode.EChar, inode.Block, inode.EBlock:
return fs.FileMode(f.perm | uint32(fs.ModeDevice))
case inode.Fifo, inode.EFifo:
return fs.FileMode(f.perm | uint32(fs.ModeNamedPipe))
case inode.Sock, inode.ESock:
return fs.FileMode(f.perm | uint32(fs.ModeSocket))
}
return fs.FileMode(f.perm)
}
func (f FileInfo) ModTime() time.Time {
return time.Unix(int64(f.modTime), 0)
}
func (f FileInfo) IsDir() bool {
return f.fileType == inode.Dir || f.fileType == inode.EDir
}
func (f FileInfo) IsSymlink() bool {
return f.fileType == inode.Sym || f.fileType == inode.ESym
}
func (f FileInfo) IsDevice() bool {
return f.fileType == inode.Block || f.fileType == inode.EBlock ||
f.fileType == inode.Char || f.fileType == inode.EChar
}
func (f FileInfo) IsFifo() bool {
return f.fileType == inode.Fifo || f.fileType == inode.EFifo
}
func (f FileInfo) IsSocket() bool {
return f.fileType == inode.Sock || f.fileType == inode.ESock
}
func (f FileInfo) Sys() any {
return nil
}
-94
View File
@@ -1,94 +0,0 @@
package squashfs
import (
"bytes"
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/inode"
)
//FileReader provides a io.Reader interface for files within a squashfs archive
type fileReader struct {
r *Reader
data *dataReader
in *inode.Inode
fragmentData []byte
fragged bool
fragOnly bool
read int
FileSize int //FileSize is the total size of the given file
}
var (
//ErrPathIsNotFile returns when trying to read from a file, but the given path is NOT a file.
errPathIsNotFile = errors.New("The given path is not a file")
)
//ReadFile provides a squashfs.FileReader for the file at the given location.
func (r *Reader) newFileReader(in *inode.Inode) (*fileReader, error) {
var rdr fileReader
rdr.in = in
if in.Type != inode.BasicFileType && in.Type != inode.ExtFileType {
return nil, errPathIsNotFile
}
switch in.Type {
case inode.BasicFileType:
fil := in.Info.(inode.BasicFile)
rdr.fragged = fil.Fragmented
rdr.fragOnly = fil.Init.BlockStart == 0
rdr.FileSize = int(fil.Init.Size)
case inode.ExtFileType:
fil := in.Info.(inode.ExtendedFile)
rdr.fragged = fil.Fragmented
rdr.fragOnly = fil.Init.BlockStart == 0
rdr.FileSize = int(fil.Init.Size)
}
var err error
if rdr.fragged {
rdr.fragmentData, err = r.getFragmentDataFromInode(in)
if err != nil {
return nil, err
}
}
if !rdr.fragOnly {
rdr.data, err = r.newDataReaderFromInode(in)
}
return &rdr, nil
}
//Close runs Close on the data reader and frees the fragmentdata
func (f *fileReader) Close() error {
if f.data != nil {
f.data.Close()
}
f.fragmentData = nil
return nil
}
func (f *fileReader) Read(p []byte) (int, error) {
if f.fragOnly {
n, err := bytes.NewBuffer(f.fragmentData[f.read:]).Read(p)
f.read += n
if err != nil {
return n, err
}
return n, nil
}
var read int
n, err := f.data.Read(p)
read += n
if f.fragged && err == io.EOF {
if f.fragmentData == nil {
f.fragmentData, err = f.r.getFragmentDataFromInode(f.in)
}
n, err = bytes.NewBuffer(f.fragmentData).Read(p[read:])
read += n
if err != nil {
return read, err
}
} else if err != nil {
return read, err
}
return read, nil
}
-80
View File
@@ -1,80 +0,0 @@
package squashfs
import (
"encoding/binary"
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/inode"
)
//FragmentEntry is an entry in the fragment table
type fragmentEntry struct {
Start uint64
Size uint32
Unused uint32
}
//GetFragmentDataFromInode returns the fragment data for a given inode.
//If the inode does not have a fragment, harmlessly returns an empty slice without an error.
func (r *Reader) getFragmentDataFromInode(in *inode.Inode) ([]byte, error) {
var size uint32
var fragIndex uint32
var fragOffset uint32
if in.Type == inode.BasicFileType {
bf := in.Info.(inode.BasicFile)
if !bf.Fragmented {
return make([]byte, 0), nil
}
if bf.Init.BlockStart == 0 {
size = bf.Init.Size
} else {
size = bf.BlockSizes[len(bf.BlockSizes)-1]
}
fragIndex = bf.Init.FragmentIndex
fragOffset = bf.Init.FragmentOffset
} else if in.Type == inode.ExtFileType {
bf := in.Info.(inode.ExtendedFile)
if !bf.Fragmented {
return make([]byte, 0), nil
}
if bf.Init.BlockStart == 0 {
size = bf.Init.Size
} else {
size = bf.BlockSizes[len(bf.BlockSizes)-1]
}
fragIndex = bf.Init.FragmentIndex
fragOffset = bf.Init.FragmentOffset
} else {
return nil, errors.New("Inode type not supported")
}
//reading the fragment entry first
fragEntryRdr, err := r.newMetadataReader(int64(r.fragOffsets[int(fragIndex/512)]))
if err != nil {
return nil, err
}
_, err = fragEntryRdr.Seek(int64(16*fragIndex), io.SeekStart)
if err != nil {
return nil, err
}
var entry fragmentEntry
err = binary.Read(fragEntryRdr, binary.LittleEndian, &entry)
if err != nil {
return nil, err
}
//now reading the actual fragment
dr, err := r.newDataReader(int64(entry.Start), []uint32{entry.Size})
if err != nil {
return nil, err
}
_, err = dr.Read(make([]byte, fragOffset))
if err != nil {
return nil, err
}
tmp := make([]byte, size)
err = binary.Read(dr, binary.LittleEndian, &tmp)
if err != nil {
return nil, err
}
return tmp, nil
}
+279
View File
@@ -0,0 +1,279 @@
package squashfs
import (
"io"
"io/fs"
"path"
"path/filepath"
"slices"
"strings"
squashfslow "github.com/CalebQ42/squashfs/low"
"github.com/CalebQ42/squashfs/low/directory"
)
// FS is a fs.FS representation of a squashfs directory.
// Implements fs.GlobFS, fs.ReadDirFS, fs.ReadFileFS, fs.StatFS, and fs.SubFS
type FS struct {
r *Reader
parent *FS
LowDir squashfslow.Directory
}
// Creates a new *FS from the given squashfs.directory
func (r *Reader) FSFromDirectory(d squashfslow.Directory, parent FS) FS {
return FS{
LowDir: d,
r: r,
parent: &parent,
}
}
// Glob returns the name of the files at the given pattern.
// All paths are relative to the FS.
// Uses filepath.Match to compare names.
func (f *FS) Glob(pattern string) (out []string, err error) {
pattern = filepath.Clean(pattern)
if !fs.ValidPath(pattern) {
return nil, &fs.PathError{
Op: "glob",
Path: pattern,
Err: fs.ErrInvalid,
}
}
split := strings.Split(pattern, "/")
for i := range f.LowDir.Entries {
if match, _ := path.Match(split[0], f.LowDir.Entries[i].Name); match {
if len(split) == 1 {
out = append(out, f.LowDir.Entries[i].Name)
continue
}
sub, err := f.Sub(split[0])
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "glob"
pathErr.Path = pattern
return nil, pathErr
}
return nil, &fs.PathError{
Op: "glob",
Path: pattern,
Err: err,
}
}
subGlob, err := sub.(fs.GlobFS).Glob(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
continue
}
pathErr.Op = "glob"
pathErr.Path = pattern
return nil, pathErr
}
return nil, &fs.PathError{
Op: "glob",
Path: pattern,
Err: err,
}
}
for i := range subGlob {
subGlob[i] = f.LowDir.Name + "/" + subGlob[i]
}
out = append(out, subGlob...)
}
}
return
}
// Opens the file at name. Returns a *File as an fs.File.
func (f FS) Open(name string) (fs.File, error) {
return f.OpenFile(name)
}
func (f FS) OpenFile(name string) (*File, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}
if name == "." || name == "" {
return f.File(), nil
}
split := strings.Split(name, "/")
if split[0] == ".." {
if f.parent == nil { // root directory
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
} else {
return f.parent.OpenFile(strings.Join(split[1:], "/"))
}
}
i, found := slices.BinarySearchFunc(f.LowDir.Entries, split[0], func(e directory.Entry, name string) int {
return strings.Compare(e.Name, name)
})
if !found {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}
b, err := f.r.Low.BaseFromEntry(f.LowDir.Entries[i])
if err != nil {
return nil, err
}
if len(split) == 1 {
return &File{
Low: b,
r: f.r,
parent: f,
}, nil
}
if !b.IsDir() {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}
d, err := b.ToDir(f.r.Low)
if err != nil {
return nil, err
}
return f.r.FSFromDirectory(d, f).OpenFile(strings.Join(split[1:], "/"))
}
// Returns all DirEntry's for the directory at name.
// If name is not a directory, returns an error.
func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrInvalid,
}
}
if name == "." || name == "" {
return f.File().ReadDir(-1)
}
fil, err := f.Open(name)
if err != nil {
return nil, err
}
return fil.(*File).ReadDir(-1)
}
// Returns the contents of the file at name.
func (f FS) ReadFile(name string) (out []byte, err error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "readfile",
Path: name,
Err: fs.ErrInvalid,
}
}
if name == "." || name == "" {
return nil, fs.ErrInvalid
}
fil, err := f.Open(name)
if err != nil {
return nil, err
}
if !fil.(*File).IsRegular() {
return nil, fs.ErrInvalid
}
return io.ReadAll(fil)
}
// Returns the fs.FileInfo for the file at name.
func (f FS) Stat(name string) (fs.FileInfo, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: fs.ErrInvalid,
}
}
if name == "." || name == "" {
return f.File().Stat()
}
fil, err := f.Open(name)
if err != nil {
return nil, err
}
return fil.(*File).Stat()
}
// Returns the FS at dir
func (f FS) Sub(dir string) (fs.FS, error) {
dir = filepath.Clean(dir)
if !fs.ValidPath(dir) {
return nil, &fs.PathError{
Op: "dir",
Path: dir,
Err: fs.ErrInvalid,
}
}
if dir == "." || dir == "" {
return f, nil
}
fil, err := f.Open(dir)
if err != nil {
return nil, err
}
if !fil.(*File).IsDir() {
return nil, &fs.PathError{
Op: "dir",
Path: dir,
Err: fs.ErrInvalid,
}
}
return fil.(*File).FS()
}
// Extract the FS to the given folder. If the file is a folder, the folder's contents will be extracted to the folder.
// Uses default extraction options.
func (f FS) Extract(folder string) error {
return f.File().Extract(folder)
}
// Extract the FS to the given folder. If the file is a folder, the folder's contents will be extracted to the folder.
// Allows setting various extraction options via ExtractionOptions.
func (f FS) ExtractWithOptions(folder string, op *ExtractionOptions) error {
return f.File().ExtractWithOptions(folder, op)
}
// Returns the FS as a *File
func (f FS) File() *File {
if f.parent != nil {
return &File{
Low: f.LowDir.FileBase,
parent: *f.parent,
r: f.r,
}
}
return &File{
Low: f.LowDir.FileBase,
r: f.r,
}
}
func (f FS) path() string {
if f.parent == nil {
return f.LowDir.Name
}
return filepath.Join(f.parent.path(), f.LowDir.Name)
}
+6 -13
View File
@@ -1,18 +1,11 @@
module github.com/CalebQ42/squashfs
go 1.15
go 1.24.0
require (
github.com/CalebQ42/GoAppImage v0.4.0
github.com/adrg/xdg v0.2.3 // indirect
github.com/google/go-cmp v0.5.4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/klauspost/compress v1.11.3
github.com/kr/text v0.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.1
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/ulikunitz/xz v0.5.8
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
github.com/klauspost/compress v1.18.0
github.com/pierrec/lz4/v4 v4.1.22
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e
github.com/therootcompany/xz v1.0.1
github.com/ulikunitz/xz v0.5.12
)
+10 -73
View File
@@ -1,73 +1,10 @@
github.com/CalebQ42/GoAppImage v0.4.0 h1:aF+Y/vyo/RGhoyZEW1CMY6WyRWrZZO4ydsRFAtIGnaY=
github.com/CalebQ42/GoAppImage v0.4.0/go.mod h1:qHudJKAn/dlkNWNnH4h1YKXp29EZ7Bppsn7sNP2HuvU=
github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo=
github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/adrg/xdg v0.2.3 h1:GxXngdYxNDkoUvZXjNJGwqZxWXi43MKbOOlA/00qZi4=
github.com/adrg/xdg v0.2.3/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc=
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pierrec/lz4/v4 v4.1.1 h1:cS6aGkNLJr4u+UwaA21yp+gbWN3WJWtKo1axmPDObMA=
github.com/pierrec/lz4/v4 v4.1.1/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-66
View File
@@ -1,66 +0,0 @@
package compression
import (
"bytes"
"compress/zlib"
"encoding/binary"
"io"
)
type gzipInit struct {
CompressionLevel int32
WindowSize int16
Strategies int16
}
//Gzip is a decompressor for gzip type compression. Uses zlib for compression and decompression
type Gzip struct {
CompressionLevel int32
HasCustomWindow bool
HasStrategies bool
}
//NewGzipCompressorWithOptions creates a new gzip compressor/decompressor with options read from the given reader.
func NewGzipCompressorWithOptions(r io.Reader) (*Gzip, error) {
var gzip Gzip
var init gzipInit
err := binary.Read(r, binary.LittleEndian, &init)
if err != nil {
return nil, err
}
gzip.CompressionLevel = init.CompressionLevel
//TODO: proper support for window size and strategies
gzip.HasCustomWindow = init.WindowSize != 15
gzip.HasStrategies = init.Strategies != 0 && init.Strategies != 1
return &gzip, nil
}
//Decompress reads the entirety of the given reader and returns it uncompressed as a byte slice.
func (g *Gzip) Decompress(r io.Reader) ([]byte, error) {
rdr, err := zlib.NewReader(r)
if err != nil {
return nil, err
}
var data bytes.Buffer
_, err = io.Copy(&data, rdr)
if err != nil {
return nil, err
}
return data.Bytes(), nil
}
//Compress compresses the given data (as a byte array) and returns the compressed data.
func (g *Gzip) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
wrt := zlib.NewWriter(&buf)
defer wrt.Close()
_, err := wrt.Write(data)
if err != nil {
return nil, err
}
err = wrt.Flush()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
-37
View File
@@ -1,37 +0,0 @@
package compression
import (
"bytes"
"encoding/binary"
"io"
"github.com/pierrec/lz4/v4"
)
//Lz4 is a Lz4 Compressor/Decompressor
type Lz4 struct {
HC bool
}
//NewLz4CompressorWithOptions creates a new lz4 compressor/decompressor with options read from the given reader.
func NewLz4CompressorWithOptions(r io.Reader) (*Lz4, error) {
var lz4 Lz4
var init struct {
Version int32
Flags int32
}
err := binary.Read(r, binary.LittleEndian, &init)
if err != nil {
return nil, err
}
lz4.HC = init.Flags == 1
return &lz4, nil
}
//Decompress decompresses all data from r and returns the uncompressed bytes
func (l *Lz4) Decompress(r io.Reader) ([]byte, error) {
rdr := lz4.NewReader(r)
var buf bytes.Buffer
_, err := io.Copy(&buf, rdr)
return buf.Bytes(), err
}
-25
View File
@@ -1,25 +0,0 @@
package compression
import (
"bytes"
"io"
"github.com/ulikunitz/xz/lzma"
)
//Lzma is a lzma decompressor
type Lzma struct{}
//Decompress decompresses all the data in the given reader and returns the uncompressed bytes.
func (l *Lzma) Decompress(rdr io.Reader) ([]byte, error) {
r, err := lzma.NewReader(rdr)
if err != nil {
return nil, err
}
var buf bytes.Buffer
_, err = io.Copy(&buf, r)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
-13
View File
@@ -1,13 +0,0 @@
package compression
import "io"
//Compressor is a squashfs decompressor interface. Allows for easy compression.
type Compressor interface {
Compress([]byte) ([]byte, error)
}
//Decompressor is a squashfs decompressor interface. Allows for easy decompression no matter the type of compression.
type Decompressor interface {
Decompress(io.Reader) ([]byte, error)
}
-55
View File
@@ -1,55 +0,0 @@
package compression
import (
"bytes"
"encoding/binary"
"io"
"github.com/ulikunitz/xz"
)
type xzInit struct {
DictionarySize int32
Filters int32
}
//Xz is a Xz decompressor.
type Xz struct {
DictionarySize int32
HasFilters bool
}
//NewXzCompressorWithOptions creates a new Xz compressor/decompressor that reads the compressor options from the given reader.
func NewXzCompressorWithOptions(rdr io.Reader) (*Xz, error) {
var x Xz
var init xzInit
err := binary.Read(rdr, binary.LittleEndian, &init)
if err != nil {
return nil, err
}
x.DictionarySize = init.DictionarySize
//TODO: When I can do filters, parse the filters
if init.Filters != 0 {
x.HasFilters = true
}
return &x, nil
}
//Decompress decompresses all the data from the rdr and returns the uncompressed bytes.
func (x *Xz) Decompress(rdr io.Reader) ([]byte, error) {
r, err := xz.NewReader(rdr)
if err != nil {
return nil, err
}
r.DictCap = int(x.DictionarySize)
err = r.Verify()
if err != nil {
return nil, err
}
var buf bytes.Buffer
_, err = io.Copy(&buf, r)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
-36
View File
@@ -1,36 +0,0 @@
package compression
import (
"bytes"
"encoding/binary"
"io"
"github.com/klauspost/compress/zstd"
)
//Zstd is a zstd compressor/decompressor
type Zstd struct {
CompressionLevel int32
}
//NewZstdCompressorWithOptions creates a new Zstd with options read from the given reader
func NewZstdCompressorWithOptions(r io.Reader) (*Zstd, error) {
var zstd Zstd
err := binary.Read(r, binary.LittleEndian, &zstd)
if err != nil {
return nil, err
}
return &zstd, nil
}
//Decompress decompresses all data from the reader and returns the uncompressed data
func (z *Zstd) Decompress(r io.Reader) ([]byte, error) {
rdr, err := zstd.NewReader(r)
if err != nil {
return nil, err
}
defer rdr.Close()
var buf bytes.Buffer
_, err = io.Copy(&buf, rdr)
return buf.Bytes(), err
}
+5
View File
@@ -0,0 +1,5 @@
package decompress
type Decompressor interface {
Decompress([]byte) ([]byte, error)
}
+30
View File
@@ -0,0 +1,30 @@
package decompress
import (
"bytes"
"io"
"sync"
"github.com/pierrec/lz4/v4"
)
type Lz4 struct {
pool sync.Pool
}
func NewLz4() *Lz4 {
return &Lz4{
pool: sync.Pool{
New: func() any {
return lz4.NewReader(nil)
},
},
}
}
func (l *Lz4) Decompress(data []byte) ([]byte, error) {
rdr := l.pool.Get().(*lz4.Reader)
defer l.pool.Put(rdr)
rdr.Reset(bytes.NewReader(data))
return io.ReadAll(rdr)
}
+24
View File
@@ -0,0 +1,24 @@
//go:build !no_obsolete
package decompress
import (
"bytes"
"io"
"github.com/ulikunitz/xz/lzma"
)
type Lzma struct{}
func NewLzma() (Lzma, error) {
return Lzma{}, nil
}
func (l Lzma) Decompress(data []byte) ([]byte, error) {
rdr, err := lzma.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
return io.ReadAll(rdr)
}
+17
View File
@@ -0,0 +1,17 @@
//go:build no_obsolete
package decompress
import (
"errors"
)
type Lzma struct{}
func NewLzma() (Lzma, error) {
return Lzma{}, errors.New("lzma compression is disable in this build with no_obsolete")
}
func (l Lzma) Decompress(data []byte) ([]byte, error) {
return nil, errors.New("lzma compression is disable in this build with no_obsolete")
}
+19
View File
@@ -0,0 +1,19 @@
//go:build !no_gpl
package decompress
import (
"bytes"
"github.com/rasky/go-lzo"
)
type Lzo struct{}
func NewLzo() (Lzo, error) {
return Lzo{}, nil
}
func (l Lzo) Decompress(data []byte) ([]byte, error) {
return lzo.Decompress1X(bytes.NewReader(data), len(data), 0)
}
+15
View File
@@ -0,0 +1,15 @@
//go:build no_gpl
package decompress
import "errors"
type Lzo struct{}
func NewLzo() (Lzo, error) {
return Lzo{}, errors.New("lzo compression is disable in this build with no_gpl")
}
func (l Lzo) Decompress(data []byte) ([]byte, error) {
return nil, errors.New("lzo compression is disable in this build with no_gpl")
}
+34
View File
@@ -0,0 +1,34 @@
package decompress
import (
"bytes"
"io"
"sync"
"github.com/therootcompany/xz"
)
type Xz struct {
pool sync.Pool
}
func NewXz() *Xz {
return &Xz{
pool: sync.Pool{
New: func() any {
rdr, _ := xz.NewReader(nil, 0)
return rdr
},
},
}
}
func (x *Xz) Decompress(data []byte) ([]byte, error) {
rdr := x.pool.Get().(*xz.Reader)
defer x.pool.Put(rdr)
err := rdr.Reset(bytes.NewReader(data))
if err != nil {
return nil, err
}
return io.ReadAll(rdr)
}
+32
View File
@@ -0,0 +1,32 @@
package decompress
import (
"bytes"
"io"
"sync"
"github.com/klauspost/compress/zlib"
)
type Zlib struct {
pool sync.Pool
}
func NewZlib() *Zlib {
return &Zlib{}
}
func (z *Zlib) Decompress(data []byte) ([]byte, error) {
rdr := z.pool.Get()
defer z.pool.Put(rdr)
var err error
if rdr == nil {
rdr, err = zlib.NewReader(bytes.NewReader(data))
} else {
err = rdr.(zlib.Resetter).Reset(bytes.NewReader(data), nil)
}
if err != nil {
return nil, err
}
return io.ReadAll(rdr.(io.ReadCloser))
}
+20
View File
@@ -0,0 +1,20 @@
package decompress
import (
"github.com/klauspost/compress/zstd"
)
type Zstd struct {
rdr *zstd.Decoder
}
func NewZstd() Zstd {
rdr, _ := zstd.NewReader(nil, zstd.WithDecoderLowmem(true))
return Zstd{
rdr: rdr,
}
}
func (z Zstd) Decompress(data []byte) ([]byte, error) {
return z.rdr.DecodeAll(data, nil)
}
-94
View File
@@ -1,94 +0,0 @@
package directory
import (
"bytes"
"encoding/binary"
"io"
)
//Header is the header for a directory in the directory table
type Header struct {
Count uint32
InodeOffset uint32
InodeNumber uint32
}
//EntryInit is the values that can be easily decoded
type EntryInit struct {
Offset uint16
InodeOffset int16
Type uint16
NameSize uint16
}
//Entry is an entry in a directory.
type Entry struct {
Init EntryInit
Name string
Header *Header
}
//NewEntry creates a new directory entry
func NewEntry(rdr io.Reader) (Entry, error) {
var entry Entry
err := binary.Read(rdr, binary.LittleEndian, &entry.Init)
if err != nil {
return Entry{}, err
}
tmp := make([]byte, entry.Init.NameSize+1)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return Entry{}, err
}
entry.Name = string(tmp)
return entry, err
}
//Directory is an entry in the directory table of a squashfs.
//Will only have multiple headers if there are more then 256 entries
type Directory struct {
Headers []Header
Entries []Entry
}
//NewDirectory reads the directory from rdr
func NewDirectory(base io.Reader, size uint32) (*Directory, error) {
var dir Directory
var err error
tmp := make([]byte, size)
base.Read(tmp)
rdr := bytes.NewBuffer(tmp)
for {
var hdr Header
err = binary.Read(rdr, binary.LittleEndian, &hdr)
if err == io.ErrUnexpectedEOF {
break
} else if err != nil {
return nil, err
}
hdr.Count++
headers := hdr.Count / 256
if hdr.Count%256 > 0 {
headers++
}
dir.Headers = append(dir.Headers, hdr)
for i := uint32(0); i < hdr.Count; i++ {
if i != 0 && i%256 == 0 {
var newHdr Header
err = binary.Read(rdr, binary.LittleEndian, &newHdr)
if err != nil {
return nil, err
}
dir.Headers = append(dir.Headers, newHdr)
}
var ent Entry
ent, err = NewEntry(rdr)
if err != nil {
return nil, err
}
ent.Header = &dir.Headers[len(dir.Headers)-1]
dir.Entries = append(dir.Entries, ent)
}
}
return &dir, nil
}
-255
View File
@@ -1,255 +0,0 @@
package inode
import (
"encoding/binary"
"io"
)
const (
BasicDirectoryType = iota + 1
BasicFileType
BasicSymlinkType
BasicBlockDeviceType
BasicCharDeviceType
BasicFifoType
BasicSocketType
ExtDirType
ExtFileType
ExtSymlinkType
ExtBlockDeviceType
ExtCharDeviceType
ExtFifoType
ExtSocketType
)
//Header is the common header for all inodes
type Header struct {
InodeType uint16
Permissions uint16
UID uint16
GID uint16
ModifiedTime uint32
Number uint32
}
//BasicDirectory is self explainatory
type BasicDirectory struct {
DirectoryIndex uint32
HardLinks uint32
DirectorySize uint16
DirectoryOffset uint16
ParentInodeNumber uint32
}
//ExtendedDirectoryInit is the information that can be directoy decoded
type ExtendedDirectoryInit struct {
HardLinks uint32
DirectorySize uint32
DirectoryIndex uint32
ParentInodeNumber uint32
IndexCount uint16 //one less then directory indexes following structure
DirectoryOffset uint16
XattrIndex uint32
}
//ExtendedDirectory is a directory with extra info
type ExtendedDirectory struct {
Init ExtendedDirectoryInit
Indexes []DirectoryIndex
}
//NewExtendedDirectory creates a new ExtendedDirectory
func NewExtendedDirectory(rdr io.Reader) (ExtendedDirectory, error) {
var inode ExtendedDirectory
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
for i := uint16(0); i < inode.Init.IndexCount; i++ {
tmp, err := NewDirectoryIndex(rdr)
if err != nil {
return inode, err
}
inode.Indexes = append(inode.Indexes, tmp)
}
return inode, err
}
//DirectoryIndexInit holds the values that can be easily decoded
type DirectoryIndexInit struct {
Offset uint32
DirTableOffset uint32
NameSize uint32
}
//DirectoryIndex is a quick lookup provided by an ExtendedDirectory
type DirectoryIndex struct {
Init DirectoryIndexInit
Name string
}
//NewDirectoryIndex return a new DirectoryIndex
func NewDirectoryIndex(rdr io.Reader) (DirectoryIndex, error) {
var index DirectoryIndex
err := binary.Read(rdr, binary.LittleEndian, &index.Init)
if err != nil {
return index, err
}
tmp := make([]byte, index.Init.NameSize+1, index.Init.NameSize+1)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return index, err
}
index.Name = string(tmp)
return index, nil
}
//BasicFileInit is the information that can be directoy decoded
type BasicFileInit struct {
BlockStart uint32
FragmentIndex uint32
FragmentOffset uint32
Size uint32
}
//BasicFile is self explainatory
type BasicFile struct {
Init BasicFileInit
BlockSizes []uint32
Fragmented bool
}
//NewBasicFile creates a new BasicFile
func NewBasicFile(rdr io.Reader, blockSize uint32) (BasicFile, error) {
var inode BasicFile
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.Fragmented = inode.Init.FragmentIndex != 0xFFFFFFFF
blocks := inode.Init.Size / blockSize
if inode.Init.Size%blockSize > 0 {
blocks++
}
inode.BlockSizes = make([]uint32, blocks, blocks)
err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes)
return inode, err
}
//ExtendedFileInit is the information that can be directly decoded
type ExtendedFileInit struct {
BlockStart uint32
Size uint32
Sparse uint64
HardLinks uint32
FragmentIndex uint32
FragmentOffset uint32
XattrIndex uint32
}
//ExtendedFile is a file with more information
type ExtendedFile struct {
Init ExtendedFileInit
BlockSizes []uint32
Fragmented bool
}
//NewExtendedFile creates a new ExtendedFile
func NewExtendedFile(rdr io.Reader, blockSize uint32) (ExtendedFile, error) {
var inode ExtendedFile
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.Fragmented = inode.Init.FragmentIndex != 0xFFFFFFFF
blocks := inode.Init.Size / blockSize
if inode.Init.Size%blockSize > 0 {
blocks++
}
inode.BlockSizes = make([]uint32, blocks, blocks)
err = binary.Read(rdr, binary.LittleEndian, &inode.BlockSizes)
return inode, err
}
//BasicSymlinkInit is all the values that can be directly decoded
type BasicSymlinkInit struct {
HardLinks uint32
TargetPathSize uint32
}
//BasicSymlink is a symlink
type BasicSymlink struct {
Init BasicSymlinkInit
targetPath []byte //len is TargetPathSize
Path string
}
//NewBasicSymlink creates a new BasicSymlink
func NewBasicSymlink(rdr io.Reader) (BasicSymlink, error) {
var inode BasicSymlink
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.targetPath = make([]byte, inode.Init.TargetPathSize, inode.Init.TargetPathSize)
err = binary.Read(rdr, binary.LittleEndian, &inode.targetPath)
if err != nil {
return inode, err
}
inode.Path = string(inode.targetPath)
return inode, err
}
//ExtendedSymlinkInit is all the values that can be directly decoded
type ExtendedSymlinkInit struct {
HardLinks uint32
TargetPathSize uint32
}
//ExtendedSymlink is a symlink with extra information
type ExtendedSymlink struct {
Init ExtendedSymlinkInit
targetPath []uint8
Path string
XattrIndex uint32
}
//NewExtendedSymlink creates a new ExtendedSymlink
func NewExtendedSymlink(rdr io.Reader) (ExtendedSymlink, error) {
var inode ExtendedSymlink
err := binary.Read(rdr, binary.LittleEndian, &inode.Init)
if err != nil {
return inode, err
}
inode.targetPath = make([]uint8, inode.Init.TargetPathSize, inode.Init.TargetPathSize)
err = binary.Read(rdr, binary.LittleEndian, &inode.targetPath)
if err != nil {
return inode, err
}
inode.Path = string(inode.targetPath)
err = binary.Read(rdr, binary.LittleEndian, &inode.XattrIndex)
return inode, err
}
//BasicDevice is a device
type BasicDevice struct {
HardLinks uint32
Device uint32
}
//ExtendedDevice is a device with more info
type ExtendedDevice struct {
BasicDevice
XattrIndex uint32
}
//BasicIPC is a Fifo or Socket device
type BasicIPC struct {
HardLink uint32
}
//ExtendedIPC is a IPC device with extra info
type ExtendedIPC struct {
BasicIPC
XattrIndex uint32
}
-125
View File
@@ -1,125 +0,0 @@
package inode
import (
"encoding/binary"
"io"
)
//Inode holds an inode. Header is the header that's common for all inodes.
//
//Info holds the actual Inode. Due to each inode type being a different type, it's store as an interface{}
type Inode struct {
Header Header
Type int //Type the inode type defined in the header. Here so it's easy to access
Info interface{} //Info is the parsed specific data. It's type is defined by Type.
}
//ProcessInode tries to read an inode from the BlockReader
func ProcessInode(br io.Reader, blockSize uint32) (*Inode, error) {
var head Header
err := binary.Read(br, binary.LittleEndian, &head)
if err != nil {
return nil, err
}
var info interface{}
switch head.InodeType {
case BasicDirectoryType:
var inode BasicDirectory
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicFileType:
inode, err := NewBasicFile(br, blockSize)
if err != nil {
return nil, err
}
info = inode
case BasicSymlinkType:
inode, err := NewBasicSymlink(br)
if err != nil {
return nil, err
}
info = inode
case BasicBlockDeviceType:
var inode BasicDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicCharDeviceType:
var inode BasicDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicFifoType:
var inode BasicIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case BasicSocketType:
var inode BasicIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtDirType:
inode, err := NewExtendedDirectory(br)
if err != nil {
return nil, err
}
info = inode
case ExtFileType:
inode, err := NewExtendedFile(br, blockSize)
if err != nil {
return nil, err
}
info = inode
case ExtSymlinkType:
inode, err := NewExtendedSymlink(br)
if err != nil {
return nil, err
}
info = inode
case ExtBlockDeviceType:
var inode ExtendedDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtCharDeviceType:
var inode ExtendedDevice
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtFifoType:
var inode ExtendedIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
case ExtSocketType:
var inode ExtendedIPC
err = binary.Read(br, binary.LittleEndian, &inode)
if err != nil {
return nil, err
}
info = inode
}
return &Inode{
Type: int(head.InodeType),
Header: head,
Info: info,
}, nil
}
+65
View File
@@ -0,0 +1,65 @@
package metadata
import (
"encoding/binary"
"io"
"github.com/CalebQ42/squashfs/internal/decompress"
)
type Reader struct {
r io.Reader
d decompress.Decompressor
dat []byte
curOffset uint16
}
func NewReader(r io.Reader, d decompress.Decompressor) Reader {
return Reader{
r: r,
d: d,
}
}
func (r *Reader) advance() error {
r.curOffset = 0
dat := make([]byte, 2)
_, err := r.r.Read(dat)
if err != nil {
return err
}
size := binary.LittleEndian.Uint16(dat)
realSize := size &^ 0x8000
r.dat = make([]byte, realSize)
_, err = r.r.Read(r.dat)
if err != nil {
return err
}
if size != realSize {
return nil
}
r.dat, err = r.d.Decompress(r.dat)
return err
}
func (r *Reader) Read(b []byte) (int, error) {
curRead := 0
var toRead int
for curRead < len(b) {
if r.curOffset >= uint16(len(r.dat)) {
if err := r.advance(); err != nil {
return curRead, err
}
}
toRead = min(len(b)-curRead, len(r.dat)-int(r.curOffset))
copy(b[curRead:], r.dat[r.curOffset:int(r.curOffset)+toRead])
r.curOffset += uint16(toRead)
curRead += toRead
}
return curRead, nil
}
func (r *Reader) Close() error {
r.dat = nil
return nil
}
+25
View File
@@ -0,0 +1,25 @@
package routinemanager
type Manager struct {
channel chan uint16
maxRoutines uint16
}
func NewManager(maxRoutines uint16) *Manager {
m := &Manager{
maxRoutines: maxRoutines,
channel: make(chan uint16, maxRoutines),
}
for i := uint16(0); i < maxRoutines; i++ {
m.channel <- i
}
return m
}
func (m *Manager) Lock() uint16 {
return <-m.channel
}
func (m *Manager) Unlock(i uint16) {
m.channel <- i
}
+19
View File
@@ -0,0 +1,19 @@
package toreader
import "io"
type OffsetReader struct {
r io.ReaderAt
off int64
}
func NewOffsetReader(r io.ReaderAt, off int64) *OffsetReader {
return &OffsetReader{
r: r,
off: off,
}
}
func (r OffsetReader) ReadAt(p []byte, off int64) (n int, e error) {
return r.r.ReadAt(p, off+r.off)
}
+21
View File
@@ -0,0 +1,21 @@
package toreader
import "io"
type Reader struct {
r io.ReaderAt
offset int64
}
func NewReader(r io.ReaderAt, start int64) *Reader {
return &Reader{
r: r,
offset: start,
}
}
func (r *Reader) Read(b []byte) (int, error) {
n, err := r.r.ReadAt(b, r.offset)
r.offset += int64(n)
return n, err
}
+3
View File
@@ -0,0 +1,3 @@
# Lower-Level Squashfs
This library is a lower level version of the main [squashfs](https://github.com/CalebQ42/squashfs) library that doesn't try to be easy to use and exposes a lot of information that is not necesary for must use cases.
+260
View File
@@ -0,0 +1,260 @@
package data
import (
"errors"
"io"
"io/fs"
"math"
"runtime"
"sync"
"github.com/CalebQ42/squashfs/internal/decompress"
)
type FragReaderConstructor func() (io.Reader, error)
type FullReader struct {
r io.ReaderAt
d decompress.Decompressor
frag FragReaderConstructor
sizes []uint32
initialOffset int64
finalBlockSize uint64
blockSize uint32
goroutineLimit uint16
closed bool
}
func NewFullReader(r io.ReaderAt, initialOffset int64, d decompress.Decompressor, sizes []uint32, finalBlockSize uint64, blockSize uint32) FullReader {
return FullReader{
r: r,
d: d,
sizes: sizes,
initialOffset: initialOffset,
goroutineLimit: uint16(runtime.NumCPU()),
finalBlockSize: finalBlockSize,
blockSize: blockSize,
}
}
func (r *FullReader) Close() error {
r.closed = true
r.r = nil
r.d = nil
r.frag = nil
r.sizes = nil
return nil
}
func (r *FullReader) AddFrag(frag FragReaderConstructor) {
r.frag = frag
}
func (r *FullReader) SetGoroutineLimit(limit uint16) {
if limit <= 0 {
r.goroutineLimit = 1
}
r.goroutineLimit = limit
}
type retValue struct {
err error
data []byte
index uint64
}
func (r FullReader) process(index uint64, fileOffset uint64, pool *sync.Pool, retChan chan *retValue) {
ret := pool.Get().(*retValue)
ret.index = index
realSize := r.sizes[index] &^ (1 << 24)
if realSize == 0 {
if index == uint64(len(r.sizes))-1 && r.frag == nil {
ret.data = make([]byte, r.finalBlockSize)
} else {
ret.data = make([]byte, r.blockSize)
}
ret.err = nil
retChan <- ret
return
}
ret.data = make([]byte, realSize)
_, ret.err = r.r.ReadAt(ret.data, r.initialOffset+int64(fileOffset))
if r.sizes[index] == realSize {
ret.data, ret.err = r.d.Decompress(ret.data)
}
retChan <- ret
}
func (r FullReader) WriteTo(w io.Writer) (int64, error) {
if r.closed {
return 0, fs.ErrClosed
}
// if wa, is := w.(io.WriterAt); is {
// return r.writeToWriteAt(wa)
// }
var curIndex uint64
var curOffset uint64
var toProcess uint16
var wrote int64
cache := make(map[uint64]*retValue)
var errCache []error
retChan := make(chan *retValue, r.goroutineLimit)
pool := &sync.Pool{
New: func() any {
return &retValue{}
},
}
for i := uint64(0); i < uint64(math.Ceil(float64(len(r.sizes))/float64(r.goroutineLimit))); i++ {
toProcess = min(uint16(len(r.sizes))-(uint16(i)*r.goroutineLimit), r.goroutineLimit)
// Start all the goroutines
for j := uint16(0); j < toProcess; j++ {
go r.process((i*uint64(r.goroutineLimit))+uint64(j), curOffset, pool, retChan)
curOffset += uint64(r.sizes[(i*uint64(r.goroutineLimit))+uint64(j)]) &^ (1 << 24)
}
// Then consume the results on retChan
for j := uint16(0); j < toProcess; j++ {
res := <-retChan
// If there's an error, we don't care about the results.
if res.err != nil {
errCache = append(errCache, res.err)
if len(cache) > 0 {
clear(cache)
}
continue
}
// If there has been an error previously, we don't care about the results.
// We still want to wait for all the goroutines to prevent resources being wasted.
if len(errCache) > 0 {
continue
}
// If we don't need the data yet, we cache it and move on
if res.index != curIndex {
cache[res.index] = res
continue
}
// If we do need the data, we write it
wr, err := w.Write(res.data)
wrote += int64(wr)
if err != nil {
errCache = append(errCache, err)
if len(cache) > 0 {
clear(cache)
}
continue
}
pool.Put(res)
curIndex++
// Now we recursively try to clear the cache
for len(cache) > 0 {
res, ok := cache[curIndex]
if !ok {
break
}
wr, err := w.Write(res.data)
wrote += int64(wr)
if err != nil {
errCache = append(errCache, err)
if len(cache) > 0 {
clear(cache)
}
break
}
delete(cache, curIndex)
pool.Put(res)
curIndex++
}
}
if len(errCache) > 0 {
return wrote, errors.Join(errCache...)
}
}
if r.frag != nil {
rdr, err := r.frag()
if err != nil {
return wrote, err
}
wr, err := io.Copy(w, rdr)
wrote += wr
if l, ok := rdr.(*io.LimitedReader); ok {
if cl, ok := l.R.(io.Closer); ok {
cl.Close()
}
}
if err != nil {
return wrote, err
}
}
return wrote, nil
}
// func (r FullReader) writeToWriteAt(w io.WriterAt) (out int64, outErr error) {
// wait := &sync.WaitGroup{}
// wait.Add(len(r.sizes))
// mgr := routinemanager.NewManager(r.goroutineLimit)
// curOffset := r.initialOffset
// for i := uint64(0); i < uint64(len(r.sizes)); i++ {
// go func(index uint64, fileOffset int64) {
// lckNum := mgr.Lock()
// defer mgr.Unlock(lckNum)
// defer wait.Done()
// realSize := r.sizes[index] &^ (1 << 24)
// if realSize == 0 {
// if index == uint64(len(r.sizes))-1 && r.frag == nil {
// _, err := w.WriteAt([]byte{0}, int64((uint64(r.blockSize)*index)+r.finalBlockSize)-1)
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// out = max(out, int64((uint64(r.blockSize)*index)+r.finalBlockSize))
// }
// return
// }
// data := make([]byte, realSize)
// err := binary.Read(toreader.NewReader(r.r, int64(fileOffset)), binary.LittleEndian, &data)
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// if r.sizes[index] == realSize {
// data, err = r.d.Decompress(data)
// }
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// _, err = w.WriteAt(data, int64(uint64(r.blockSize)*index))
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// out = max(out, int64(uint64(r.blockSize)*(index+1)))
// }(i, curOffset)
// curOffset += int64(r.sizes[i]) &^ (1 << 24)
// }
// if r.frag != nil {
// wait.Add(1)
// go func() {
// lckNum := mgr.Lock()
// defer mgr.Unlock(lckNum)
// defer wait.Done()
// rdr, err := r.frag()
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// dat, err := io.ReadAll(rdr)
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// _, err = w.WriteAt(dat, int64(int(r.blockSize)*len(r.sizes)))
// if err != nil {
// outErr = errors.Join(outErr, err)
// return
// }
// out = int64(int(r.blockSize)*len(r.sizes)) + int64(r.finalBlockSize)
// }()
// }
// wait.Wait()
// return
// }
+104
View File
@@ -0,0 +1,104 @@
package data
import (
"io"
"io/fs"
"github.com/CalebQ42/squashfs/internal/decompress"
)
type Reader struct {
r io.Reader
d decompress.Decompressor
frag io.Reader
sizes []uint32
dat []byte
curOffset int
curIndex uint64
finalBlockSize uint64
blockSize uint32
closed bool
}
func NewReader(r io.Reader, d decompress.Decompressor, sizes []uint32, finalBlockSize uint64, blockSize uint32) Reader {
return Reader{
r: r,
d: d,
sizes: sizes,
finalBlockSize: finalBlockSize,
blockSize: blockSize,
}
}
func (r *Reader) AddFrag(fragRdr io.Reader) {
r.frag = fragRdr
}
func (r *Reader) advance() error {
r.curOffset = 0
defer func() { r.curIndex++ }()
var err error
if r.curIndex == uint64(len(r.sizes)) && r.frag != nil {
r.dat, err = io.ReadAll(r.frag)
return err
} else if r.curIndex >= uint64(len(r.sizes)) {
r.dat = []byte{}
return io.EOF
}
realSize := r.sizes[r.curIndex] &^ (1 << 24)
if realSize == 0 {
if r.curIndex == uint64(len(r.sizes))-1 && r.frag == nil {
r.dat = make([]byte, r.finalBlockSize)
} else {
r.dat = make([]byte, r.blockSize)
}
return nil
}
r.dat = make([]byte, realSize)
_, err = r.r.Read(r.dat)
if err != nil {
return err
}
if r.sizes[r.curIndex] != realSize {
return nil
}
r.dat, err = r.d.Decompress(r.dat)
return err
}
func (r *Reader) Read(b []byte) (int, error) {
if r.closed {
return 0, fs.ErrClosed
}
curRead := 0
var toRead int
for curRead < len(b) {
if r.curOffset >= len(r.dat) {
if err := r.advance(); err != nil {
return curRead, err
}
}
toRead = min(len(b)-curRead, len(r.dat)-r.curOffset)
toRead = copy(b[curRead:], r.dat[r.curOffset:r.curOffset+toRead])
r.curOffset += toRead
curRead += toRead
}
return curRead, nil
}
func (r *Reader) Close() error {
r.closed = true
r.r = nil
r.d = nil
if r.frag != nil {
if l, ok := r.frag.(*io.LimitedReader); ok {
if cl, ok := l.R.(io.Closer); ok {
cl.Close()
}
}
}
r.frag = nil
r.sizes = nil
r.dat = nil
return nil
}
+83
View File
@@ -0,0 +1,83 @@
package squashfslow
import (
"errors"
"io/fs"
"path/filepath"
"slices"
"strings"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
"github.com/CalebQ42/squashfs/low/directory"
"github.com/CalebQ42/squashfs/low/inode"
)
type Directory struct {
FileBase
Entries []directory.Entry
}
func (r Reader) directoryFromRef(ref uint64, name string) (Directory, error) {
i, err := r.InodeFromRef(ref)
if err != nil {
return Directory{}, err
}
var blockStart uint32
var size uint32
var offset uint16
switch i.Type {
case inode.Dir:
blockStart = i.Data.(inode.Directory).BlockStart
size = uint32(i.Data.(inode.Directory).Size)
offset = i.Data.(inode.Directory).Offset
case inode.EDir:
blockStart = i.Data.(inode.EDirectory).BlockStart
size = i.Data.(inode.EDirectory).Size
offset = i.Data.(inode.EDirectory).Offset
default:
return Directory{}, errors.New("not a directory")
}
dirRdr := metadata.NewReader(toreader.NewReader(r.r, int64(r.Superblock.DirTableStart)+int64(blockStart)), r.d)
defer dirRdr.Close()
_, err = dirRdr.Read(make([]byte, offset))
if err != nil {
return Directory{}, err
}
entries, err := directory.ReadDirectory(&dirRdr, size)
if err != nil {
return Directory{}, err
}
return Directory{
FileBase: r.BaseFromInode(i, name),
Entries: entries,
}, nil
}
func (d Directory) Open(r Reader, path string) (FileBase, error) {
path = filepath.Clean(path)
if path == "." || path == "" {
return d.FileBase, nil
}
split := strings.Split(path, "/")
i, found := slices.BinarySearchFunc(d.Entries, split[0], func(e directory.Entry, name string) int {
return strings.Compare(e.Name, name)
})
if !found {
return FileBase{}, fs.ErrNotExist
}
b, err := r.BaseFromEntry(d.Entries[i])
if err != nil {
return FileBase{}, err
}
if len(split) == 1 {
return b, nil
} else if !b.IsDir() {
return FileBase{}, fs.ErrNotExist
}
dir, err := b.ToDir(r)
if err != nil {
return FileBase{}, err
}
return dir.Open(r, strings.Join(split[1:], "/"))
}
+62
View File
@@ -0,0 +1,62 @@
package directory
import (
"encoding/binary"
"io"
)
type header struct {
Count uint32
BlockStart uint32
Num uint32
}
type decEntry struct {
Offset uint16
NumOffset int16
InodeType uint16
NameSize uint16
// Name []byte (not decoded along with decEntry)
}
type Entry struct {
Name string
BlockStart uint32
Offset uint16
InodeType uint16
Num uint32
}
func ReadDirectory(r io.Reader, size uint32) (out []Entry, err error) {
size -= 3
var curRead uint32
var h header
var de decEntry
for curRead < size {
err = binary.Read(r, binary.LittleEndian, &h)
if err != nil {
return
}
curRead += 12
for i := uint32(0); i < h.Count+1 && curRead < size; i++ {
err = binary.Read(r, binary.LittleEndian, &de)
if err != nil {
return
}
nameTmp := make([]byte, de.NameSize+1)
err = binary.Read(r, binary.LittleEndian, &nameTmp)
if err != nil {
return
}
curRead += 8 + uint32(de.NameSize) + 1
out = append(out, Entry{
BlockStart: h.BlockStart,
Offset: de.Offset,
Name: string(nameTmp),
InodeType: de.InodeType,
Num: h.Num + uint32(de.NumOffset),
})
}
}
return
}
+203
View File
@@ -0,0 +1,203 @@
package squashfslow
import (
"errors"
"io"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
"github.com/CalebQ42/squashfs/low/data"
"github.com/CalebQ42/squashfs/low/directory"
"github.com/CalebQ42/squashfs/low/inode"
)
type FileBase struct {
Inode inode.Inode
Name string
}
func (r Reader) BaseFromInode(i inode.Inode, name string) FileBase {
return FileBase{Inode: i, Name: name}
}
func (r Reader) BaseFromEntry(e directory.Entry) (FileBase, error) {
in, err := r.InodeFromEntry(e)
if err != nil {
return FileBase{}, err
}
return FileBase{Inode: in, Name: e.Name}, nil
}
func (r Reader) BaseFromRef(ref uint64, name string) (FileBase, error) {
in, err := r.InodeFromRef(ref)
if err != nil {
return FileBase{}, err
}
return FileBase{Inode: in, Name: name}, nil
}
func (b FileBase) Uid(r *Reader) (uint32, error) {
return r.Id(b.Inode.UidInd)
}
func (b FileBase) Gid(r *Reader) (uint32, error) {
return r.Id(b.Inode.GidInd)
}
func (b FileBase) IsDir() bool {
return b.Inode.Type == inode.Dir || b.Inode.Type == inode.EDir
}
func (b FileBase) ToDir(r Reader) (Directory, error) {
var blockStart uint32
var size uint32
var offset uint16
switch b.Inode.Type {
case inode.Dir:
blockStart = b.Inode.Data.(inode.Directory).BlockStart
size = uint32(b.Inode.Data.(inode.Directory).Size)
offset = b.Inode.Data.(inode.Directory).Offset
case inode.EDir:
blockStart = b.Inode.Data.(inode.EDirectory).BlockStart
size = b.Inode.Data.(inode.EDirectory).Size
offset = b.Inode.Data.(inode.EDirectory).Offset
default:
return Directory{}, errors.New("not a directory")
}
dirRdr := metadata.NewReader(toreader.NewReader(r.r, int64(r.Superblock.DirTableStart)+int64(blockStart)), r.d)
defer dirRdr.Close()
_, err := dirRdr.Read(make([]byte, offset))
if err != nil {
return Directory{}, err
}
entries, err := directory.ReadDirectory(&dirRdr, size)
if err != nil {
return Directory{}, err
}
return Directory{
FileBase: b,
Entries: entries,
}, nil
}
func (b FileBase) IsRegular() bool {
return b.Inode.Type == inode.Fil || b.Inode.Type == inode.EFil
}
func (b FileBase) GetRegFileReaders(r Reader) (data.Reader, data.FullReader, error) {
if !b.IsRegular() {
return data.Reader{}, data.FullReader{}, errors.New("not a regular file")
}
var blockStart uint64
var fragIndex uint32
var fragOffset uint32
var fragSize uint64
var sizes []uint32
if b.Inode.Type == inode.Fil {
blockStart = uint64(b.Inode.Data.(inode.File).BlockStart)
fragIndex = b.Inode.Data.(inode.File).FragInd
fragOffset = b.Inode.Data.(inode.File).FragOffset
sizes = b.Inode.Data.(inode.File).BlockSizes
fragSize = uint64(b.Inode.Data.(inode.File).Size % r.Superblock.BlockSize)
} else {
blockStart = b.Inode.Data.(inode.EFile).BlockStart
fragIndex = b.Inode.Data.(inode.EFile).FragInd
fragOffset = b.Inode.Data.(inode.EFile).FragOffset
sizes = b.Inode.Data.(inode.EFile).BlockSizes
fragSize = b.Inode.Data.(inode.EFile).Size % uint64(r.Superblock.BlockSize)
}
frag := func() (io.Reader, error) {
ent, err := r.fragEntry(fragIndex)
if err != nil {
return nil, err
}
frag := data.NewReader(toreader.NewReader(r.r, int64(ent.Start)), r.d, []uint32{ent.Size}, uint64(r.Superblock.BlockSize), r.Superblock.BlockSize)
frag.Read(make([]byte, fragOffset))
return io.LimitReader(&frag, int64(fragSize)), nil
}
outRdr := data.NewReader(toreader.NewReader(r.r, int64(blockStart)), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
f, err := frag()
if err != nil {
return data.Reader{}, data.FullReader{}, err
}
outRdr.AddFrag(f)
}
outFull := data.NewFullReader(r.r, int64(blockStart), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
outFull.AddFrag(frag)
}
return outRdr, outFull, nil
}
func (b FileBase) GetFullReader(r *Reader) (data.FullReader, error) {
if !b.IsRegular() {
return data.FullReader{}, errors.New("not a regular file")
}
var blockStart uint64
var fragIndex uint32
var fragOffset uint32
var fragSize uint64
var sizes []uint32
if b.Inode.Type == inode.Fil {
blockStart = uint64(b.Inode.Data.(inode.File).BlockStart)
fragIndex = b.Inode.Data.(inode.File).FragInd
fragOffset = b.Inode.Data.(inode.File).FragOffset
sizes = b.Inode.Data.(inode.File).BlockSizes
fragSize = uint64(b.Inode.Data.(inode.File).Size % r.Superblock.BlockSize)
} else {
blockStart = b.Inode.Data.(inode.EFile).BlockStart
fragIndex = b.Inode.Data.(inode.EFile).FragInd
fragOffset = b.Inode.Data.(inode.EFile).FragOffset
sizes = b.Inode.Data.(inode.EFile).BlockSizes
fragSize = b.Inode.Data.(inode.EFile).Size % uint64(r.Superblock.BlockSize)
}
outFull := data.NewFullReader(r.r, int64(blockStart), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
outFull.AddFrag(func() (io.Reader, error) {
ent, err := r.fragEntry(fragIndex)
if err != nil {
return nil, err
}
frag := data.NewReader(toreader.NewReader(r.r, int64(ent.Start)), r.d, []uint32{ent.Size}, uint64(r.Superblock.BlockSize), r.Superblock.BlockSize)
frag.Read(make([]byte, fragOffset))
return io.LimitReader(&frag, int64(fragSize)), nil
})
}
return outFull, nil
}
func (b FileBase) GetReader(r *Reader) (data.Reader, error) {
if !b.IsRegular() {
return data.Reader{}, errors.New("not a regular file")
}
var blockStart uint64
var fragIndex uint32
var fragOffset uint32
var fragSize uint64
var sizes []uint32
if b.Inode.Type == inode.Fil {
blockStart = uint64(b.Inode.Data.(inode.File).BlockStart)
fragIndex = b.Inode.Data.(inode.File).FragInd
fragOffset = b.Inode.Data.(inode.File).FragOffset
sizes = b.Inode.Data.(inode.File).BlockSizes
fragSize = uint64(b.Inode.Data.(inode.File).Size % r.Superblock.BlockSize)
} else {
blockStart = b.Inode.Data.(inode.EFile).BlockStart
fragIndex = b.Inode.Data.(inode.EFile).FragInd
fragOffset = b.Inode.Data.(inode.EFile).FragOffset
sizes = b.Inode.Data.(inode.EFile).BlockSizes
fragSize = b.Inode.Data.(inode.EFile).Size % uint64(r.Superblock.BlockSize)
}
outRdr := data.NewReader(toreader.NewReader(r.r, int64(blockStart)), r.d, sizes, fragSize, r.Superblock.BlockSize)
if fragIndex != 0xffffffff {
ent, err := r.fragEntry(fragIndex)
if err != nil {
return data.Reader{}, err
}
frag := data.NewReader(toreader.NewReader(r.r, int64(ent.Start)), r.d, []uint32{ent.Size}, uint64(r.Superblock.BlockSize), r.Superblock.BlockSize)
frag.Read(make([]byte, fragOffset))
outRdr.AddFrag(io.LimitReader(&frag, int64(fragSize)))
}
return outRdr, nil
}
+7
View File
@@ -0,0 +1,7 @@
package squashfslow
type fragEntry struct {
Start uint64
Size uint32
_ uint32
}
+26
View File
@@ -0,0 +1,26 @@
package squashfslow
import (
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
"github.com/CalebQ42/squashfs/low/directory"
"github.com/CalebQ42/squashfs/low/inode"
)
func (r Reader) InodeFromRef(ref uint64) (inode.Inode, error) {
offset, meta := (ref>>16)+r.Superblock.InodeTableStart, ref&0xFFFF
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
defer rdr.Close()
_, err := rdr.Read(make([]byte, meta))
if err != nil {
return inode.Inode{}, err
}
return inode.Read(&rdr, r.Superblock.BlockSize)
}
func (r Reader) InodeFromEntry(e directory.Entry) (inode.Inode, error) {
rdr := metadata.NewReader(toreader.NewReader(r.r, int64(r.Superblock.InodeTableStart)+int64(e.BlockStart)), r.d)
defer rdr.Close()
rdr.Read(make([]byte, e.Offset))
return inode.Read(&rdr, r.Superblock.BlockSize)
}
+78
View File
@@ -0,0 +1,78 @@
package inode
import (
"encoding/binary"
"io"
)
type Directory struct {
BlockStart uint32
LinkCount uint32
Size uint16
Offset uint16
ParentNum uint32
}
type EDirectory struct {
LinkCount uint32
Size uint32
BlockStart uint32
ParentNum uint32
IndCount uint16
Offset uint16
XattrInd uint32
Indexes []DirectoryIndex
}
type DirectoryIndex struct {
Ind uint32
Start uint32
NameSize uint32
Name []byte
}
func ReadDir(r io.Reader) (d Directory, err error) {
dat := make([]byte, 16)
_, err = r.Read(dat)
if err != nil {
return
}
d.BlockStart = binary.LittleEndian.Uint32(dat)
d.LinkCount = binary.LittleEndian.Uint32(dat[4:])
d.Size = binary.LittleEndian.Uint16(dat[8:])
d.Offset = binary.LittleEndian.Uint16(dat[10:])
d.ParentNum = binary.LittleEndian.Uint32(dat[12:])
return
}
func ReadEDir(r io.Reader) (d EDirectory, err error) {
dat := make([]byte, 24)
_, err = r.Read(dat)
if err != nil {
return
}
d.LinkCount = binary.LittleEndian.Uint32(dat)
d.Size = binary.LittleEndian.Uint32(dat[4:])
d.BlockStart = binary.LittleEndian.Uint32(dat[8:])
d.ParentNum = binary.LittleEndian.Uint32(dat[12:])
d.IndCount = binary.LittleEndian.Uint16(dat[16:])
d.Offset = binary.LittleEndian.Uint16(dat[18:])
d.XattrInd = binary.LittleEndian.Uint32(dat[20:])
d.Indexes = make([]DirectoryIndex, d.IndCount)
for i := range d.IndCount {
dat = make([]byte, 12)
_, err = r.Read(dat)
if err != nil {
return
}
d.Indexes[i].Ind = binary.LittleEndian.Uint32(dat)
d.Indexes[i].Start = binary.LittleEndian.Uint32(dat[4:])
d.Indexes[i].NameSize = binary.LittleEndian.Uint32(dat[8:])
d.Indexes[i].Name = make([]byte, d.Indexes[i].NameSize+1)
_, err = r.Read(d.Indexes[i].Name)
if err != nil {
return
}
}
return
}
+70
View File
@@ -0,0 +1,70 @@
package inode
import (
"encoding/binary"
"io"
"math"
)
type File struct {
BlockStart uint32
FragInd uint32
FragOffset uint32
Size uint32
BlockSizes []uint32
}
type eFileInit struct {
BlockStart uint64
Size uint64
Sparse uint64
LinkCount uint32
FragInd uint32
FragOffset uint32
XattrInd uint32
}
type EFile struct {
eFileInit
BlockSizes []uint32
}
func ReadFile(r io.Reader, blockSize uint32) (f File, err error) {
dat := make([]byte, 16)
_, err = r.Read(dat)
if err != nil {
return
}
f.BlockStart = binary.LittleEndian.Uint32(dat)
f.FragInd = binary.LittleEndian.Uint32(dat[4:])
f.FragOffset = binary.LittleEndian.Uint32(dat[8:])
f.Size = binary.LittleEndian.Uint32(dat[12:])
toRead := int(math.Floor(float64(f.Size) / float64(blockSize)))
if f.FragInd == 0xFFFFFFFF && f.Size%blockSize > 0 {
toRead++
}
dat = make([]byte, toRead*4)
_, err = r.Read(dat)
if err != nil {
return
}
f.BlockSizes = make([]uint32, toRead)
for i := range toRead {
f.BlockSizes[i] = binary.LittleEndian.Uint32(dat[i*4:])
}
return
}
func ReadEFile(r io.Reader, blockSize uint32) (f EFile, err error) {
err = binary.Read(r, binary.LittleEndian, &f.eFileInit)
if err != nil {
return
}
toRead := int(math.Floor(float64(f.Size) / float64(blockSize)))
if f.FragInd == 0xFFFFFFFF && f.Size%uint64(blockSize) > 0 {
toRead++
}
f.BlockSizes = make([]uint32, toRead)
err = binary.Read(r, binary.LittleEndian, &f.BlockSizes)
return
}
+137
View File
@@ -0,0 +1,137 @@
package inode
import (
"encoding/binary"
"errors"
"io"
"io/fs"
"strconv"
)
const (
Dir = uint16(iota + 1)
Fil
Sym
Block
Char
Fifo
Sock
EDir
EFil
ESym
EBlock
EChar
EFifo
ESock
)
type Header struct {
Type uint16
Perm uint16
UidInd uint16
GidInd uint16
ModTime uint32
Num uint32
}
type Inode struct {
Header
Data any
}
func Read(r io.Reader, blockSize uint32) (i Inode, err error) {
err = binary.Read(r, binary.LittleEndian, &i.Header)
if err != nil {
return
}
switch i.Type {
case Dir:
i.Data, err = ReadDir(r)
case Fil:
i.Data, err = ReadFile(r, blockSize)
case Sym:
i.Data, err = ReadSym(r)
case Block:
fallthrough
case Char:
i.Data, err = ReadDevice(r)
case Fifo:
fallthrough
case Sock:
i.Data, err = ReadIPC(r)
case EDir:
i.Data, err = ReadEDir(r)
case EFil:
i.Data, err = ReadEFile(r, blockSize)
case ESym:
i.Data, err = ReadESym(r)
case EBlock:
fallthrough
case EChar:
i.Data, err = ReadEDevice(r)
case EFifo:
fallthrough
case ESock:
i.Data, err = ReadEIPC(r)
default:
return i, errors.New("invalid inode type " + strconv.Itoa(int(i.Type)))
}
return
}
func (i Inode) Mode() (out fs.FileMode) {
out = fs.FileMode(i.Perm)
switch i.Type {
case Dir, EDir:
out |= fs.ModeDir
case Sym, ESym:
out |= fs.ModeSymlink
case Char, EChar, Block, EBlock:
out |= fs.ModeDevice
case Fifo, EFifo:
out |= fs.ModeNamedPipe
case Sock, ESock:
out |= fs.ModeSocket
}
return
}
func (i Inode) LinkCount() uint32 {
switch i.Data.(type) {
case EFile:
return i.Data.(EFile).LinkCount
case Directory:
return i.Data.(Directory).LinkCount
case EDirectory:
return i.Data.(EDirectory).LinkCount
case Device:
return i.Data.(Device).LinkCount
case EDevice:
return i.Data.(EDevice).LinkCount
case IPC:
return i.Data.(IPC).LinkCount
case EIPC:
return i.Data.(EIPC).LinkCount
case Symlink:
return i.Data.(Symlink).LinkCount
case ESymlink:
return i.Data.(ESymlink).LinkCount
default:
return 0
}
}
func (i Inode) Size() uint64 {
switch i.Data.(type) {
case File:
return uint64(i.Data.(File).Size)
case EFile:
return i.Data.(EFile).Size
// case Directory:
// return uint64(i.Data.(Directory).Size)
// case EDirectory:
// return uint64(i.Data.(EDirectory).Size)
default:
return 0
}
}
+45
View File
@@ -0,0 +1,45 @@
package inode
import (
"encoding/binary"
"io"
)
type Device struct {
LinkCount uint32
Dev uint32
}
type EDevice struct {
Device
XattrInd uint32
}
func ReadDevice(r io.Reader) (d Device, err error) {
err = binary.Read(r, binary.LittleEndian, &d)
return
}
func ReadEDevice(r io.Reader) (d EDevice, err error) {
err = binary.Read(r, binary.LittleEndian, &d)
return
}
type IPC struct {
LinkCount uint32
}
type EIPC struct {
IPC
XattrInd uint32
}
func ReadIPC(r io.Reader) (i IPC, err error) {
err = binary.Read(r, binary.LittleEndian, &i)
return
}
func ReadEIPC(r io.Reader) (i EIPC, err error) {
err = binary.Read(r, binary.LittleEndian, &i)
return
}
+46
View File
@@ -0,0 +1,46 @@
package inode
import (
"encoding/binary"
"io"
)
type symlinkInit struct {
LinkCount uint32
TargetSize uint32
}
type Symlink struct {
symlinkInit
Target []byte
}
type ESymlink struct {
symlinkInit
Target []byte
XattrInd uint32
}
func ReadSym(r io.Reader) (s Symlink, err error) {
err = binary.Read(r, binary.LittleEndian, &s.symlinkInit)
if err != nil {
return
}
s.Target = make([]byte, s.TargetSize)
err = binary.Read(r, binary.LittleEndian, &s.Target)
return
}
func ReadESym(r io.Reader) (s ESymlink, err error) {
err = binary.Read(r, binary.LittleEndian, &s.symlinkInit)
if err != nil {
return
}
s.Target = make([]byte, s.TargetSize)
err = binary.Read(r, binary.LittleEndian, &s.Target)
if err != nil {
return
}
err = binary.Read(r, binary.LittleEndian, &s.XattrInd)
return
}
+218
View File
@@ -0,0 +1,218 @@
package squashfslow
import (
"encoding/binary"
"errors"
"io"
"math"
"github.com/CalebQ42/squashfs/internal/decompress"
"github.com/CalebQ42/squashfs/internal/metadata"
"github.com/CalebQ42/squashfs/internal/toreader"
"github.com/CalebQ42/squashfs/low/inode"
)
// The types of compression supported by squashfs
const (
ZlibCompression = uint16(iota + 1)
LZMACompression
LZOCompression
XZCompression
LZ4Compression
ZSTDCompression
)
var (
ErrorMagic = errors.New("magic incorrect. probably not reading squashfs archive or archive is corrupted")
ErrorLog = errors.New("block log is incorrect. possible corrupted archive")
ErrorVersion = errors.New("squashfs version of archive is not 4.0. may be corrupted")
ErrorNotExportable = errors.New("archive does not have an export table")
)
type Reader struct {
r io.ReaderAt
d decompress.Decompressor
Root Directory
fragTable []fragEntry
idTable []uint32
exportTable []uint64
Superblock superblock
}
func NewReader(r io.ReaderAt) (rdr Reader, err error) {
rdr.r = r
err = binary.Read(toreader.NewReader(r, 0), binary.LittleEndian, &rdr.Superblock)
if err != nil {
return rdr, errors.Join(errors.New("failed to read superblock"), err)
}
if !rdr.Superblock.ValidMagic() {
return rdr, ErrorMagic
}
if !rdr.Superblock.ValidBlockLog() {
return rdr, ErrorLog
}
if !rdr.Superblock.ValidVersion() {
return rdr, ErrorVersion
}
switch rdr.Superblock.CompType {
case ZlibCompression:
rdr.d = decompress.NewZlib()
case LZMACompression:
rdr.d, err = decompress.NewLzma()
if err != nil {
return rdr, err
}
case LZOCompression:
rdr.d, err = decompress.NewLzo()
if err != nil {
return rdr, err
}
case XZCompression:
rdr.d = decompress.NewXz()
case LZ4Compression:
rdr.d = decompress.NewLz4()
case ZSTDCompression:
rdr.d = decompress.NewZstd()
default:
return rdr, errors.New("invalid compression type. possible corrupted archive")
}
rdr.Root, err = rdr.directoryFromRef(rdr.Superblock.RootInodeRef, "")
if err != nil {
return rdr, errors.Join(errors.New("failed to read root directory"), err)
}
return
}
// Get a uid/gid at the given index. Lazily populates the reader's Id table as necessary.
func (r *Reader) Id(i uint16) (uint32, error) {
if len(r.idTable) > int(i) {
return r.idTable[i], nil
} else if i >= r.Superblock.IdCount {
return 0, errors.New("id out of bounds")
}
// Populate the id table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/2048)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.idTable) / 2048
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var idsToRead uint16
var idsTmp []uint32
var err error
var rdr metadata.Reader
// We can *maybe* have a slight speed increase by manually decoding instead of using reflection via binary.Read
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.IdTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return 0, err
}
idsToRead = min(r.Superblock.IdCount-uint16(len(r.idTable)), 2048)
idsTmp = make([]uint32, idsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(&rdr, binary.LittleEndian, &idsTmp)
rdr.Close()
if err != nil {
return 0, err
}
r.idTable = append(r.idTable, idsTmp...)
}
return r.idTable[i], nil
}
// Get a fragment entry at the given index. Lazily populates the reader's fragment table as necessary.
func (r *Reader) fragEntry(i uint32) (fragEntry, error) {
if len(r.fragTable) > int(i) {
return r.fragTable[i], nil
} else if i >= r.Superblock.FragCount {
return fragEntry{}, errors.New("fragment out of bounds")
}
// Populate the fragment table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/512)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.fragTable) / 512
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var fragsToRead uint32
var fragsTmp []fragEntry
var err error
var rdr metadata.Reader
// We can *maybe* have a slight speed increase by manually decoding instead of using reflection via binary.Read
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.FragTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return fragEntry{}, err
}
fragsToRead = min(r.Superblock.FragCount-uint32(len(r.fragTable)), 512)
fragsTmp = make([]fragEntry, fragsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(&rdr, binary.LittleEndian, &fragsTmp)
rdr.Close()
if err != nil {
return fragEntry{}, err
}
r.fragTable = append(r.fragTable, fragsTmp...)
}
return r.fragTable[i], nil
}
// Get an inode reference at the given index. Lazily populates the reader's export table as necessary.
func (r *Reader) inodeRef(i uint32) (uint64, error) {
if !r.Superblock.Exportable() {
return 0, ErrorNotExportable
}
if len(r.exportTable) > int(i) {
return r.exportTable[i], nil
} else if i >= r.Superblock.InodeCount {
return 0, errors.New("inode out of bounds")
}
// Populate the export table as needed
var blockNum uint32
if i != 0 { // If i == 0, we go negatives causing issues with uint32s
blockNum = uint32(math.Ceil(float64(i+1)/1024)) - 1
} else {
blockNum = 0
}
blocksRead := len(r.exportTable) / 1024
blocksToRead := int(blockNum) - blocksRead + 1
var offset uint64
var refsToRead uint32
var refsTmp []uint64
var err error
var rdr metadata.Reader
// We can *maybe* have a slight speed increase by manually decoding instead of using reflection via binary.Read
for i := blocksRead; i < int(blocksRead)+blocksToRead; i++ {
err = binary.Read(toreader.NewReader(r.r, int64(r.Superblock.ExportTableStart)+int64(8*i)), binary.LittleEndian, &offset)
if err != nil {
return 0, err
}
refsToRead = min(r.Superblock.InodeCount-uint32(len(r.exportTable)), 1024)
refsTmp = make([]uint64, refsToRead)
rdr = metadata.NewReader(toreader.NewReader(r.r, int64(offset)), r.d)
err = binary.Read(&rdr, binary.LittleEndian, &refsTmp)
rdr.Close()
if err != nil {
return 0, err
}
r.exportTable = append(r.exportTable, refsTmp...)
}
return r.exportTable[i], nil
}
func (r Reader) Inode(i uint32) (inode.Inode, error) {
ref, err := r.inodeRef(i)
if err != nil {
return inode.Inode{}, err
}
return r.InodeFromRef(ref)
}
+146
View File
@@ -0,0 +1,146 @@
package squashfslow
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"testing"
)
const (
squashfsURL = "https://darkstorm.tech/files/LinuxPATest.sfs"
squashfsName = "airootfs.sfs"
)
func preTest(dir string) (fil *os.File, err error) {
fil, err = os.Open(filepath.Join(dir, squashfsName))
if err != nil {
_, err = os.Open(dir)
if os.IsNotExist(err) {
err = os.Mkdir(dir, 0755)
}
if err != nil {
return
}
os.Remove(filepath.Join(dir, squashfsName))
fil, err = os.Create(filepath.Join(dir, squashfsName))
if err != nil {
return
}
var resp *http.Response
resp, err = http.DefaultClient.Get(squashfsURL)
if err != nil {
return
}
_, err = io.Copy(fil, resp.Body)
if err != nil {
return
}
}
_, err = exec.LookPath("unsquashfs")
if err != nil {
return
}
_, err = exec.LookPath("mksquashfs")
return
}
func TestMisc(t *testing.T) {
tmpDir := "../testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
defer fil.Close()
rdr, err := NewReader(fil)
if err != nil {
t.Fatal(err)
}
t.Log(rdr.Superblock.FragCount)
t.Fatal(rdr.fragEntry(1233))
}
func TestReader(t *testing.T) {
tmpDir := "../testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
defer fil.Close()
rdr, err := NewReader(fil)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(tmpDir, "extractTest")
os.RemoveAll(path)
os.MkdirAll(path, 0777)
err = extractToDir(rdr, rdr.Root.FileBase, path)
t.Fatal(err)
}
var singleFile = "PortableApps/CPU-X/CPU-X-v4.2.0-x86_64.AppImage"
func TestSingleFile(t *testing.T) {
tmpDir := "../testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
defer fil.Close()
rdr, err := NewReader(fil)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(tmpDir, "extractTest")
os.RemoveAll(path)
os.MkdirAll(path, 0777)
b, err := rdr.Root.Open(rdr, singleFile)
if err != nil {
t.Fatal(err)
}
err = extractToDir(rdr, b, path)
t.Fatal(err)
}
func extractToDir(rdr Reader, b FileBase, folder string) error {
path := filepath.Join(folder, b.Name)
if b.IsDir() {
d, err := b.ToDir(rdr)
if err != nil {
return err
}
err = os.MkdirAll(path, 0777)
if err != nil {
return err
}
var nestBast FileBase
for _, e := range d.Entries {
nestBast, err = rdr.BaseFromEntry(e)
if err != nil {
return err
}
err = extractToDir(rdr, nestBast, path)
if err != nil {
return err
}
}
} else if b.IsRegular() {
_, full, err := b.GetRegFileReaders(rdr)
if err != nil {
return err
}
fil, err := os.Create(path)
if err != nil {
return err
}
_, err = full.WriteTo(fil)
if err != nil {
return err
}
fmt.Println("Successfully extracted file:", b.Name)
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
package squashfslow
import "math"
type superblock struct {
Magic uint32
InodeCount uint32
ModTime uint32
BlockSize uint32
FragCount uint32
CompType uint16
BlockLog uint16
Flags uint16
IdCount uint16
VerMaj uint16
VerMin uint16
RootInodeRef uint64
Size uint64
IdTableStart uint64
XattrTableStart uint64
InodeTableStart uint64
DirTableStart uint64
FragTableStart uint64
ExportTableStart uint64
}
func (s superblock) ValidMagic() bool {
return s.Magic == 0x73717368
}
func (s superblock) ValidBlockLog() bool {
return s.BlockLog == uint16(math.Log2(float64(s.BlockSize)))
}
func (s superblock) ValidVersion() bool {
return s.VerMaj == 4 && s.VerMin == 0
}
func (s superblock) UncompressedInodes() bool {
return s.Flags&0x1 == 0x1
}
func (s superblock) UncompressedData() bool {
return s.Flags&0x2 == 0x2
}
func (s superblock) UncompressedFragments() bool {
return s.Flags&0x8 == 0x8
}
func (s superblock) NoFragments() bool {
return s.Flags&0x10 == 0x10
}
func (s superblock) AlwaysFragment() bool {
return s.Flags&0x20 == 0x20
}
func (s superblock) Duplicates() bool {
return s.Flags&0x40 == 0x40
}
func (s superblock) Exportable() bool {
return s.Flags&0x80 == 0x80
}
func (s superblock) UncompressedXattrs() bool {
return s.Flags&0x100 == 0x100
}
func (s superblock) NoXattrs() bool {
return s.Flags&0x200 == 0x200
}
func (s superblock) CompressionOptions() bool {
return s.Flags&0x400 == 0x400
}
func (s superblock) UncompressedIDs() bool {
return s.Flags&0x800 == 0x800
}
-188
View File
@@ -1,188 +0,0 @@
package squashfs
import (
"bytes"
"encoding/binary"
"errors"
"io"
)
type metadata struct {
raw uint16
size uint16
compressed bool
}
//MetadataReader is a block reader for metadata. It will automatically read the next block, when it reaches the end of a block.
type metadataReader struct {
s *Reader
offset int64
headers []*metadata
data []byte
readOffset int
}
//NewMetadataReader creates a new MetadataReader, beginning to read at the given offset. It will automatically cache the first block at the location.
func (s *Reader) newMetadataReader(offset int64) (*metadataReader, error) {
var br metadataReader
br.s = s
br.offset = offset
err := br.parseMetadata()
if err != nil {
return nil, err
}
err = br.readNextDataBlock()
if err != nil {
return nil, err
}
return &br, nil
}
//NewMetadataReaderFromInodeRef creates a new MetadataReader with the offsets set by the given inode reference.
func (s *Reader) newMetadataReaderFromInodeRef(ref uint64) (*metadataReader, error) {
offset, metaOffset := processInodeRef(ref)
br, err := s.newMetadataReader(int64(s.super.InodeTableStart + offset))
if err != nil {
return nil, err
}
_, err = br.Seek(int64(metaOffset), io.SeekStart)
if err != nil {
return nil, err
}
return br, nil
}
//ProcessInodeRef processes an inode reference and returns two values
//
//The first value is the inode table offset. AKA, it's where the metadata block of the inode STARTS relative to the inode table.
//
//The second value is the offset of the inode, INSIDE of the metadata.
func processInodeRef(inodeRef uint64) (tableOffset uint64, metaOffset uint64) {
tableOffset = inodeRef >> 16
metaOffset = inodeRef &^ 0xFFFFFFFF0000
return
}
func (br *metadataReader) parseMetadata() error {
var raw uint16
err := binary.Read(io.NewSectionReader(br.s.r, br.offset, 2), binary.LittleEndian, &raw)
if err != nil {
return err
}
br.offset += 2
compressed := raw&0x8000 != 0x8000
size := raw &^ 0x8000
br.headers = append(br.headers, &metadata{
raw: raw,
size: size,
compressed: compressed,
})
return nil
}
func (br *metadataReader) readNextDataBlock() error {
meta := br.headers[len(br.headers)-1]
r := io.NewSectionReader(br.s.r, br.offset, int64(meta.size))
if meta.compressed {
byts, err := br.s.decompressor.Decompress(r)
if err != nil {
return err
}
br.offset += int64(meta.size)
br.data = append(br.data, byts...)
return nil
}
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
return err
}
br.offset += int64(meta.size)
br.data = append(br.data, buf.Bytes()...)
return nil
}
//Read reads bytes into the given byte slice. Returns the amount of data read.
func (br *metadataReader) Read(p []byte) (int, error) {
if br.readOffset+len(p) <= len(br.data) {
for i := 0; i < len(p); i++ {
p[i] = br.data[br.readOffset+i]
}
br.readOffset += len(p)
return len(p), nil
}
read := 0
for read < len(p) {
err := br.parseMetadata()
if err != nil {
br.readOffset += read
return read, err
}
err = br.readNextDataBlock()
if err != nil {
br.readOffset += read
return read, err
}
for ; read < len(p); read++ {
if br.readOffset+read < len(br.data) {
p[read] = br.data[br.readOffset+read]
} else {
break
}
}
}
br.readOffset += read
if read != len(p) {
return read, errors.New("Didn't read enough data")
}
return read, nil
}
//Seek will seek to the specified location (if possible). Seeking is relative to the uncompressed data.
//When io.SeekCurrent or io.SeekStart is set, if seeking would put the offset beyond the current cached data, it will try to read the next data blocks to accomodate. On a failure it will seek to the end of the data.
//When io.SeekEnd is set, it wil seek reletive to the currently cached data.
func (br *metadataReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekCurrent:
br.readOffset += int(offset)
for {
if br.readOffset < len(br.data) {
break
}
err := br.parseMetadata()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
err = br.readNextDataBlock()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
}
case io.SeekStart:
br.readOffset = int(offset)
for {
if br.readOffset < len(br.data) {
break
}
err := br.parseMetadata()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
err = br.readNextDataBlock()
if err != nil {
br.readOffset = len(br.data)
return int64(br.readOffset), err
}
}
case io.SeekEnd:
br.readOffset = len(br.data) - int(offset)
if br.readOffset < 0 {
br.readOffset = 0
return int64(br.readOffset), errors.New("Trying to seek to a negative value")
}
}
return int64(br.readOffset), nil
}
+18 -238
View File
@@ -1,257 +1,37 @@
package squashfs
import (
"encoding/binary"
"errors"
"io"
"math"
"time"
"github.com/CalebQ42/squashfs/internal/compression"
"github.com/CalebQ42/squashfs/internal/inode"
"github.com/CalebQ42/squashfs/internal/toreader"
squashfslow "github.com/CalebQ42/squashfs/low"
)
const (
magic = 0x73717368
)
var (
//ErrNoMagic is returned if the magic number in the superblock isn't correct.
errNoMagic = errors.New("Magic number doesn't match. Either isn't a squashfs or corrupted")
//ErrIncompatibleCompression is returned if the compression type in the superblock doesn't work.
errIncompatibleCompression = errors.New("Compression type unsupported")
//ErrCompressorOptions is returned if compressor options is present. It's not currently supported.
errCompressorOptions = errors.New("Compressor options is not currently supported")
//ErrOptions is returned when compression options that I haven't tested is set. When this is returned, the Reader is also returned.
ErrOptions = errors.New("Possibly incompatible compressor options")
)
//Reader processes and reads a squashfs archive.
type Reader struct {
r io.ReaderAt
super superblock
flags superblockFlags
decompressor compression.Decompressor
fragOffsets []uint64
idTable []uint32
FS
Low squashfslow.Reader
}
//NewSquashfsReader returns a new squashfs.Reader from an io.ReaderAt
func NewSquashfsReader(r io.ReaderAt) (*Reader, error) {
hasUnsupportedOptions := false
var rdr Reader
rdr.r = r
err := binary.Read(io.NewSectionReader(rdr.r, 0, int64(binary.Size(rdr.super))), binary.LittleEndian, &rdr.super)
func NewReader(r io.ReaderAt) (Reader, error) {
rdr, err := squashfslow.NewReader(r)
if err != nil {
return nil, err
return Reader{}, err
}
if rdr.super.Magic != magic {
return nil, errNoMagic
out := Reader{
Low: rdr,
}
// if rdr.super.BlockLog == uint16(math.Log2(float64(rdr.super.BlockSize))) {
// return nil, errors.New("BlockSize and BlockLog doesn't match. The archive is probably corrupt")
// }
rdr.flags = rdr.super.GetFlags()
if rdr.flags.CompressorOptions {
switch rdr.super.CompressionType {
case gzipCompression:
gzip, err := compression.NewGzipCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if gzip.HasCustomWindow || gzip.HasStrategies {
hasUnsupportedOptions = true
}
rdr.decompressor = gzip
case xzCompression:
xz, err := compression.NewXzCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if xz.HasFilters {
return nil, errors.New("XZ compression options has filters. These are not yet supported")
}
rdr.decompressor = xz
case lz4Compression:
lz4, err := compression.NewLz4CompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 8))
if err != nil {
return nil, err
}
if lz4.HC {
hasUnsupportedOptions = true
}
rdr.decompressor = lz4
case zstdCompression:
zstd, err := compression.NewZstdCompressorWithOptions(io.NewSectionReader(rdr.r, int64(binary.Size(rdr.super)), 4))
if err != nil {
return nil, err
}
rdr.decompressor = zstd
default:
return nil, errCompressorOptions
}
} else {
switch rdr.super.CompressionType {
case gzipCompression:
rdr.decompressor = &compression.Gzip{}
case lzmaCompression:
rdr.decompressor = &compression.Lzma{}
case xzCompression:
rdr.decompressor = &compression.Xz{}
case lz4Compression:
rdr.decompressor = &compression.Lz4{}
case zstdCompression:
rdr.decompressor = &compression.Zstd{}
default:
//TODO: all compression types.
return nil, errIncompatibleCompression
}
out.FS = FS{
LowDir: rdr.Root,
r: &out,
}
fragBlocks := int(math.Ceil(float64(rdr.super.FragCount) / 512))
if fragBlocks > 0 {
offset := int64(rdr.super.FragTableStart)
for i := 0; i < fragBlocks; i++ {
tmp := make([]byte, 8)
_, err = r.ReadAt(tmp, offset)
if err != nil {
return nil, err
}
rdr.fragOffsets = append(rdr.fragOffsets, binary.LittleEndian.Uint64(tmp))
offset += 8
}
}
unread := rdr.super.IDCount
blockOffsets := make([]uint64, int(math.Ceil(float64(rdr.super.IDCount)/2048)))
for i := range blockOffsets {
secRdr := io.NewSectionReader(r, int64(rdr.super.IDTableStart)+(8*int64(i)), 8)
err = binary.Read(secRdr, binary.LittleEndian, &blockOffsets[i])
if err != nil {
return nil, err
}
idRdr, err := rdr.newMetadataReader(int64(blockOffsets[i]))
if err != nil {
return nil, err
}
read := uint16(math.Min(float64(unread), 2048))
for i := uint16(0); i < read; i++ {
var tmp uint32
err = binary.Read(idRdr, binary.LittleEndian, &tmp)
if err != nil {
return nil, err
}
rdr.idTable = append(rdr.idTable, tmp)
}
unread -= read
}
if hasUnsupportedOptions {
return &rdr, ErrOptions
}
return &rdr, nil
return out, nil
}
func NewReaderAtOffset(r io.ReaderAt, offset int64) (Reader, error) {
return NewReader(toreader.NewOffsetReader(r, offset))
}
//ModTime is the last time the file was modified/created.
func (r *Reader) ModTime() time.Time {
return time.Unix(int64(r.super.CreationTime), 0)
}
//ExtractTo tries to extract ALL files to the given path. This is the same as getting the root folder and extracting that.
func (r *Reader) ExtractTo(path string) []error {
root, err := r.GetRootFolder()
if err != nil {
return []error{err}
}
return root.ExtractTo(path)
}
//GetRootFolder returns a squashfs.File that references the root directory of the squashfs archive.
func (r *Reader) GetRootFolder() (root *File, err error) {
root = new(File)
mr, err := r.newMetadataReaderFromInodeRef(r.super.RootInodeRef)
if err != nil {
return nil, err
}
root.in, err = inode.ProcessInode(mr, r.super.BlockSize)
if err != nil {
return nil, err
}
root.path = "/"
root.filType = root.in.Type
root.r = r
return root, nil
}
//GetAllFiles returns a slice of ALL files and folders contained in the squashfs.
func (r *Reader) GetAllFiles() (fils []*File, err error) {
root, err := r.GetRootFolder()
if err != nil {
return nil, err
}
return root.GetChildrenRecursively()
}
//FindFile returns the first file (in the same order as Reader.GetAllFiles) that the given function returns true for. Returns nil if nothing is found.
func (r *Reader) FindFile(query func(*File) bool) *File {
root, err := r.GetRootFolder()
if err != nil {
return nil
}
fils, err := root.GetChildren()
if err != nil {
return nil
}
var childrenDirs []*File
for _, fil := range fils {
if query(fil) {
return fil
}
if fil.IsDir() {
childrenDirs = append(childrenDirs, fil)
}
}
for len(childrenDirs) != 0 {
var tmp []*File
for _, dirs := range childrenDirs {
chil, err := dirs.GetChildren()
if err != nil {
return nil
}
for _, child := range chil {
if query(child) {
return child
}
if child.IsDir() {
tmp = append(tmp, child)
}
}
}
childrenDirs = tmp
}
return nil
}
//FindAll returns all files where the given function returns true.
func (r *Reader) FindAll(query func(*File) bool) (all []*File) {
root, err := r.GetRootFolder()
if err != nil {
return nil
}
fils, err := root.GetChildrenRecursively()
if err != nil {
return nil
}
for _, fil := range fils {
if query(fil) {
all = append(all, fil)
}
}
return
}
//GetFileAtPath will return the file at the given path. If the file cannot be found, will return nil.
func (r *Reader) GetFileAtPath(path string) *File {
dir, err := r.GetRootFolder()
if err != nil {
return nil
}
return dir.GetFileAtPath(path)
return time.Unix(int64(r.Low.Superblock.ModTime), 0)
}
-132
View File
@@ -1,132 +0,0 @@
package squashfs
import (
"io"
"net/http"
"os"
"testing"
goappimage "github.com/CalebQ42/GoAppImage"
)
const (
downloadURL = "https://github.com/Swordfish90/cool-retro-term/releases/download/1.1.1/Cool-Retro-Term-1.1.1-x86_64.AppImage"
appImageName = "Cool-Retro-Term.AppImage"
squashfsName = "airootfs.sfs" //testing with a ArchLinux root fs from the live img
)
//Right now, don't use. Arch linux sfs uses XZ compression with filters, which isn't supported
func TestSquashfs(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
squashFil, err := os.Open(wd + "/testing/" + squashfsName)
if err != nil {
t.Fatal(err)
}
rdr, err := NewSquashfsReader(squashFil)
if err != nil {
t.Fatal(err)
}
os.RemoveAll(wd + "/testing/" + squashfsName + ".d")
root, _ := rdr.GetRootFolder()
errs := root.ExtractWithOptions(wd+"/testing/"+squashfsName+".d", false, false, os.ModePerm, true)
t.Fatal(errs)
}
func TestAppImage(t *testing.T) {
t.Parallel()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
aiFil, err := os.Open(wd + "/testing/" + appImageName)
if os.IsNotExist(err) {
downloadTestAppImage(t, wd+"/testing")
aiFil, err = os.Open(wd + "/testing/" + appImageName)
if err != nil {
t.Fatal(err)
}
} else if err != nil {
t.Fatal(err)
}
defer aiFil.Close()
stat, _ := aiFil.Stat()
ai := goappimage.NewAppImage(wd + "/testing/" + appImageName)
rdr, err := NewSquashfsReader(io.NewSectionReader(aiFil, ai.Offset, stat.Size()-ai.Offset))
if err != nil {
t.Fatal(err)
}
errs := rdr.ExtractTo(wd + "/testing/cool-retro")
if len(errs) > 0 {
t.Fatal(errs)
}
// os.RemoveAll(wd + "/testing/" + appImageName + ".d")
// root, _ := rdr.GetRootFolder()
// errs := root.ExtractWithOptions(wd+"/testing/"+appImageName+".d", true, os.ModePerm, true)
// t.Fatal(errs)
t.Fatal("No problemo!")
}
func downloadTestAppImage(t *testing.T, dir string) {
//seems to time out on slow connections. Might fix that at some point... or not. It's just a test...
os.Mkdir(dir, os.ModePerm)
appImage, err := os.Create(dir + "/" + appImageName)
if err != nil {
t.Fatal(err)
}
defer appImage.Close()
check := http.Client{
CheckRedirect: func(r *http.Request, _ []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
}
resp, err := check.Get(downloadURL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
_, err = io.Copy(appImage, resp.Body)
if err != nil {
t.Fatal(err)
}
}
func TestCreateSquashFromAppImage(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
err = os.Mkdir(wd+"/testing", 0777)
if err != nil && !os.IsExist(err) {
t.Fatal(err)
}
_, err = os.Open(wd + "/testing/" + appImageName)
if os.IsNotExist(err) {
downloadTestAppImage(t, wd+"/testing")
_, err = os.Open(wd + "/testing/" + appImageName)
if err != nil {
t.Fatal(err)
}
} else if err != nil {
t.Fatal(err)
}
ai := goappimage.NewAppImage(wd + "/testing/" + appImageName)
aiFil, err := os.Open(wd + "/testing/" + appImageName)
if err != nil {
t.Fatal(err)
}
defer aiFil.Close()
aiFil.Seek(ai.Offset, 0)
os.Remove(wd + "/testing/" + appImageName + ".squashfs")
aiSquash, err := os.Create(wd + "/testing/" + appImageName + ".squashfs")
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(aiSquash, aiFil)
if err != nil {
t.Fatal(err)
}
}
+209
View File
@@ -0,0 +1,209 @@
package squashfs
//Actually proper tests go here.
import (
"errors"
"io"
"io/fs"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
)
const (
squashfsURL = "https://darkstorm.tech/files/LinuxPATest.sfs"
squashfsName = "tensorflow.sqfs"
)
func preTest(dir string) (fil *os.File, err error) {
fil, err = os.Open(filepath.Join(dir, squashfsName))
if err != nil {
_, err = os.Open(dir)
if os.IsNotExist(err) {
err = os.Mkdir(dir, 0755)
}
if err != nil {
return
}
os.Remove(filepath.Join(dir, squashfsName))
fil, err = os.Create(filepath.Join(dir, squashfsName))
if err != nil {
return
}
var resp *http.Response
resp, err = http.DefaultClient.Get(squashfsURL)
if err != nil {
return
}
_, err = io.Copy(fil, resp.Body)
if err != nil {
return
}
}
_, err = exec.LookPath("unsquashfs")
if err != nil {
return
}
_, err = exec.LookPath("mksquashfs")
return
}
func TestMisc(t *testing.T) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
rdr, err := NewReader(fil)
if err != nil {
t.Fatal(err)
}
_ = rdr
// Put testing here
// t.Fatal("UM")
}
func BenchmarkExtract(b *testing.B) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
b.Fatal(err)
}
libPath := filepath.Join(tmpDir, "ExtractLib")
os.RemoveAll(libPath)
rdr, err := NewReader(fil)
if err != nil {
b.Fatal(err)
}
err = rdr.ExtractWithOptions(libPath, FastOptions())
if err != nil {
b.Fatal(err)
}
}
func BenchmarkRace(b *testing.B) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
b.Fatal(err)
}
libPath := filepath.Join(tmpDir, "ExtractLib")
unsquashPath := filepath.Join(tmpDir, "ExtractSquashfs")
os.RemoveAll(libPath)
os.RemoveAll(unsquashPath)
var libTime, unsquashTime time.Duration
start := time.Now()
rdr, err := NewReader(fil)
if err != nil {
b.Fatal(err)
}
err = rdr.ExtractWithOptions(libPath, FastOptions())
if err != nil {
b.Fatal(err)
}
libTime = time.Since(start)
cmd := exec.Command("unsquashfs", "-q", "-d", unsquashPath, fil.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
start = time.Now()
err = cmd.Run()
if err != nil {
b.Log("Unsquashfs error:", err)
}
unsquashTime = time.Since(start)
b.Log("Library took:", libTime.Round(time.Millisecond))
b.Log("unsquashfs took:", unsquashTime.Round(time.Millisecond))
b.Log("unsquashfs is", strconv.FormatFloat(float64(libTime.Milliseconds())/float64(unsquashTime.Milliseconds()), 'f', 2, 64), "times faster")
}
func TestExtractQuick(t *testing.T) {
//First, setup everything and extract the archive using the library and unsquashfs
// tmpDir := bTempDir()
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
libPath := filepath.Join(tmpDir, "ExtractLib")
unsquashPath := filepath.Join(tmpDir, "ExtractSquashfs")
os.RemoveAll(libPath)
os.RemoveAll(unsquashPath)
rdr, err := NewReader(fil)
if err != nil {
t.Fatal(err)
}
os.RemoveAll(filepath.Join(tmpDir, "testLog.txt"))
logFil, _ := os.Create(filepath.Join(tmpDir, "testLog.txt"))
op := FastOptions()
op.Verbose = true
op.IgnorePerm = true
op.LogOutput = logFil
err = rdr.ExtractWithOptions(libPath, op)
if err != nil {
t.Fatal(err)
}
cmd := exec.Command("unsquashfs", "-d", unsquashPath, fil.Name())
err = cmd.Run()
if err != nil {
t.Fatal(err)
}
//Then compare the sizes and existance between the two (using unsquashfs as a reference).
//If the file doesn't exist, or the size is different, we exit.
//TODO: Add long test that checks contents.
squashFils := os.DirFS(unsquashPath)
err = fs.WalkDir(squashFils, ".", func(path string, _ fs.DirEntry, _ error) error {
libFil, e := os.Open(filepath.Join(libPath, path))
if e != nil {
return e
}
sfsFile, e := os.Open(filepath.Join(unsquashPath, path))
if e != nil {
return e
}
sfsStat, _ := sfsFile.Stat()
libStat, _ := libFil.Stat()
if sfsStat.Size() != libStat.Size() {
t.Log(libFil.Name(), "not the same size between library and unsquashfs")
t.Log("File is", libStat.Size())
t.Log("Should be", sfsStat.Size())
return errors.New("file not the correct size")
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
var filePath = "usr/sbin/add-shell"
func TestSingleFile(t *testing.T) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
os.RemoveAll("testing/stuff")
rdr, err := NewReader(fil)
if err != nil {
t.Fatal(err)
}
f, err := rdr.Open(filePath)
if err != nil {
t.Fatal(err)
}
op := DefaultOptions()
op.Verbose = true
err = f.(*File).ExtractWithOptions("testing/stuff", op)
if err != nil {
t.Fatal(err)
}
}
-67
View File
@@ -1,67 +0,0 @@
package squashfs
const (
gzipCompression = 1 + iota
lzmaCompression
lzoCompression
xzCompression
lz4Compression
zstdCompression
)
//Superblock contains important information about a squashfs file. Located at the very front of the archive.
type superblock struct {
Magic uint32
InodeCount uint32
CreationTime uint32
BlockSize uint32
FragCount uint32
CompressionType uint16
BlockLog uint16
Flags uint16
IDCount uint16
MajorVersion uint16
MinorVersion uint16
RootInodeRef uint64
BytesUsed uint64
IDTableStart uint64
XattrTableStart uint64
InodeTableStart uint64
DirTableStart uint64
FragTableStart uint64
ExportTableStart uint64
}
//SuperblockFlags is the parsed version of Superblock.Flags
type superblockFlags struct {
UncompressedInodes bool
UncompressedData bool
Check bool
UncompressedFragments bool
NoFragments bool
AlwaysFragments bool
Duplicates bool
Exportable bool
UncompressedXattr bool
NoXattr bool
CompressorOptions bool
UncompressedIDs bool
}
//GetFlags returns a SuperblockFlags for a given superblock.
func (s *superblock) GetFlags() superblockFlags {
return superblockFlags{
UncompressedInodes: s.Flags&0x1 == 0x1,
UncompressedData: s.Flags&0x2 == 0x2,
Check: s.Flags&0x4 == 0x4,
UncompressedFragments: s.Flags&0x8 == 0x8,
NoFragments: s.Flags&0x10 == 0x10,
AlwaysFragments: s.Flags&0x20 == 0x20,
Duplicates: s.Flags&0x40 == 0x40,
Exportable: s.Flags&0x80 == 0x80,
UncompressedXattr: s.Flags&0x100 == 0x100,
NoXattr: s.Flags&0x200 == 0x200,
CompressorOptions: s.Flags&0x400 == 0x400,
UncompressedIDs: s.Flags&0x800 == 0x800,
}
}