Skip to content

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

@jozkee@danmoseley@carlossanlop@stephentoub@GSPP
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Use UTF8 encoding on Tar string fields by jozkee · Pull Request #75902 · dotnet/runtime · GitHub
Skip to content

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

@jozkee@danmoseley@carlossanlop@stephentoub@GSPP
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Use UTF8 encoding on Tar string fields by jozkee · Pull Request #75902 · dotnet/runtime · GitHub
Skip to content

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

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

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

@jozkee@danmoseley@carlossanlop@stephentoub@GSPP
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Use UTF8 encoding on Tar string fields by jozkee · Pull Request #75902 · dotnet/runtime · GitHub
Skip to content

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

@jozkee@danmoseley@carlossanlop@stephentoub@GSPP
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Use UTF8 encoding on Tar string fields by jozkee · Pull Request #75902 · dotnet/runtime · GitHub
Skip to content

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

@jozkee@danmoseley@carlossanlop@stephentoub@GSPP
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Use UTF8 encoding on Tar string fields by jozkee · Pull Request #75902 · dotnet/runtime · GitHub
Skip to content

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

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

Use UTF8 encoding on Tar string fields - #75902

Merged
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names
Sep 28, 2022
Merged

Use UTF8 encoding on Tar string fields#75902
jozkee merged 11 commits into
dotnet:mainfrom
jozkee:tar_utf8names

Conversation

@jozkee

@jozkeejozkee commented Sep 20, 2022

Copy link
Copy Markdown
Member

Fixes#75482
Fixes#75360
Fixes#75921

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #75482.

Author:Jozkee
Assignees:-
Labels:

area-System.IO

Milestone:-

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

int checksum = Checksum(destination);

if (utf16NameTruncatedLength < name.Length)

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 the expected behavior if truncatedName as UTF8 only takes up, say, 98 of the 100 bytes in the Name field and then we spill over to Prefix for the rest? How do tools know the extra two bytes in Name aren't actually part of the name?

@jozkeejozkeeSep 20, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the thing is, the prefix logic is wrong, I filed #75360 for it. I don't want to fix it here because of time constraint.

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.

It's not clear to me from that issue: is the problem with how we read/interpret name and prefix, or is the issue with how we write out the name and prefix, or both?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

the issue is how we write it.
We currently do this:

Ustar and PAX save in the name byte array only the bytes that fit, and the rest of that string is saved in the prefix field.

Which is actually a suffix.

Instead we should do this:

If the pathname is too long to fit in the 100 bytes provided by the standard format, it can be split at any / character with the first portion going into the prefix field. If the prefix field is not empty, the reader will prepend the prefix value and a / character to the regular name field to obtain the full pathname. The standard does not require a trailing / character on directory names, though most implementations still include this for compatibility reasons.

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.

Then if the code that's there is 100% wrong and the code in the PR it's being updated to is 100% wrong, why are we changing that code? Seems like we should either fix it in this PR, or as part of changing it in this PR, this should fail in a more reliable way rather than outputting corrupt data. Do we plan to fix this issue for 7.0 as well?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

IMO, we should fix it for 7.0 as well. Please see #75902 (comment).

Comment threadsrc/libraries/System.Formats.Tar/tests/TarTestsBase.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Could you describe the interop testing you're planning to do for this? Trying with various tar implementations. Might be worth a table.

@jozkee

Copy link
Copy Markdown
MemberAuthor

