') + ')', '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('^' + ".*" + ', '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" + ', '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('^' + ".*" + ', '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); } })(); })(); Tar: Use indexer setter instead of Add on ExtendedAttributes dictionary by jozkee · Pull Request #76404 · dotnet/runtime · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -720,39 +720,33 @@ static int CountDigits(int value)
// extended attributes. They get collected and saved in that dictionary, with no restrictions.
private void CollectExtendedAttributesFromStandardFieldsIfNeeded()
{
ExtendedAttributes.Add(PaxEaName, _name);
ExtendedAttributes[PaxEaName] = _name;
ExtendedAttributes[PaxEaMTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(_mTime);

if (!ExtendedAttributes.ContainsKey(PaxEaMTime))
{
ExtendedAttributes.Add(PaxEaMTime, TarHelpers.GetTimestampStringFromDateTimeOffset(_mTime));
}

if (!string.IsNullOrEmpty(_gName))
{
TryAddStringField(ExtendedAttributes, PaxEaGName, _gName, FieldLengths.GName);
}

if (!string.IsNullOrEmpty(_uName))
{
TryAddStringField(ExtendedAttributes, PaxEaUName, _uName, FieldLengths.UName);
}
TryAddStringField(ExtendedAttributes, PaxEaGName, _gName, FieldLengths.GName);
TryAddStringField(ExtendedAttributes, PaxEaUName, _uName, FieldLengths.UName);

if (!string.IsNullOrEmpty(_linkName))
Comment thread
carlossanlop marked this conversation as resolved.
{
ExtendedAttributes.Add(PaxEaLinkName, _linkName);
Debug.Assert(_typeFlag is TarEntryType.SymbolicLink or TarEntryType.HardLink);

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.

Nice.

ExtendedAttributes[PaxEaLinkName] = _linkName;
}

if (_size > 99_999_999)
{
ExtendedAttributes.Add(PaxEaSize, _size.ToString());
ExtendedAttributes[PaxEaSize] = _size.ToString();
}

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.

We could add an else here, in case the data stream gets reduced below the max allowed size, in which case, we can remove the PaxEaSize value from the dictionary entry.

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.

In my other PR which was replaced by this one, I had a fake stream implementation that simulated having 99_999_999 length. You can reuse that to test this.


// Adds the specified string to the dictionary if it's longer than the specified max byte length.
static void TryAddStringField(Dictionary<string, string> extendedAttributes, string key, string value, int maxLength)
// Sets the specified string to the dictionary if it's longer than the specified max byte length; otherwise, remove it.
static void TryAddStringField(Dictionary<string, string> extendedAttributes, string key, string? value, int maxLength)
{
if (Encoding.UTF8.GetByteCount(value) > maxLength)
if (string.IsNullOrEmpty(value) || GetUtf8TextLength(value) <= maxLength)
{
extendedAttributes.Remove(key);
}
else
{
extendedAttributes.Add(key, value);
extendedAttributes[key] = value;
}
}
}
Expand Down
15 changes: 14 additions & 1 deletion src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -481,7 +481,7 @@ protected TarEntryType GetTarEntryTypeForTarEntryFormat(TarEntryType entryType,
return entryType;
}

protected TarEntry InvokeTarEntryCreationConstructor(TarEntryFormat targetFormat, TarEntryType entryType, string entryName)
protected static TarEntry InvokeTarEntryCreationConstructor(TarEntryFormat targetFormat, TarEntryType entryType, string entryName)
=> targetFormat switch
{
TarEntryFormat.V7 => new V7TarEntry(entryType, entryName),
Expand DownExpand Up@@ -796,5 +796,18 @@ internal enum NameCapabilities
NameAndPrefix,
Unlimited
}

internal static void WriteTarArchiveWithOneEntry(Stream s, TarEntryFormat entryFormat, TarEntryType entryType)
{
using TarWriter writer = new(s, leaveOpen: true);

TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, "foo");
if (entryType == TarEntryType.SymbolicLink)
{
entry.LinkName = "bar";
}

writer.WriteEntry(entry);
}
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,5 +147,103 @@ public void UserNameGroupNameRoundtrips(TarEntryFormat entryFormat, bool unseeka
Assert.Equal(userGroupName, posixEntry.UserName);
Assert.Equal(userGroupName, posixEntry.GroupName);
}

[Theory]
[InlineData(TarEntryType.RegularFile)]
[InlineData(TarEntryType.Directory)]
[InlineData(TarEntryType.HardLink)]
[InlineData(TarEntryType.SymbolicLink)]
public void PaxExtendedAttributes_DoNotOverwritePublicProperties_WhenTheyFitOnLegacyFields(TarEntryType entryType)
{
Dictionary<string, string> extendedAttributes = new();
extendedAttributes[PaxEaName] = "ea_name";
extendedAttributes[PaxEaGName] = "ea_gname";
extendedAttributes[PaxEaUName] = "ea_uname";
extendedAttributes[PaxEaMTime] = GetTimestampStringFromDateTimeOffset(TestModificationTime);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
extendedAttributes[PaxEaLinkName] = "ea_linkname";
}

PaxTarEntry writeEntry = new PaxTarEntry(entryType, "name", extendedAttributes);
writeEntry.Name = new string('a', 100);
// GName and UName must be longer than 32 to be written as extended attribute.
writeEntry.GroupName = new string('b', 32);
writeEntry.UserName = new string('c', 32);
// There's no limit on MTime, we just ensure it roundtrips.
writeEntry.ModificationTime = TestModificationTime.AddDays(1);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
writeEntry.LinkName = new string('d', 100);
}

