std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

std.tar: support pax headers and gnu_long{name,link} - #15382

Closed
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests
Closed

std.tar: support pax headers and gnu_long{name,link}#15382
archaistvolts wants to merge 6 commits into
ziglang:masterfrom
archaistvolts:tar-addtests

Conversation

@archaistvolts

@archaistvoltsarchaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
Contributor

tar.zig:

  • add HeaderIterator() type and convert pipeToFileSystem() to use it.
  • add tests parseNumeric and parsePaxTime ported from https://go.dev/src/archive/tar/strconv_test.go.
  • add initial support for options.executable_bit_only.
  • initial windows support:
    • skip symlinks which require admin rights
    • workaround file.updateTimes() panic by truncating file times

lib/std/compress/tar/testdata/

reader_test.zig:

test_decompress.zig:

  • runs tar.pipeToFileSystem() on valid testdata/ files.

@archaistvoltsarchaistvolts changed the title std.tar: support more features and add testsstd.tar: support pax headers and gnulong_{name,link}Apr 21, 2023
@archaistvoltsarchaistvolts changed the title std.tar: support pax headers and gnulong_{name,link}std.tar: support pax headers and gnu_long{name,link}Apr 21, 2023
@archaistvolts

archaistvolts commented Apr 21, 2023

Copy link
Copy Markdown
ContributorAuthor

addresses, maybe closes #14310
closes #15342#15222
supersedes #15228

@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 62eec46 to de62a00CompareApril 21, 2023 08:32
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 003656c to 03da105CompareApril 21, 2023 14:15
Comment threadlib/std/compress/tar/test_decompress.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 2 times, most recently from 89081a2 to 47dda50CompareApril 22, 2023 00:09
@archaistvolts

archaistvolts commented Apr 22, 2023

Copy link
Copy Markdown
ContributorAuthor

I have updated the testdata/ files. Previously they were .tar files and the testdata/ directory was around 136K. Now they are gzipped w/ -9 and total 6.9K (around 1/20 the size).

Also, tests now use @embedFile on fixed lists of .gz files rather than iterating the testdata/ directory and opening/reading files.

Comment threadlib/std/tar.zig Outdated
Comment threadlib/std/tar.zig
Comment threadlib/std/tar.zig

@truemediantruemedian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else here looks good.

Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from ad95885 to eb2ea02CompareApril 23, 2023 00:10
Comment threadlib/std/tar.zig Outdated
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 5 times, most recently from 5b68673 to 955ba4aCompareApril 23, 2023 06:27
@archaistvolts
archaistvoltsforce-pushed the tar-addtests branch 3 times, most recently from 8aa9cfe to 7767b61CompareJune 22, 2023 12:23
@alichraghialichraghi mentioned this pull request Jun 22, 2023
1 task
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

I don't think the aarch64-macos-debug ci failure is related to this PR. Seems to be an OOM.

From the log:

zig test ReleaseSafe native: error: memory usage peaked at 4572626944 bytes, exceeding the declared upper bound of 4080218931

@jacobly0

jacobly0 commented Jun 23, 2023

Copy link
Copy Markdown
Member

Yeah you're fine, fix is on master if you want to rebase. When that's the only error, all tests still passed.

archaistvolts added a commit to archaistvolts/zig that referenced this pull request Jun 23, 2023
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
@emidoots

Copy link
Copy Markdown

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

