Created
March 12, 2024 18:25
-
-
Save awakecoding/b32afd9b13ac76e231b1a8bc40c4a272 to your computer and use it in GitHub Desktop.
Set zip file unix permissions (useful to set execute bit on executables without using chmod +x)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env pwsh | |
param( | |
[Parameter(Position = 0, Mandatory = $true)] | |
[string] $ZipFilePath, | |
[Parameter(Position = 1, Mandatory = $true)] | |
[string] $FilePattern, | |
[Parameter(Position = 2, Mandatory = $true)] | |
[string] $FilePermissions | |
) | |
Add-Type -TypeDefinition @" | |
using System; | |
using System.IO; | |
using System.Runtime.InteropServices; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
public class ZipFileHelper | |
{ | |
// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT | |
// https://games.greggman.com/game/zip-rant/ | |
public const uint ZipLocalFileHeaderSignature = 0x04034b50; | |
public const uint ZipLocalFileHeaderSize = 30; | |
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 30)] | |
public struct ZipLocalFileHeader | |
{ | |
public uint signature; | |
public ushort version; | |
public ushort bitflags; | |
public ushort compressionMethod; | |
public ushort lastModFileTime; | |
public ushort lastModFileDate; | |
public uint crc32; | |
public uint compressedSize; | |
public uint uncompressedSize; | |
public ushort fileNameLength; | |
public ushort extraFieldLength; | |
} | |
public const uint ZipCentralFileHeaderSignature = 0x02014b50; | |
public const uint ZipCentralFileHeaderSize = 46; | |
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 46)] | |
public struct ZipCentralFileHeader | |
{ | |
public uint signature; | |
public ushort versionUsed; | |
public ushort versionRequired; | |
public ushort bitflags; | |
public ushort compressionMethod; | |
public ushort lastModeFileTime; | |
public ushort lastModFileDate; | |
public uint crc32; | |
public uint compressedSize; | |
public uint uncompressedSize; | |
public ushort fileNameLength; | |
public ushort extraFieldLength; | |
public ushort fileCommentLength; | |
public ushort diskNumberStart; | |
public ushort internalFileAttributes; | |
public uint externalFileAttributes; | |
public uint relativeOffsetOfLocalHeader; | |
} | |
public const uint ZipEndOfCentralDirHeaderSignature = 0x06054b50; | |
public const uint ZipEndOfCentralDirHeaderSize = 22; | |
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 22)] | |
public struct ZipEndOfCentralDirHeader | |
{ | |
public uint signature; | |
public ushort diskNumberCurrent; | |
public ushort diskNumberCentral; | |
public ushort diskEntryCountCurrent; | |
public ushort diskEntryCountCentral; | |
public uint centralDirSize; | |
public uint centralDirOffset; | |
public ushort fileCommentLength; | |
} | |
// https://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute | |
public const int S_IFIFO = 0x1000; // named pipe (fifo) | |
public const int S_IFCHR = 0x2000; // character special | |
public const int S_IFDIR = 0x4000; // directory | |
public const int S_IFBLK = 0x6000; // block special | |
public const int S_IFREG = 0x8000; // regular | |
public const int S_IFLNK = 0xA000; // symbolic link | |
public const int S_IFSOCK = 0xC000; // socket | |
public const int S_ISUID = 0x800; // set user id on execution | |
public const int S_ISGID = 0x400; // set group id on execution | |
public const int S_ISTXT = 0x200; // sticky bit | |
public const int S_IRWXU = 0x1C0; // RWX mask for owner | |
public const int S_IRUSR = 0x100; // R for owner | |
public const int S_IWUSR = 0x80; // W for owner | |
public const int S_IXUSR = 0x40; // X for owner | |
public const int S_IRWXG = 0x38; // RWX mask for group | |
public const int S_IRGRP = 0x20; // R for group | |
public const int S_IWGRP = 0x10; // W for group | |
public const int S_IXGRP = 0x8; // X for group | |
public const int S_IRWXO = 0x7; // RWX mask for other | |
public const int S_IROTH = 0x4; // R for other | |
public const int S_IWOTH = 0x2; // W for other | |
public const int S_IXOTH = 0x1; // X for other | |
public const int S_ISVTX = 0x200; // save swapped text even after use | |
static uint ConvertSymbolicPermissionsToOctal(string permissions) | |
{ | |
if (permissions.Length == 9) { | |
Console.WriteLine("updating permissions length"); | |
permissions = "-" + permissions; | |
} | |
if (permissions.Length != 10) | |
throw new ArgumentException("Invalid permission length. Permission string should be 10 characters long."); | |
uint octalPermissions = 0; | |
// Calculate the permission for user | |
octalPermissions += (permissions[1] == 'r' ? 4u : 0u); | |
octalPermissions += (permissions[2] == 'w' ? 2u : 0u); | |
octalPermissions += (permissions[3] == 'x' ? 1u : 0u); | |
octalPermissions <<= 3; // Shift left for the next group | |
// Calculate the permission for group | |
octalPermissions += (permissions[4] == 'r' ? 4u : 0u); | |
octalPermissions += (permissions[5] == 'w' ? 2u : 0u); | |
octalPermissions += (permissions[6] == 'x' ? 1u : 0u); | |
octalPermissions <<= 3; // Shift left for others | |
// Calculate the permission for others | |
octalPermissions += (permissions[7] == 'r' ? 4u : 0u); | |
octalPermissions += (permissions[8] == 'w' ? 2u : 0u); | |
octalPermissions += (permissions[9] == 'x' ? 1u : 0u); | |
return octalPermissions; | |
} | |
public static void SetZipFileUnixPermissions(string filePath, string pattern, string permissions) | |
{ | |
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite)) | |
using (BinaryReader br = new BinaryReader(fs)) | |
{ | |
long fileLength = fs.Length; | |
while (fs.Position < fileLength) | |
{ | |
long headerStartPosition = fs.Position; | |
uint signature = br.ReadUInt32(); | |
fs.Seek(-4, SeekOrigin.Current); // Move back to the start of the signature | |
if (signature == ZipLocalFileHeaderSignature) | |
{ | |
var header = ReadStruct<ZipLocalFileHeader>(br); | |
string fileName = Encoding.UTF8.GetString(br.ReadBytes(header.fileNameLength)); | |
fs.Seek(header.compressedSize + header.extraFieldLength, SeekOrigin.Current); | |
} | |
else if (signature == ZipCentralFileHeaderSignature) | |
{ | |
var header = ReadStruct<ZipCentralFileHeader>(br); | |
byte[] fileNameBytes = br.ReadBytes(header.fileNameLength); | |
string fileName = Encoding.UTF8.GetString(fileNameBytes); | |
Console.WriteLine("File: {0}", fileName); | |
if (Regex.IsMatch(fileName, pattern)) | |
{ | |
uint fileAttributes = 0x0001; // DOS attributes (lower byte) | |
uint unixFileType = S_IFREG; // regular file | |
uint unixPermissions = ConvertSymbolicPermissionsToOctal(permissions); | |
uint unixAttributes = unixFileType | unixPermissions; | |
fileAttributes |= unixAttributes << 16; | |
Console.WriteLine($"Updated file external attributes: {fileAttributes:X8}"); | |
header.versionUsed = (ushort)((header.versionUsed & 0x00FF) | (0x03 << 8)); // Unix | |
header.externalFileAttributes = fileAttributes; | |
fs.Seek(headerStartPosition, SeekOrigin.Begin); | |
WriteStruct(fs, header); | |
fs.Seek(header.fileNameLength, SeekOrigin.Current); | |
} | |
fs.Seek(header.extraFieldLength + header.fileCommentLength, SeekOrigin.Current); | |
} | |
else if (signature == ZipEndOfCentralDirHeaderSignature) | |
{ | |
break; // No need to continue reading after the end of central directory record | |
} | |
else | |
{ | |
Console.WriteLine($"Unknown Header: 0x{signature:X8}"); | |
break; | |
} | |
} | |
} | |
} | |
private static T ReadStruct<T>(BinaryReader reader) | |
{ | |
byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(T))); | |
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); | |
try | |
{ | |
return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); | |
} | |
finally | |
{ | |
handle.Free(); | |
} | |
} | |
private static void WriteStruct<T>(FileStream fs, T theStruct) where T : struct | |
{ | |
byte[] bytes = new byte[Marshal.SizeOf(typeof(T))]; | |
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); | |
Marshal.StructureToPtr(theStruct, handle.AddrOfPinnedObject(), true); | |
fs.Write(bytes, 0, bytes.Length); | |
handle.Free(); | |
} | |
} | |
"@ -Language CSharp | |
[ZipFileHelper]::SetZipFileUnixPermissions($ZipFilePath, $FilePattern, $FilePermissions) | |
# ./Set-ZipFileUnixPermissions.ps1 -ZipFilePath "zip-exec.zip" -FilePattern "jetsocat" -FilePermissions "-r-xr-xr-x" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment