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 fadbf314e4d51f..f36935ae7f39ae 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 41c1b0bb9976f8..de3aeb31cb8920 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/TarWriter/TarWriter.File.Base.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Unix.cs index 8b613191b0ef87..4bdb470ce2ae04 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 f9dfde9f3bacd2..df9f842152c822 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 @@ -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 771f7ab2620e85..97d667f896b376 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 @@ -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(); + } } }