@andrewrk
andrewrk self-requested a review July 3, 2023 22:56
-- 4/20/23 --
tar.zig:
* add HeaderIterator() type and convert pipeToFileSystem() to use it.
* add initial support for options.executable_bit_only.
* initial windows support:
* skip symlinks which require admin rights
* workaround file.updateTimes() panic by truncating file times
* add tests parseNumeric and parsePaxTime ported from
https://go.dev/src/archive/tar/strconv_test.go.
lib/std/compress/tar/testdata/
* copy a subset of tar files from
https://go.dev/src/archive/tar/testdata
* gzip them all with -9. results in around 95% file size reduction
reader_test.zig:
* validate headers against files from testdata/. a port of
https://go.dev/src/archive/tar/reader_test.go.
test_decompress.zig:
* runs tar.pipeToFileSystem() on valid testdata/ files.
-------------
std.tar: cleanups and fixes
-- 4/22/23 --
tar.zig:
* convert V7Header, UstarHeader, StarHeader, GnuHeader to have fields of
byte arrays instead of accessor methods.
* Header.getFormat() - optimize by using 64 bit compares rather than
mem.eql().
* gnu_long{name,link} and pax headers - allow them to be longer than
512 bytes by adding an allocator param to pipeToFileSystem().
* symlinks: cleanup logic and skip if wasi
* remove custom toPosixPath() wasn't necessary.
* workaround header.name corruption issue after makeOpenPath() call
and add notes about it.
* skip setDirProperties() entrely. leave as a TODO.
src/Package.zig: pass gpa to pipeToFileSystem()
tests: minor cosmetic changes
-------------
std.tar: fix memory errors
-- 4/25/23 --
tar.zig:
* previously HeaderIterator.header()'s v7 param was by value, causing it
to return invalid pointers to locals for various string fields. now
the param along with a few others have been made *const. this solves
the perceived 'stack corruption' i thought i was seeing. because v7
is 512 bytes, it wasn't clobbered until a fn w/ a larger stack frame,
dir.makeOpenPath -> dir.openDir -> os.toPosixPath, was called.
* symlinks - check for and set flags.is_directory param
-------------
std.tar: hardlinks, more cleanup
-- 4/25/23 --
tar.zig:
* properly handle hardlinks by copying files.
* improve symlink is_directory check - replace dir.access() call with
dir.openFile() + handle error.FileNotFound.
* isValidPax(): optimize: replace mem.eql()s with ComptimeStringMap.
tests:
* add testdata/hardlink.tar.gz
-------------
std.tar: more cleanup
-- 4/25/23 --
tests: add testdata/dir-symlink.tar
-------------
this patch converts all the crashes submitted by @squeek502
(in ziglang#15382 (comment))
to errors.
HeaderIterator:
* add bounds checks to PaxIterator.next()
* convert several unsafe int casts to safe ones
misc:
* added FileType.tagName() for debugging which returns null for unnamed
enum values
* make unixTime() fallible
prevent crashes and return errors when:
* any 'named type' header's file path contains a `NUL` character.
* prevent std.bit_set assertion failure when `header.type` is outside
the bounds of FileType.named_types_bitset.
* return error when header.size is negative via math.cast
* readBlocks(): change 'size' param from u64 to usize to avoid
unnecessary @intcast
* std.math -> math
* builtins
* mem.alignForward
* fs.file.File.Kind
@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

@travisstaloch looks like you have some more merge conflicts - just thought I'd mention as I am very interested in this PR.

thanks for the heads up. just rebased and pushed.

@andrewrk

Copy link
Copy Markdown
Member

Thanks for keeping this up to date. I'm sorry for taking so long with the review but I want you to know that it's on my mind, definitely looking to get it landed before the release.

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

Thanks for the update. No worries, I know there is a lot to do.

@emidoots

emidoots commented Jul 22, 2023

Copy link
Copy Markdown

Not helpful-I know, but wanted to mention this is the #1 most impactful thing for us in terms of being able to get rid of submodules in Mach. We have a handful of packages ready-to-go but we just can't depend on them yet due to std.tar not being able to extract the archive.

@andrewrkandrewrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this and diligently keeping it rebased on top of master.

This code is not up to standards. It is rife with code smells, it looks like it blindly ports Go logic rather than building a tar implementation on first principles, it abuses comptime logic and generics for no reason, which causes code bloat and reduces compilation speeds, and it regresses the API, adding an allocator requirement with no documented justification.

Furthermore, I see no discussion of the performance delta on the PR writeup.

I'm sorry, but this is not going to make it into 0.11.0.

As a path forward, I suggest to start with incremental improvements to std.tar, rather than a +2,000 line opinionated change that takes things in many different directions at once.

Comment threadlib/std/tar.zig
Comment on lines +51 to +53
return inline for (std.meta.fields(FileType)) |f| {
if (@intFromEnum(ft) == f.value) break f.name;
} else null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an inline switch instead.

Comment threadlib/std/tar.zig
Comment on lines +280 to +317
/// merges key-value pair `kv` into hdr if its a valid PAX field.
/// TODO merge PAX schilly xattrs
pub fn mergePax(kv: [2][]const u8, hdr: *Header) !void {
const k = kv[0];
const v = kv[1];
log.debug("mergePax k={s} v={s}", .{ k, v });
if (v.len == 0) return;

pub fn is_ustar(header: Header) bool {
return std.mem.eql(u8, header.bytes[257..][0..6], "ustar\x00");
const map = std.ComptimeStringMap(std.meta.FieldEnum(Header), .{
.{ Pax.path, .name },
.{ Pax.linkpath, .linkname },
.{ Pax.uname, .uname },
.{ Pax.gname, .gname },
.{ Pax.uid, .uid },
.{ Pax.gid, .gid },
.{ Pax.atime, .atime },
.{ Pax.mtime, .mtime },
.{ Pax.ctime, .ctime },
.{ Pax.size, .size },
});

if (map.get(k)) |field_enum| switch (field_enum) {
.name => hdr.name = v,
.linkname => hdr.linkname = v,
.uname => hdr.uname = v,
.gname => hdr.gname = v,
.uid => hdr.uid = @truncate(try fmt.parseInt(i64, v, 10)),
.gid => hdr.gid = @truncate(try fmt.parseInt(i64, v, 10)),
.atime => hdr.atime = try parsePaxTime(v),
.ctime => hdr.ctime = try parsePaxTime(v),
.mtime => hdr.mtime = try parsePaxTime(v),
.size => hdr.size = try fmt.parseInt(i64, v, 10),
else => unreachable,
} else {
// TODO merge PAX schilly xattrs
// log.debug("TODO handle pax header key={s}", .{k});
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this doing here? Looks incomplete and it has log statements.

Comment threadlib/std/tar.zig
},

else => {
hdr.merge(pax_hdr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this, why are we merging stuff?

Comment threadlib/std/tar.zig
hdr = try self.header(v7) orelse return null;

format.setIntersection(hdr.fmt);
log.debug("hdr={}", .{hdr});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unscoped log statement. Generally I think std lib code should not have these

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm not planning to leave these log statements in. I just didn't remove them yet anticipating more reviews and avoiding ci churn.

Comment threadlib/std/tar.zig
.normal;
}

// Set the final guess at the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we guessing? don't guess

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand all the details the tar format. My understanding it that It is messy and attempts to support old extensions. Maybe someone else can help explain why this guessing is necessary.

Here are some references.

https://www.gnu.org/software/tar/manual/html_node/Standard.html
https://go.dev/src/archive/tar/common.go#L201
https://go.dev/src/archive/tar/format.go#L169

Comment threadlib/std/tar.zig

// Set the final guess at the format.
if (format.contains(.ustar) and format.contains(.pax))
format.setIntersection(fmt_ustar);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what in the hell is going on here

Comment threadsrc/Package.zig
defer decompress.deinit();

try std.tar.pipeToFileSystem(out_dir, decompress.reader(), .{
try std.tar.pipeToFileSystem(gpa, out_dir, decompress.reader(), .{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booooooo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. But if we want to support long file names, it is necessary to have an allocator. See @truemedian's comment #15382 (comment)

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@travisstaloch it might be possible to use a buffer of size std.fs.MAX_NAME_BYTES and read the path one-component-at-a-time (erroring if any component is > MAX_NAME_BYTES long (when encoded as UTF-8) since it shouldn't be possible to create it on the filesystem anyway), using the Dir of the last created component to create the next component and so on.

EDIT: Unless there's a reason to need to store the full path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_NAME_BYTES may work, but it needs to be able to reasonably handle windows systems (where such value is 260). You can't use the directory of the last component because tar files aren't necessarily laid out as a tree, you always have to create the file from the extraction root.

@squeek502squeek502Jul 29, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

260 is MAX_PATH on Windows (and is irrelevant to Zig since we use extended-length path APIs exclusively). See std.os.windows.NAME_MAX and std.fs.MAX_NAME_BYTES.

I'm not sure what you mean by the second part. If each file in a tar has a filepath from the extraction root, then, for example:

some-long-component/some-long-component/<... repeated however many times ...>/foo.txt

I'm suggesting something like:

  • Read into [MAX_NAME_BYTES]u8 until either you hit a path separator or run out of room in the buffer (in which case, fatal error since the component won't be create-able on the filesystem)
  • Use a temporary buffer of one byte to check if this is the last component (read into it until you hit a non-path-separator or end-of-filepath)
  • If this is not the last component, create/open it as a directory (using something like Dir.makePath with the extraction root's Dir or the last created component's Dir)
    • If this is the last component, create it as a file and move to the next path in the tar
  • If there are more components to read, put the byte from the temporary buffer into the [MAX_NAME_BYTES]u8 buffer and repeat from the start

Might need some refining but that's the idea (if there's not something I'm missing that makes it impossible).

Comment on lines +1 to +4
test {
_ = @import("tar/reader_test.zig");
_ = @import("tar/test_decompress.zig");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this file, this looks like a big kludge

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

it looks like it blindly ports Go logic rather than building a tar implementation on first principles

You're right. It does blindly port Go logic. As much as I might want to help, I don't think I'm capable of creating a tar implementation from first principles. I'll step aside and let someone else work on this. Feel free continue with this PR or use anything from it in a different PR.

@andrewrk

Copy link
Copy Markdown
Member

Thank you and I apologize for the harsh words. I know a lot of people are counting on this improvement

@archaistvolts

Copy link
Copy Markdown
ContributorAuthor

No worries. I support the desire for high quality code. And understand the desire for smaller PRs. I'll continue to do what I can to help with std.tar. I just don't feel confident in my ability to create a from scratch implementation.

@andrewrk

Copy link
Copy Markdown
Member

Understood. I've been working on the release notes and your name has come up quite a few times. This release is regardless going to be much better off because of you.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@archaistvolts@squeek502@motiejus@jacobly0@emidoots@andrewrk@rohlem@truemedian