diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetGroupName.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetGroupName.cs
index fadbf314e4d51..f36935ae7f39a 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetGroupName.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetGroupName.cs
@@ -7,19 +7,26 @@
using System;
using System.Collections.Generic;
using System.Reflection;
+using System.IO;
+using System.Diagnostics.CodeAnalysis;
internal static partial class Interop
{
internal static partial class Sys
{
///
- /// Gets the group name associated to the specified group ID.
+ /// Tries to get the group name associated to the specified group ID.
///
/// The group ID.
- /// On success, return a string with the group name. On failure, throws an IOException.
- internal static string GetGroupName(uint gid) => GetGroupNameInternal(gid) ?? throw GetIOException(GetLastErrorInfo());
+ /// When this method returns true, gets the value of the group name associated with the specified id. On failure, it is null.
+ /// On success, returns true. On failure, returns false.
+ internal static bool TryGetGroupName(uint gid, [NotNullWhen(returnValue: true)] out string? groupName)
+ {
+ groupName = GetGroupName(gid);
+ return groupName != null;
+ }
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetGroupName", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)]
- private static unsafe partial string? GetGroupNameInternal(uint uid);
+ private static unsafe partial string? GetGroupName(uint uid);
}
}
diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs
index 357e4a8a7587f..ccdc2b49b017c 100644
--- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs
+++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Unix.cs
@@ -82,8 +82,10 @@ private TarEntry ConstructEntryForWriting(string fullPath, string entryName, Fil
entry._header._gid = (int)status.Gid;
if (!_groupIdentifiers.TryGetValue(status.Gid, out string? gName))
{
- gName = Interop.Sys.GetGroupName(status.Gid);
- _groupIdentifiers.Add(status.Gid, gName);
+ if (Interop.Sys.TryGetGroupName(status.Gid, out gName))
+ {
+ _groupIdentifiers.Add(status.Gid, gName);
+ }
}
entry._header._gName = gName;
diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs
index cee5f6bc7dfd8..d5438928a59dd 100644
--- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs
@@ -23,7 +23,7 @@ public static IEnumerable GetFormatsAndSpecialFiles()
}
}
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[MemberData(nameof(GetFormatsAndSpecialFiles))]
public void Extract_SpecialFiles(TarEntryFormat format, TarEntryType entryType)
{
@@ -36,7 +36,7 @@ public void Extract_SpecialFiles(TarEntryFormat format, TarEntryType entryType)
Verify_Extract_SpecialFiles(destination, entry, entryType);
}
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[MemberData(nameof(GetFormatsAndSpecialFiles))]
public async Task Extract_SpecialFiles_Async(TarEntryFormat format, TarEntryType entryType)
{
diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
index 7a4aca955bf32..601fa85c3a23a 100644
--- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
@@ -12,6 +12,8 @@ namespace System.Formats.Tar.Tests
{
public abstract partial class TarTestsBase : FileCleanupTestBase
{
+ protected static bool IsRemoteExecutorSupportedAndPrivilegedProcess => RemoteExecutor.IsSupported && PlatformDetection.IsUnixAndSuperUser;
+
protected const string InitialEntryName = "InitialEntryName.ext";
protected readonly string ModifiedEntryName = "ModifiedEntryName.ext";
@@ -208,7 +210,6 @@ public enum TestTarFormat
// GNU formatted files. Format used by GNU tar versions up to 1.13.25.
gnu
}
- protected static bool IsRemoteExecutorSupportedAndOnUnixAndSuperUser => RemoteExecutor.IsSupported && PlatformDetection.IsUnixAndSuperUser;
protected static bool IsUnixButNotSuperUser => !PlatformDetection.IsWindows && !PlatformDetection.IsSuperUser;
@@ -707,7 +708,7 @@ internal static IEnumerable GetNamesNonAsciiTestData(NameCapabilities ma
// this is 256 but is supported because prefix is not required to end in separator.
yield return Repeat(OneByteCharacter, 155) + Separator + Repeat(OneByteCharacter, 100);
- // non-ascii prefix + name
+ // non-ascii prefix + name
yield return Repeat(TwoBytesCharacter, 155 / 2) + Separator + Repeat(OneByteCharacter, 100);
yield return Repeat(FourBytesCharacter, 155 / 4) + Separator + Repeat(OneByteCharacter, 100);
diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs
index 8b613191b0ef8..4bdb470ce2ae0 100644
--- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
using System.IO;
using Xunit;
@@ -20,7 +21,7 @@ protected void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry)
if (entry is PosixTarEntry posix)
{
- string gname = Interop.Sys.GetGroupName(status.Gid);
+ Assert.True(Interop.Sys.TryGetGroupName(status.Gid, out string gname));
string uname = Interop.Sys.GetUserNameFromPasswd(status.Uid);
Assert.Equal(gname, posix.GroupName);
@@ -51,5 +52,49 @@ protected void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry)
}
}
}
+
+ protected int CreateGroup(string groupName)
+ {
+ Execute("groupadd", groupName);
+ return GetGroupId(groupName);
+ }
+
+ protected int GetGroupId(string groupName)
+ {
+ string standardOutput = Execute("getent", $"group {groupName}");
+ string[] values = standardOutput.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ return int.Parse(values[^1]);
+ }
+
+ protected void SetGroupAsOwnerOfFile(string groupName, string filePath) =>
+ Execute("chgrp", $"{groupName} {filePath}");
+
+
+ protected void DeleteGroup(string groupName) =>
+ Execute("groupdel", groupName);
+
+ private string Execute(string command, string arguments)
+ {
+ using Process p = new Process();
+
+ p.StartInfo.UseShellExecute = false;
+ p.StartInfo.FileName = command;
+ p.StartInfo.Arguments = arguments;
+ p.StartInfo.RedirectStandardOutput = true;
+ p.StartInfo.RedirectStandardError = true;
+
+ p.Start();
+ p.WaitForExit();
+
+ string standardOutput = p.StandardOutput.ReadToEnd();
+ string standardError = p.StandardError.ReadToEnd();
+
+ if (p.ExitCode != 0)
+ {
+ throw new IOException($"Error '{p.ExitCode}' when executing '{command} {arguments}'. Message: {standardError}");
+ }
+
+ return standardOutput;
+ }
}
}
diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs
index 5ca600f992f93..42bcf2c36db21 100644
--- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.File.Tests.Unix.cs
@@ -9,7 +9,7 @@ namespace System.Formats.Tar.Tests
{
public partial class TarWriter_WriteEntry_File_Tests : TarWriter_File_Base
{
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[InlineData(TarEntryFormat.Ustar)]
[InlineData(TarEntryFormat.Pax)]
[InlineData(TarEntryFormat.Gnu)]
@@ -51,7 +51,7 @@ public void Add_Fifo(TarEntryFormat format)
}, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
}
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[InlineData(TarEntryFormat.Ustar)]
[InlineData(TarEntryFormat.Pax)]
[InlineData(TarEntryFormat.Gnu)]
@@ -96,7 +96,7 @@ public void Add_BlockDevice(TarEntryFormat format)
}, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
}
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[InlineData(TarEntryFormat.Ustar)]
[InlineData(TarEntryFormat.Pax)]
[InlineData(TarEntryFormat.Gnu)]
@@ -139,5 +139,49 @@ public void Add_CharacterDevice(TarEntryFormat format)
}, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
}
+
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
+ [InlineData(TarEntryFormat.Ustar)]
+ [InlineData(TarEntryFormat.Pax)]
+ [InlineData(TarEntryFormat.Gnu)]
+ public void CreateEntryFromFileOwnedByNonExistentGroup(TarEntryFormat f)
+ {
+ RemoteExecutor.Invoke((string strFormat) =>
+ {
+ using TempDirectory root = new TempDirectory();
+
+ string fileName = "file.txt";
+ string filePath = Path.Join(root.Path, fileName);
+ File.Create(filePath).Dispose();
+
+ string groupName = Path.GetRandomFileName()[0..6];
+ int groupId = CreateGroup(groupName);
+
+ try
+ {
+ SetGroupAsOwnerOfFile(groupName, filePath);
+ }
+ finally
+ {
+ DeleteGroup(groupName);
+ }
+
+ using MemoryStream archive = new MemoryStream();
+ using (TarWriter writer = new TarWriter(archive, Enum.Parse(strFormat), leaveOpen: true))
+ {
+ writer.WriteEntry(filePath, fileName); // Should not throw
+ }
+ archive.Seek(0, SeekOrigin.Begin);
+
+ using (TarReader reader = new TarReader(archive, leaveOpen: false))
+ {
+ PosixTarEntry entry = reader.GetNextEntry() as PosixTarEntry;
+ Assert.NotNull(entry);
+ Assert.Equal(entry.GroupName, string.Empty);
+ Assert.Equal(groupId, entry.Gid);
+ Assert.Null(reader.GetNextEntry());
+ }
+ }, f.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
+ }
}
}
diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs
index ba4a5600ae81d..343c4f4e0ca2c 100644
--- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntryAsync.File.Tests.Unix.cs
@@ -10,7 +10,7 @@ namespace System.Formats.Tar.Tests
{
public partial class TarWriter_WriteEntryAsync_File_Tests : TarWriter_File_Base
{
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[InlineData(TarEntryFormat.Ustar)]
[InlineData(TarEntryFormat.Pax)]
[InlineData(TarEntryFormat.Gnu)]
@@ -55,7 +55,7 @@ public void Add_Fifo_Async(TarEntryFormat format)
}, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
}
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[InlineData(TarEntryFormat.Ustar)]
[InlineData(TarEntryFormat.Pax)]
[InlineData(TarEntryFormat.Gnu)]
@@ -103,7 +103,7 @@ public void Add_BlockDevice_Async(TarEntryFormat format)
}, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
}
- [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
[InlineData(TarEntryFormat.Ustar)]
[InlineData(TarEntryFormat.Pax)]
[InlineData(TarEntryFormat.Gnu)]
@@ -149,5 +149,49 @@ public void Add_CharacterDevice_Async(TarEntryFormat format)
}
}, format.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
}
+
+ [ConditionalTheory(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
+ [InlineData(TarEntryFormat.Ustar)]
+ [InlineData(TarEntryFormat.Pax)]
+ [InlineData(TarEntryFormat.Gnu)]
+ public void CreateEntryFromFileOwnedByNonExistentGroup_Async(TarEntryFormat f)
+ {
+ RemoteExecutor.Invoke(async (string strFormat) =>
+ {
+ using TempDirectory root = new TempDirectory();
+
+ string fileName = "file.txt";
+ string filePath = Path.Join(root.Path, fileName);
+ File.Create(filePath).Dispose();
+
+ string groupName = Path.GetRandomFileName()[0..6];
+ int groupId = CreateGroup(groupName);
+
+ try
+ {
+ SetGroupAsOwnerOfFile(groupName, filePath);
+ }
+ finally
+ {
+ DeleteGroup(groupName);
+ }
+
+ await using MemoryStream archive = new MemoryStream();
+ await using (TarWriter writer = new TarWriter(archive, Enum.Parse(strFormat), leaveOpen: true))
+ {
+ await writer.WriteEntryAsync(filePath, fileName); // Should not throw
+ }
+ archive.Seek(0, SeekOrigin.Begin);
+
+ await using (TarReader reader = new TarReader(archive, leaveOpen: false))
+ {
+ PosixTarEntry entry = await reader.GetNextEntryAsync() as PosixTarEntry;
+ Assert.NotNull(entry);
+ Assert.Equal(entry.GroupName, string.Empty);
+ Assert.Equal(groupId, entry.Gid);
+ Assert.Null(await reader.GetNextEntryAsync());
+ }
+ }, f.ToString(), new RemoteInvokeOptions { RunAsSudo = true }).Dispose();
+ }
}
}