MemoryStream ms = new();
using (TarWriter w = new(ms, leaveOpen: true))
{
w.WriteEntry(writeEntry);
}
ms.Position = 0;

using TarReader r = new(ms);
PaxTarEntry readEntry = Assert.IsType<PaxTarEntry>(r.GetNextEntry());
Assert.Null(r.GetNextEntry());

Assert.Equal(writeEntry.Name, readEntry.Name);
Assert.Equal(writeEntry.GroupName, readEntry.GroupName);
Assert.Equal(writeEntry.UserName, readEntry.UserName);
Assert.Equal(writeEntry.ModificationTime, readEntry.ModificationTime);
Assert.Equal(writeEntry.LinkName, readEntry.LinkName);
}

[Theory]
[InlineData(TarEntryType.RegularFile)]
[InlineData(TarEntryType.Directory)]
[InlineData(TarEntryType.HardLink)]
[InlineData(TarEntryType.SymbolicLink)]
public void PaxExtendedAttributes_DoNotOverwritePublicProperties_WhenLargerThanLegacyFields(TarEntryType entryType)
{
Dictionary<string, string> extendedAttributes = new();
extendedAttributes[PaxEaName] = "ea_name";
extendedAttributes[PaxEaGName] = "ea_gname";
extendedAttributes[PaxEaUName] = "ea_uname";
extendedAttributes[PaxEaMTime] = GetTimestampStringFromDateTimeOffset(TestModificationTime);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
extendedAttributes[PaxEaLinkName] = "ea_linkname";
}

PaxTarEntry writeEntry = new PaxTarEntry(entryType, "name", extendedAttributes);
writeEntry.Name = new string('a', MaxPathComponent);
// GName and UName must be longer than 32 to be written as extended attribute.
writeEntry.GroupName = new string('b', 32 + 1);
writeEntry.UserName = new string('c', 32 + 1);
// There's no limit on MTime, we just ensure it roundtrips.
writeEntry.ModificationTime = TestModificationTime.AddDays(1);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
writeEntry.LinkName = new string('d', 100 + 1);
}

MemoryStream ms = new();
using (TarWriter w = new(ms, leaveOpen: true))
{
w.WriteEntry(writeEntry);
}
ms.Position = 0;

using TarReader r = new(ms);
PaxTarEntry readEntry = Assert.IsType<PaxTarEntry>(r.GetNextEntry());
Assert.Null(r.GetNextEntry());

Assert.Equal(writeEntry.Name, readEntry.Name);
Assert.Equal(writeEntry.GroupName, readEntry.GroupName);
Assert.Equal(writeEntry.UserName, readEntry.UserName);
Assert.Equal(writeEntry.ModificationTime, readEntry.ModificationTime);
Assert.Equal(writeEntry.LinkName, readEntry.LinkName);
}
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -450,5 +450,45 @@ public void WriteEntry_TooLongGroupName_Throws(TarEntryFormat entryFormat, strin

Assert.Throws<ArgumentException>("entry", () => writer.WriteEntry(entry));
}

public static IEnumerable<object[]> WriteEntry_UsingTarEntry_FromTarReader_IntoTarWriter_TheoryData()
{
foreach (var entryFormat in new[] { TarEntryFormat.V7, TarEntryFormat.Ustar, TarEntryFormat.Pax, TarEntryFormat.Gnu })
{
foreach (var entryType in new[] { entryFormat == TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile, TarEntryType.Directory, TarEntryType.SymbolicLink })
{
foreach (bool unseekableStream in new[] { false, true })
{
yield return new object[] { entryFormat, entryType, unseekableStream };
}
}
}
}

[Theory]
[MemberData(nameof(WriteEntry_UsingTarEntry_FromTarReader_IntoTarWriter_TheoryData))]
public void WriteEntry_UsingTarEntry_FromTarReader_IntoTarWriter(TarEntryFormat entryFormat, TarEntryType entryType, bool unseekableStream)
{
MemoryStream msSource = new();
MemoryStream msDestination = new();

WriteTarArchiveWithOneEntry(msSource, entryFormat, entryType);
msSource.Position = 0;

Stream source = new WrappedStream(msSource, msSource.CanRead, msSource.CanWrite, canSeek: !unseekableStream);
Stream destination = new WrappedStream(msDestination, msDestination.CanRead, msDestination.CanWrite, canSeek: !unseekableStream);

using (TarReader reader = new(source))
using (TarWriter writer = new(destination))
{
TarEntry entry;
while ((entry = reader.GetNextEntry()) != null)
{
writer.WriteEntry(entry);
}
}

AssertExtensions.SequenceEqual(msSource.ToArray(), msDestination.ToArray());
}
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,5 +96,103 @@ public async Task UserNameGroupNameRoundtripsAsync(TarEntryFormat entryFormat, b
Assert.Equal(userGroupName, posixEntry.UserName);
Assert.Equal(userGroupName, posixEntry.GroupName);
}

[Theory]
[InlineData(TarEntryType.RegularFile)]
[InlineData(TarEntryType.Directory)]
[InlineData(TarEntryType.HardLink)]
[InlineData(TarEntryType.SymbolicLink)]
public async Task PaxExtendedAttributes_DoNotOverwritePublicProperties_WhenTheyFitOnLegacyFieldsAsync(TarEntryType entryType)
{
Dictionary<string, string> extendedAttributes = new();
extendedAttributes[PaxEaName] = "ea_name";
extendedAttributes[PaxEaGName] = "ea_gname";
extendedAttributes[PaxEaUName] = "ea_uname";
extendedAttributes[PaxEaMTime] = GetTimestampStringFromDateTimeOffset(TestModificationTime);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
extendedAttributes[PaxEaLinkName] = "ea_linkname";
}

PaxTarEntry writeEntry = new PaxTarEntry(entryType, "name", extendedAttributes);
writeEntry.Name = new string('a', 100);
// GName and UName must be longer than 32 to be written as extended attribute.
writeEntry.GroupName = new string('b', 32);
writeEntry.UserName = new string('c', 32);
// There's no limit on MTime, we just ensure it roundtrips.
writeEntry.ModificationTime = TestModificationTime.AddDays(1);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
writeEntry.LinkName = new string('d', 100);
}

MemoryStream ms = new();
await using (TarWriter w = new(ms, leaveOpen: true))
{
await w.WriteEntryAsync(writeEntry);
}
ms.Position = 0;

await using TarReader r = new(ms);
PaxTarEntry readEntry = Assert.IsType<PaxTarEntry>(await r.GetNextEntryAsync());
Assert.Null(await r.GetNextEntryAsync());

Assert.Equal(writeEntry.Name, readEntry.Name);
Assert.Equal(writeEntry.GroupName, readEntry.GroupName);
Assert.Equal(writeEntry.UserName, readEntry.UserName);
Assert.Equal(writeEntry.ModificationTime, readEntry.ModificationTime);
Assert.Equal(writeEntry.LinkName, readEntry.LinkName);
}

[Theory]
[InlineData(TarEntryType.RegularFile)]
[InlineData(TarEntryType.Directory)]
[InlineData(TarEntryType.HardLink)]
[InlineData(TarEntryType.SymbolicLink)]
public async Task PaxExtendedAttributes_DoNotOverwritePublicProperties_WhenLargerThanLegacyFieldsAsync(TarEntryType entryType)
{
Dictionary<string, string> extendedAttributes = new();
extendedAttributes[PaxEaName] = "ea_name";
extendedAttributes[PaxEaGName] = "ea_gname";
extendedAttributes[PaxEaUName] = "ea_uname";
extendedAttributes[PaxEaMTime] = GetTimestampStringFromDateTimeOffset(TestModificationTime);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
extendedAttributes[PaxEaLinkName] = "ea_linkname";
}

PaxTarEntry writeEntry = new PaxTarEntry(entryType, "name", extendedAttributes);
writeEntry.Name = new string('a', MaxPathComponent);
// GName and UName must be longer than 32 to be written as extended attribute.
writeEntry.GroupName = new string('b', 32 + 1);
writeEntry.UserName = new string('c', 32 + 1);
// There's no limit on MTime, we just ensure it roundtrips.
writeEntry.ModificationTime = TestModificationTime.AddDays(1);

if (entryType is TarEntryType.HardLink or TarEntryType.SymbolicLink)
{
writeEntry.LinkName = new string('d', 100 + 1);
}

MemoryStream ms = new();
await using (TarWriter w = new(ms, leaveOpen: true))
{
await w.WriteEntryAsync(writeEntry);
}
ms.Position = 0;

await using TarReader r = new(ms);
PaxTarEntry readEntry = Assert.IsType<PaxTarEntry>(await r.GetNextEntryAsync());
Assert.Null(await r.GetNextEntryAsync());

Assert.Equal(writeEntry.Name, readEntry.Name);
Assert.Equal(writeEntry.GroupName, readEntry.GroupName);
Assert.Equal(writeEntry.UserName, readEntry.UserName);
Assert.Equal(writeEntry.ModificationTime, readEntry.ModificationTime);
Assert.Equal(writeEntry.LinkName, readEntry.LinkName);
}
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -379,5 +379,34 @@ public async Task WriteEntry_TooLongGroupName_Throws_Async(TarEntryFormat entryF

await Assert.ThrowsAsync<ArgumentException>("entry", () => writer.WriteEntryAsync(entry));
}

public static IEnumerable<object[]> WriteEntry_UsingTarEntry_FromTarReader_IntoTarWriter_Async_TheoryData()
=> TarWriter_WriteEntry_Tests.WriteEntry_UsingTarEntry_FromTarReader_IntoTarWriter_TheoryData();

[Theory]
[MemberData(nameof(WriteEntry_UsingTarEntry_FromTarReader_IntoTarWriter_Async_TheoryData))]
public async Task WriteEntry_UsingTarEntry_FromTarReader_IntoTarWriter_Async(TarEntryFormat entryFormat, TarEntryType entryType, bool unseekableStream)
{
using MemoryStream msSource = new();
using MemoryStream msDestination = new();

WriteTarArchiveWithOneEntry(msSource, entryFormat, entryType);
msSource.Position = 0;

Stream source = new WrappedStream(msSource, msSource.CanRead, msSource.CanWrite, canSeek: !unseekableStream);
Stream destination = new WrappedStream(msDestination, msDestination.CanRead, msDestination.CanWrite, canSeek: !unseekableStream);

await using (TarReader reader = new(source))
await using (TarWriter writer = new(destination))
{
TarEntry entry;
while ((entry = await reader.GetNextEntryAsync()) != null)
{
await writer.WriteEntryAsync(entry);
}
}

AssertExtensions.SequenceEqual(msSource.ToArray(), msDestination.ToArray());
}
}
}