@danmoseley I wasn't planning on adding interop tests to System.Formats.Tar.Tests. I did play a bit with GNU Tar with all the formats we support, to see how they handled non-ascii characters and all formats are able to write non-ascii correctly.
I also tested groupname and username and checked with linux commands useradd and groupadd and both commands were able to handle non-ascii.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Last commit addresses the problem with ustar prefix (#75360) and partially addresses the truncation issue (#75921) as the name won't be truncated on formats that do not suppoort unlimited size names, I said "partially" because other fields are still being truncated (linkname, uname, gname). I was hoping that I could present an sketch of what fixing those issues would look like and extend the fix to the other fields once you agree with it.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
const int MaxPathname = FieldLengths.Prefix + 1 + FieldLengths.Name;

@stephentoubstephentoubSep 22, 2022

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 the +1 for? The separator between the values in the two fields? Worth a comment.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

For the separator between prefix and name, it can be neglected as the fields will be rejoined with one on read.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

if (_format is TarEntryFormat.V7 && name.Length != utf16NameTruncatedLength)
{
throw new ArgumentException(SR.TarEntryNameExceedsMaxLength, ArgNameEntry);

@jozkeejozkeeSep 22, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The semantic here is that TarWriter.WriteEntry[Async] throws if entry's Name exceeds max length. It could be better to throw on TarEntry.set_Name.
Let me know if you want me to change this behavior.

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.

Do we know all the constraints at that point? If so, I agree that validating the name when it's being set is better, though in practice I don't know what consumption patterns look like and whether it'll actually make a meaningful difference, e.g. if you're just using our higher-level TarFile APIs, the exception's going to come from the same place regardless of where we throw it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Do we know all the constraints at that point?

Yes, the only constraints are the entry format and the encoded size of the name. The only downside I see is that it will make this PR a bigger change.

Regarding the consumption patterns, I assume most people using TarWriter directly want to build a tar archive feeding it with a folder enumeration, and write it down to anything other than a FileStream.

@jozkee

Copy link
Copy Markdown
MemberAuthor

Updated PR to also fix the other two related issues, field truncation and ustar prefix logic (see description).

scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;

if (lastIdx < 1) // splitting at the root is not allowed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

// splitting at the root is not allowed.

I copied this behavior from GNU Tar, it does not store one single separtor in the prefix, I assume we should do the same.

No big deal to go the other way but "absolute path" is already very odd scenario for tar and I don't have a reason to not do the same.

On top of that, I think we should instruct users to be aware about/avoid using absolute paths with Tar APIs.

@carlossanlopcarlossanlop 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.

Thanks @jozkee for your help getting the 3 issues fixed at once. I left some suggestions and questions.

Comment on lines +921 to +923
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);

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.

I don't think this method is needed since it's only doing one thing. We can just call Encoding.UTF8.GetByteCount directly.

Suggested change
privatestaticintGetUtf8TextLength(ReadOnlySpan<char>text)
=>Encoding.UTF8.GetByteCount(text);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I just added it as it is a recurrent call and it is easier to read IMO.

@carlossanlopcarlossanlopSep 28, 2022

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.

I leave it to you as a nit. You can either remove them as I suggested, or make sure all the other call sites for Encoding.UTF8.GetByteCount in this same file call the method you're adding.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated

@carlossanlopcarlossanlop 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.

LGTM. Thank you for your help, @jozkee. I left a few extra nits, consider them optional and can be addressed later.

Comment threadsrc/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Outdated
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry);

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.

Suggested change
thrownewArgumentException(SR.TarEntryFieldExceedsMaxLength,ArgNameEntry);
thrownewArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength,nameof(TarEntry.Name)),ArgNameEntry);

insidious bug!

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.

if we had something like Debug.Assert(!Regex.IsMatch("{\d}", message) in the Exception base class constructor, would it catch bugs like this?

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

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.

Cc @stephentoub might it be worth it?

@stephentoubstephentoubSep 28, 2022

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.

You could experiment and see. It'd only catch things in builds where corelib is debug/checked, and if there's any case in all of dotnet/runtime where we validly construct an exception with such a message, it'd need to be reverted. I'd be surprised if there isn't at least one.

@jozkeejozkeeSep 28, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(It wouldn't catch something like throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, ArgNameEntry)); but that seems less likely)

You could compare the matches of {\d} against args.Length in SR.Format to also catch this case and the prior.

@jozkee
jozkee merged commit 620bc76 into dotnet:mainSep 28, 2022
@jozkee
jozkee deleted the tar_utf8names branch September 28, 2022 18:39
@carlossanlop

Copy link
Copy Markdown
Contributor

/backport to release/7.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3146104764

@ghostghost locked as resolved and limited conversation to collaborators Oct 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

5 participants

@jozkee@danmoseley@carlossanlop@stephentoub@GSPP