mirror of
https://github.com/whoeevee/ivinject.git
synced 2026-01-09 00:25:03 +01:00
First commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using ivinject.Common.Models;
|
||||
|
||||
namespace ivinject.Features.Codesigning;
|
||||
|
||||
internal static class CodesigningMachOExtensions
|
||||
{
|
||||
internal static async Task<bool> SignAsync(
|
||||
this IviMachOBinary binary,
|
||||
string identity,
|
||||
bool force,
|
||||
FileInfo? entitlements,
|
||||
bool preserveEntitlements = false
|
||||
)
|
||||
{
|
||||
var arguments = new StringBuilder($"-s {identity}");
|
||||
|
||||
if (entitlements is not null)
|
||||
arguments.Append($" --entitlements {entitlements.FullName}");
|
||||
else if (preserveEntitlements)
|
||||
arguments.Append(" --preserve-metadata=entitlements");
|
||||
|
||||
if (force)
|
||||
arguments.Append(" -f");
|
||||
|
||||
arguments.Append($" {binary.FullName}");
|
||||
|
||||
using var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "codesign",
|
||||
Arguments = arguments.ToString(),
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
|
||||
internal static async Task<bool> RemoveSignatureAsync(this IviMachOBinary binary)
|
||||
{
|
||||
using var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "codesign",
|
||||
Arguments = $"--remove-signature {binary.FullName}"
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
|
||||
internal static async Task<bool> DumpEntitlementsAsync(this IviMachOBinary binary, string outputFilePath)
|
||||
{
|
||||
using var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "codesign",
|
||||
Arguments = $"-d --entitlements {outputFilePath} --xml {binary.FullName}",
|
||||
RedirectStandardError = true
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using ivinject.Common;
|
||||
using ivinject.Common.Models;
|
||||
using ivinject.Features.Codesigning.Models;
|
||||
using ivinject.Features.Packaging.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static ivinject.Features.Packaging.Models.DirectoryNames;
|
||||
using static ivinject.Common.Models.BinaryHeaders;
|
||||
|
||||
namespace ivinject.Features.Codesigning;
|
||||
|
||||
internal class CodesigningManager(ILogger logger) : IDisposable
|
||||
{
|
||||
private IviPackageInfo _packageInfo = null!;
|
||||
|
||||
private readonly List<IviMachOBinary> _allBinaries = [];
|
||||
private List<IviMachOBinary> Binaries => _allBinaries[1..];
|
||||
private IviMachOBinary MainBinary => _allBinaries[0];
|
||||
|
||||
private FileInfo? _savedMainEntitlements;
|
||||
|
||||
internal void UpdateWithPackage(IviPackageInfo packageInfo)
|
||||
{
|
||||
_packageInfo = packageInfo;
|
||||
ProcessBinaries(packageInfo);
|
||||
}
|
||||
|
||||
private void ProcessBinaries(IviPackageInfo packageInfo)
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(
|
||||
packageInfo.DirectoriesInfo.BundleDirectory,
|
||||
"*",
|
||||
SearchOption.AllDirectories))
|
||||
{
|
||||
var header = File.OpenRead(file).FileHeader();
|
||||
|
||||
if (!MhHeaders.Contains(header) && !FatHeaders.Contains(header))
|
||||
continue;
|
||||
|
||||
if (file.Contains("Stub"))
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(
|
||||
packageInfo.DirectoriesInfo.BundleDirectory,
|
||||
file
|
||||
);
|
||||
logger.LogWarning(
|
||||
"Skipping stub executable {}, its signature may not be modified",
|
||||
relativePath
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
_allBinaries.Add(new IviMachOBinary(file));
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<IviEncryptionInfo> GetEncryptionStateAsync()
|
||||
{
|
||||
var results = await Task.WhenAll(
|
||||
Binaries.Select(
|
||||
async binary => new
|
||||
{
|
||||
Binary = binary,
|
||||
IsEncrypted = await binary.IsEncrypted()
|
||||
}
|
||||
));
|
||||
|
||||
return new IviEncryptionInfo
|
||||
{
|
||||
IsMainBinaryEncrypted = await MainBinary.IsEncrypted(),
|
||||
EncryptedBinaries = results
|
||||
.Where(result => result.IsEncrypted)
|
||||
.Select(result => result.Binary)
|
||||
};
|
||||
}
|
||||
|
||||
internal async Task SaveMainBinaryEntitlementsAsync()
|
||||
{
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
if (await MainBinary.DumpEntitlementsAsync(tempFile))
|
||||
{
|
||||
logger.LogInformation("Saved {} entitlements", MainBinary.Name);
|
||||
_savedMainEntitlements = new FileInfo(tempFile);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogWarning(
|
||||
"Unable to save {} entitlements. The binary is likely unsigned.",
|
||||
MainBinary.Name
|
||||
);
|
||||
}
|
||||
|
||||
internal async Task<bool> SignAsync(string identity, bool isAdHocSigning, FileInfo? entitlements)
|
||||
{
|
||||
var mainExecutablesCount = 0;
|
||||
|
||||
var signingResults = await Task.WhenAll(
|
||||
Binaries.Select(async binary =>
|
||||
{
|
||||
var isMainExecutable = !Path.GetRelativePath(
|
||||
_packageInfo.DirectoriesInfo.BundleDirectory,
|
||||
binary.FullName
|
||||
).Contains(FrameworksDirectoryName);
|
||||
|
||||
if (isMainExecutable)
|
||||
mainExecutablesCount++;
|
||||
|
||||
return await binary.SignAsync(
|
||||
identity,
|
||||
isAdHocSigning,
|
||||
isMainExecutable
|
||||
? entitlements
|
||||
: null,
|
||||
isMainExecutable && entitlements is null
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (signingResults.Any(result => !result))
|
||||
return false;
|
||||
|
||||
if (!await MainBinary.SignAsync(identity, false, _savedMainEntitlements ?? entitlements))
|
||||
return false;
|
||||
|
||||
logger.LogInformation(
|
||||
"Signed {} binaries ({} main executables) with the specified identity",
|
||||
_allBinaries.Count,
|
||||
mainExecutablesCount + 1 // App main executable
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal async Task<bool> RemoveSignatureAsync(bool allBinaries)
|
||||
{
|
||||
if (allBinaries)
|
||||
{
|
||||
var removingResults = await Task.WhenAll(
|
||||
_allBinaries.Select(binary => binary.RemoveSignatureAsync())
|
||||
);
|
||||
|
||||
if (removingResults.Any(result => !result))
|
||||
return false;
|
||||
|
||||
logger.LogInformation("Signature removed from {} binaries", _allBinaries.Count);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!await MainBinary.RemoveSignatureAsync())
|
||||
return false;
|
||||
|
||||
logger.LogInformation("Removed {} signature", MainBinary.Name);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose() => _savedMainEntitlements?.Delete();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using ivinject.Common.Models;
|
||||
|
||||
namespace ivinject.Features.Codesigning.Models;
|
||||
|
||||
internal class IviEncryptionInfo
|
||||
{
|
||||
internal bool IsMainBinaryEncrypted { get; init; }
|
||||
internal IEnumerable<IviMachOBinary> EncryptedBinaries { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.IO.Compression;
|
||||
using ivinject.Common.Models;
|
||||
|
||||
namespace ivinject.Features.Command;
|
||||
|
||||
internal class IviRootCommand : RootCommand
|
||||
{
|
||||
private static string ParseAppPackageResult(ArgumentResult result)
|
||||
{
|
||||
var value = result.Tokens[0].Value;
|
||||
|
||||
if (!RegularExpressions.ApplicationPackage().IsMatch(value))
|
||||
result.ErrorMessage = "The application package must be either an .app bundle or an .ipa$ archive.";
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
private readonly Argument<string> _targetArgument = new(
|
||||
name: "target",
|
||||
description: "The application package, either .app bundle or ipa$",
|
||||
parse: ParseAppPackageResult
|
||||
);
|
||||
|
||||
private readonly Argument<string> _outputArgument = new(
|
||||
name: "output",
|
||||
description: "The output application package, either .app bundle or ipa$",
|
||||
parse: ParseAppPackageResult
|
||||
);
|
||||
|
||||
private readonly Option<bool> _overwriteOutputOption = new(
|
||||
"--overwrite",
|
||||
"Overwrite the output if it already exists"
|
||||
);
|
||||
|
||||
private readonly Option<CompressionLevel> _compressionLevelOption = new(
|
||||
"--compression-level",
|
||||
description: "The compression level for ipa$ archive output",
|
||||
getDefaultValue: () => CompressionLevel.Fastest
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
private readonly Option<IEnumerable<FileInfo>> _itemsOption = new("--items")
|
||||
{
|
||||
Description = "The entries to inject (Debian packages, Frameworks, and Bundles)",
|
||||
AllowMultipleArgumentsPerToken = true
|
||||
};
|
||||
|
||||
private readonly Option<string> _codesignIdentityOption = new(
|
||||
"--sign",
|
||||
"The identity for code signing (use \"-\" for ad hoc, a.k.a. fake signing)"
|
||||
);
|
||||
|
||||
private readonly Option<FileInfo> _codesignEntitlementsOption = new(
|
||||
"--entitlements",
|
||||
"The file containing entitlements that will be written into main executables"
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
private readonly Option<string> _customBundleIdOption = new(
|
||||
"--bundleId",
|
||||
"The custom identifier that will be applied to application bundles"
|
||||
);
|
||||
|
||||
private readonly Option<bool> _enableDocumentsSupportOption = new(
|
||||
"--enable-documents-support",
|
||||
"Enables documents support (file sharing) for the application"
|
||||
);
|
||||
|
||||
private readonly Option<bool> _removeSupportedDevicesOption = new(
|
||||
"--remove-supported-devices",
|
||||
"Removes supported devices property"
|
||||
);
|
||||
|
||||
private readonly Option<IEnumerable<string>> _directoriesToRemoveOption = new("--remove-directories")
|
||||
{
|
||||
Description = "Directories to remove in the app package, e.g. PlugIns, Watch, AppClip",
|
||||
AllowMultipleArgumentsPerToken = true
|
||||
};
|
||||
|
||||
internal IviRootCommand() : base("The most demure iOS app injector and signer")
|
||||
{
|
||||
_itemsOption.AddAlias("-i");
|
||||
_codesignIdentityOption.AddAlias("-s");
|
||||
_compressionLevelOption.AddAlias("--level");
|
||||
_codesignEntitlementsOption.AddAlias("-e");
|
||||
|
||||
_customBundleIdOption.AddAlias("-b");
|
||||
_enableDocumentsSupportOption.AddAlias("-d");
|
||||
_removeSupportedDevicesOption.AddAlias("-u");
|
||||
_directoriesToRemoveOption.AddAlias("-r");
|
||||
|
||||
AddArgument(_targetArgument);
|
||||
AddArgument(_outputArgument);
|
||||
AddOption(_overwriteOutputOption);
|
||||
AddOption(_compressionLevelOption);
|
||||
|
||||
AddOption(_itemsOption);
|
||||
AddOption(_codesignIdentityOption);
|
||||
AddOption(_codesignEntitlementsOption);
|
||||
|
||||
AddOption(_customBundleIdOption);
|
||||
AddOption(_enableDocumentsSupportOption);
|
||||
AddOption(_removeSupportedDevicesOption);
|
||||
AddOption(_directoriesToRemoveOption);
|
||||
|
||||
this.SetHandler(async (iviParameters, loggerFactory) =>
|
||||
{
|
||||
var commandProcessor = new IviRootCommandProcessor(loggerFactory);
|
||||
await commandProcessor.ProcessRootCommand(iviParameters);
|
||||
},
|
||||
new IviRootCommandParametersBinder(
|
||||
_targetArgument,
|
||||
_outputArgument,
|
||||
_overwriteOutputOption,
|
||||
_compressionLevelOption,
|
||||
_itemsOption,
|
||||
_codesignIdentityOption,
|
||||
_codesignEntitlementsOption,
|
||||
_customBundleIdOption,
|
||||
_enableDocumentsSupportOption,
|
||||
_removeSupportedDevicesOption,
|
||||
_directoriesToRemoveOption
|
||||
),
|
||||
new LoggerFactoryBinder()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Binding;
|
||||
using System.IO.Compression;
|
||||
using ivinject.Features.Command.Models;
|
||||
using ivinject.Features.Injection.Models;
|
||||
|
||||
namespace ivinject.Features.Command;
|
||||
|
||||
internal class IviRootCommandParametersBinder(
|
||||
Argument<string> targetArgument,
|
||||
Argument<string> outputArgument,
|
||||
Option<bool> overwriteOutputOption,
|
||||
Option<CompressionLevel> compressionLevelOption,
|
||||
Option<IEnumerable<FileInfo>> itemsOption,
|
||||
Option<string> codesignIdentityOption,
|
||||
Option<FileInfo> codesignEntitlementsOption,
|
||||
Option<string> customBundleIdOption,
|
||||
Option<bool> enableDocumentsSupportOption,
|
||||
Option<bool> removeSupportedDevicesOption,
|
||||
Option<IEnumerable<string>> directoriesToRemoveOption
|
||||
) : BinderBase<IviParameters>
|
||||
{
|
||||
protected override IviParameters GetBoundValue(BindingContext bindingContext)
|
||||
{
|
||||
var targetAppPackage =
|
||||
bindingContext.ParseResult.GetValueForArgument(targetArgument);
|
||||
var outputAppPackage =
|
||||
bindingContext.ParseResult.GetValueForArgument(outputArgument);
|
||||
var overwriteOutput =
|
||||
bindingContext.ParseResult.GetValueForOption(overwriteOutputOption);
|
||||
var compressionLevel =
|
||||
bindingContext.ParseResult.GetValueForOption(compressionLevelOption);
|
||||
|
||||
var items =
|
||||
bindingContext.ParseResult.GetValueForOption(itemsOption);
|
||||
var codesignIdentity =
|
||||
bindingContext.ParseResult.GetValueForOption(codesignIdentityOption);
|
||||
var codesignEntitlements =
|
||||
bindingContext.ParseResult.GetValueForOption(codesignEntitlementsOption);
|
||||
|
||||
var bundleId =
|
||||
bindingContext.ParseResult.GetValueForOption(customBundleIdOption);
|
||||
var enableDocumentsSupport =
|
||||
bindingContext.ParseResult.GetValueForOption(enableDocumentsSupportOption);
|
||||
var removeSupportedDevices =
|
||||
bindingContext.ParseResult.GetValueForOption(removeSupportedDevicesOption);
|
||||
var directoriesToRemove =
|
||||
bindingContext.ParseResult.GetValueForOption(directoriesToRemoveOption);
|
||||
|
||||
IviPackagingInfo? packagingInfo;
|
||||
|
||||
if (bundleId is null
|
||||
&& directoriesToRemove is null
|
||||
&& !enableDocumentsSupport
|
||||
&& !removeSupportedDevices)
|
||||
packagingInfo = null;
|
||||
else
|
||||
packagingInfo = new IviPackagingInfo
|
||||
{
|
||||
CustomBundleId = bundleId,
|
||||
EnableDocumentsSupport = enableDocumentsSupport,
|
||||
RemoveSupportedDevices = removeSupportedDevices,
|
||||
DirectoriesToRemove = directoriesToRemove ?? []
|
||||
};
|
||||
|
||||
return new IviParameters
|
||||
{
|
||||
TargetAppPackage = targetAppPackage,
|
||||
OutputAppPackage = outputAppPackage,
|
||||
OverwriteOutput = overwriteOutput,
|
||||
CompressionLevel = compressionLevel,
|
||||
InjectionEntries = items?.Select(item => new IviInjectionEntry(item)) ?? [],
|
||||
SigningInfo = codesignIdentity is null
|
||||
? null
|
||||
: new IviSigningInfo
|
||||
{
|
||||
Identity = codesignIdentity,
|
||||
Entitlements = codesignEntitlements
|
||||
},
|
||||
PackagingInfo = packagingInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using ivinject.Features.Codesigning;
|
||||
using ivinject.Features.Command.Models;
|
||||
using ivinject.Features.Injection;
|
||||
using ivinject.Features.Injection.Models;
|
||||
using ivinject.Features.Packaging;
|
||||
using ivinject.Features.Packaging.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ivinject.Features.Command;
|
||||
|
||||
internal class IviRootCommandProcessor
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly PackageManager _packageManager;
|
||||
private readonly InjectionManager _injectionManager;
|
||||
private readonly CodesigningManager _codesigningManager;
|
||||
|
||||
internal IviRootCommandProcessor(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger("Main");
|
||||
_packageManager = new PackageManager(
|
||||
loggerFactory.CreateLogger("PackageManager")
|
||||
);
|
||||
_injectionManager = new InjectionManager(
|
||||
loggerFactory.CreateLogger("InjectionManager")
|
||||
);
|
||||
_codesigningManager = new CodesigningManager(
|
||||
loggerFactory.CreateLogger("CodesigningManager")
|
||||
);
|
||||
}
|
||||
|
||||
[SuppressMessage("Usage", "CA2254")]
|
||||
private void CriticalError(string? message, params object?[] args)
|
||||
{
|
||||
_logger.LogCritical(message, args);
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
private async Task InjectEntries(IEnumerable<IviInjectionEntry> injectionEntries)
|
||||
{
|
||||
await _injectionManager.AddEntriesAsync(injectionEntries);
|
||||
|
||||
if (!await _injectionManager.ThinCopiedBinariesAsync())
|
||||
CriticalError("Unable to thin one or more binaries.");
|
||||
|
||||
await _injectionManager.CopyKnownFrameworksAsync();
|
||||
await _injectionManager.FixCopiedDependenciesAsync();
|
||||
}
|
||||
|
||||
private async Task CheckForEncryptedBinaries(IviPackageInfo packageInfo)
|
||||
{
|
||||
var encryptionInfo = await _codesigningManager.GetEncryptionStateAsync();
|
||||
|
||||
if (encryptionInfo.IsMainBinaryEncrypted)
|
||||
CriticalError("The main application binary, {}, is encrypted.", packageInfo.MainBinary.Name);
|
||||
|
||||
if (encryptionInfo.EncryptedBinaries.Any())
|
||||
{
|
||||
var encryptedPaths = encryptionInfo.EncryptedBinaries.Select(binary =>
|
||||
Path.GetRelativePath(packageInfo.DirectoriesInfo.BundleDirectory, binary.FullName)
|
||||
);
|
||||
|
||||
_logger.LogError(
|
||||
"The app package contains encrypted binaries. Consider removing them: \n{}",
|
||||
string.Join("\n", encryptedPaths)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessSigning(IviSigningInfo? signingInfo)
|
||||
{
|
||||
var hasIdentity = signingInfo is not null;
|
||||
var isAdHocSigning = signingInfo?.IsAdHocSigning ?? false;
|
||||
var hasEntitlements = signingInfo?.Entitlements is not null;
|
||||
|
||||
if (hasIdentity && !isAdHocSigning && !hasEntitlements)
|
||||
CriticalError("Entitlements are required for non ad hoc identity signing.");
|
||||
|
||||
if (isAdHocSigning && !hasEntitlements)
|
||||
await _codesigningManager.SaveMainBinaryEntitlementsAsync();
|
||||
|
||||
if (!await _codesigningManager.RemoveSignatureAsync(!hasIdentity || hasEntitlements))
|
||||
CriticalError("Unable to remove signature from one or more binaries.");
|
||||
|
||||
await _injectionManager.InsertLoadCommandsAsync();
|
||||
|
||||
if (hasIdentity)
|
||||
{
|
||||
if (!await _codesigningManager.SignAsync(
|
||||
signingInfo!.Identity,
|
||||
isAdHocSigning,
|
||||
signingInfo.Entitlements))
|
||||
CriticalError("Unable to sign one or more binaries.");
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task ProcessRootCommand(IviParameters parameters)
|
||||
{
|
||||
_packageManager.LoadAppPackage(parameters.TargetAppPackage);
|
||||
_logger.LogInformation("Loaded app package");
|
||||
|
||||
var packageInfo = _packageManager.PackageInfo;
|
||||
|
||||
if (parameters.PackagingInfo is { } packagingInfo)
|
||||
await _packageManager.PerformPackageModifications(packagingInfo);
|
||||
|
||||
//
|
||||
|
||||
_injectionManager.UpdateWithPackage(packageInfo);
|
||||
await InjectEntries(parameters.InjectionEntries);
|
||||
|
||||
_codesigningManager.UpdateWithPackage(packageInfo);
|
||||
await CheckForEncryptedBinaries(packageInfo);
|
||||
|
||||
await ProcessSigning(parameters.SigningInfo);
|
||||
|
||||
if (!_packageManager.CreateAppPackage(
|
||||
parameters.OutputAppPackage,
|
||||
parameters.OverwriteOutput,
|
||||
parameters.CompressionLevel
|
||||
))
|
||||
CriticalError(
|
||||
"The app package couldn't be created. If it already exists, use --overwrite to replace."
|
||||
);
|
||||
|
||||
_codesigningManager.Dispose();
|
||||
_packageManager.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.CommandLine.Binding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ivinject.Features.Command;
|
||||
|
||||
internal class LoggerFactoryBinder : BinderBase<ILoggerFactory>
|
||||
{
|
||||
protected override ILoggerFactory GetBoundValue(BindingContext bindingContext)
|
||||
=> GetLoggerFactory();
|
||||
private static ILoggerFactory GetLoggerFactory()
|
||||
{
|
||||
var loggerFactory = LoggerFactory.Create(builder =>
|
||||
builder.AddConsole());
|
||||
|
||||
return loggerFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ivinject.Features.Command.Models;
|
||||
|
||||
internal class IviPackagingInfo
|
||||
{
|
||||
internal string? CustomBundleId { get; init; }
|
||||
internal bool RemoveSupportedDevices { get; init; }
|
||||
internal bool EnableDocumentsSupport { get; init; }
|
||||
internal required IEnumerable<string> DirectoriesToRemove { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.IO.Compression;
|
||||
using ivinject.Features.Injection.Models;
|
||||
|
||||
namespace ivinject.Features.Command.Models;
|
||||
|
||||
internal class IviParameters
|
||||
{
|
||||
internal required string TargetAppPackage { get; init; }
|
||||
internal required string OutputAppPackage { get; init; }
|
||||
internal bool OverwriteOutput { get; init; }
|
||||
internal CompressionLevel CompressionLevel { get; init; }
|
||||
internal required IEnumerable<IviInjectionEntry> InjectionEntries { get; init; }
|
||||
internal IviSigningInfo? SigningInfo { get; init; }
|
||||
internal IviPackagingInfo? PackagingInfo { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ivinject.Features.Command.Models;
|
||||
|
||||
internal class IviSigningInfo
|
||||
{
|
||||
internal required string Identity { get; init; }
|
||||
internal bool IsAdHocSigning => Identity == "-";
|
||||
internal FileInfo? Entitlements { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Diagnostics;
|
||||
using ivinject.Features.Injection.Models;
|
||||
using static ivinject.Common.DirectoryExtensions;
|
||||
|
||||
namespace ivinject.Features.Injection;
|
||||
|
||||
internal class DebManager(IviInjectionEntry debEntry) : IDisposable
|
||||
{
|
||||
private readonly string _tempPath = TempDirectoryPath();
|
||||
|
||||
internal async Task<IviInjectionEntry[]> ExtractDebEntries()
|
||||
{
|
||||
if (debEntry.Type is not IviInjectionEntryType.DebianPackage)
|
||||
throw new ArgumentException("Entry type is not DebianPackage");
|
||||
|
||||
Directory.CreateDirectory(_tempPath);
|
||||
|
||||
var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "tar",
|
||||
Arguments = $"-xf {debEntry.FullName} --directory={_tempPath}"
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
|
||||
var dataArchive = Directory.GetFiles(_tempPath, "data*.*")[0];
|
||||
|
||||
process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "tar",
|
||||
Arguments = $"-xf {dataArchive}",
|
||||
WorkingDirectory = _tempPath
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
|
||||
var dataFiles = Directory.EnumerateFiles(
|
||||
_tempPath,
|
||||
"*",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
|
||||
var dataDirectories = Directory.EnumerateDirectories(
|
||||
_tempPath,
|
||||
"*",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
|
||||
return dataFiles.Concat(dataDirectories)
|
||||
.Select(entry => new IviInjectionEntry(entry))
|
||||
.Where(entry => entry.Type is not IviInjectionEntryType.Unknown)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose() => Directory.Delete(_tempPath, true);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Diagnostics;
|
||||
using ivinject.Common.Models;
|
||||
using ivinject.Features.Injection.Models;
|
||||
using RegularExpressions = ivinject.Features.Injection.Models.RegularExpressions;
|
||||
|
||||
namespace ivinject.Features.Injection;
|
||||
|
||||
internal static class DependencyExtensions
|
||||
{
|
||||
internal static async Task<string[]> GetSharedLibraries(this IviMachOBinary binary)
|
||||
{
|
||||
using var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "otool",
|
||||
Arguments = $"-L {binary.FullName}",
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
);
|
||||
|
||||
var output = await process!.StandardOutput.ReadToEndAsync();
|
||||
var matches = RegularExpressions.OToolSharedLibrary().Matches(output);
|
||||
|
||||
// the first result is actually LC_ID_DYLIB, not LC_LOAD_DYLIB
|
||||
return matches.Select(match => match.Groups[1].Value).ToArray()[1..];
|
||||
}
|
||||
|
||||
internal static async Task ChangeDependency(
|
||||
this IviMachOBinary binary,
|
||||
string oldPath,
|
||||
string newPath
|
||||
)
|
||||
{
|
||||
using var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "install_name_tool",
|
||||
Arguments = $"-change {oldPath} {newPath} {binary.FullName}",
|
||||
RedirectStandardError = true
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
}
|
||||
|
||||
internal static async Task<bool> AddRunPath(
|
||||
this IviMachOBinary binary,
|
||||
string rPath
|
||||
)
|
||||
{
|
||||
using var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "install_name_tool",
|
||||
Arguments = $"-add_rpath {rPath} {binary.FullName}",
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
|
||||
internal static async Task InsertDependency(
|
||||
this IviMachOBinary binary,
|
||||
string dependency
|
||||
)
|
||||
{
|
||||
using var process = Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "insert-dylib",
|
||||
Arguments = $"{dependency} {binary.FullName} --all-yes --inplace",
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
);
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
}
|
||||
|
||||
internal static async Task<IEnumerable<string>> AllDependencies(this List<IviCopiedBinary> copiedBinaries)
|
||||
{
|
||||
return (await Task.WhenAll(
|
||||
copiedBinaries.Select(async binary => await binary.Binary.GetSharedLibraries())
|
||||
))
|
||||
.SelectMany(dependencies => dependencies)
|
||||
.Distinct();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using Claunia.PropertyList;
|
||||
using ivinject.Common.Models;
|
||||
using ivinject.Features.Injection.Models;
|
||||
using ivinject.Features.Packaging;
|
||||
using ivinject.Features.Packaging.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static ivinject.Common.DirectoryExtensions;
|
||||
|
||||
namespace ivinject.Features.Injection;
|
||||
|
||||
internal class InjectionManager(ILogger logger)
|
||||
{
|
||||
private readonly List<IviCopiedBinary> _copiedBinaries = [];
|
||||
private IviPackageInfo _packageInfo = null!;
|
||||
|
||||
private static readonly IviInjectionEntry[] KnownFrameworkEntries = Directory.GetDirectories(
|
||||
Path.Combine(HomeDirectoryPath(), ".ivinject"),
|
||||
"*.framework"
|
||||
)
|
||||
.Select(framework => new IviInjectionEntry(framework))
|
||||
.ToArray();
|
||||
|
||||
private static readonly IviInjectionEntry OrionFramework = KnownFrameworkEntries
|
||||
.Single(framework => framework.Name == "Orion.framework");
|
||||
|
||||
private static readonly IviInjectionEntry SubstrateFramework = KnownFrameworkEntries
|
||||
.Single(framework => framework.Name == "CydiaSubstrate.framework");
|
||||
|
||||
internal void UpdateWithPackage(IviPackageInfo packageInfo) =>
|
||||
_packageInfo = packageInfo;
|
||||
|
||||
private async Task<IviInjectionEntry[]> PrepareEntriesAsync(
|
||||
IviInjectionEntry[] entries,
|
||||
List<IDisposable> debManagers
|
||||
)
|
||||
{
|
||||
var finalEntries = entries.Where(entry =>
|
||||
entry.Type is not IviInjectionEntryType.DebianPackage
|
||||
).ToList();
|
||||
|
||||
var debFiles = entries.Where(entry =>
|
||||
entry.Type is IviInjectionEntryType.DebianPackage
|
||||
);
|
||||
|
||||
foreach (var debFile in debFiles)
|
||||
{
|
||||
var name = debFile.Name;
|
||||
|
||||
var debManager = new DebManager(debFile);
|
||||
|
||||
var debEntries = await debManager.ExtractDebEntries();
|
||||
logger.LogInformation("{} entries within {} will be injected", debEntries.Length, name);
|
||||
|
||||
finalEntries.AddRange(debEntries);
|
||||
debManagers.Add(debManager);
|
||||
}
|
||||
|
||||
return finalEntries.ToArray();
|
||||
}
|
||||
|
||||
private void CopyEntries(IEnumerable<IviInjectionEntry> entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var isReplaced = false;
|
||||
var pathInBundle = entry.GetPathInBundle(_packageInfo.DirectoriesInfo);
|
||||
|
||||
var isDynamicLibrary = entry.Type is IviInjectionEntryType.DynamicLibrary;
|
||||
|
||||
if (isDynamicLibrary || entry.Type is IviInjectionEntryType.Unknown)
|
||||
{
|
||||
if (File.Exists(pathInBundle))
|
||||
{
|
||||
File.Delete(pathInBundle);
|
||||
isReplaced = true;
|
||||
}
|
||||
|
||||
File.Copy(entry.FullName, pathInBundle);
|
||||
|
||||
if (isDynamicLibrary)
|
||||
_copiedBinaries.Add(
|
||||
new IviCopiedBinary
|
||||
{
|
||||
Binary = new IviMachOBinary(pathInBundle),
|
||||
Type = entry.Type
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Directory.Exists(pathInBundle))
|
||||
{
|
||||
Directory.Delete(pathInBundle, true);
|
||||
isReplaced = true;
|
||||
}
|
||||
|
||||
CopyDirectory(entry.FullName, pathInBundle, true);
|
||||
|
||||
if (entry.Type is not IviInjectionEntryType.Bundle) {
|
||||
var infoDictionaryPath = Path.Combine(pathInBundle, "Info.plist");
|
||||
var infoDictionary = (NSDictionary)PropertyListParser.Parse(infoDictionaryPath);
|
||||
|
||||
_copiedBinaries.Add(
|
||||
new IviCopiedBinary
|
||||
{
|
||||
Binary = new IviMachOBinary(
|
||||
Path.Combine(pathInBundle, infoDictionary.BundleExecutable())
|
||||
),
|
||||
Type = entry.Type
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("{} {}", isReplaced ? "Replaced" : "Copied", entry.Name);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task AddEntriesAsync(IEnumerable<IviInjectionEntry> files)
|
||||
{
|
||||
var debManagers = new List<IDisposable>();
|
||||
|
||||
var entries = await PrepareEntriesAsync(files.ToArray(), debManagers);
|
||||
CopyEntries(entries);
|
||||
|
||||
debManagers.ForEach(manager => manager.Dispose());
|
||||
}
|
||||
|
||||
internal async Task<bool> ThinCopiedBinariesAsync()
|
||||
{
|
||||
var fatBinaries =
|
||||
_copiedBinaries.Select(binary => binary.Binary).Where(binary => binary.IsFatFile);
|
||||
|
||||
foreach (var fatBinary in fatBinaries)
|
||||
{
|
||||
var previousSize = fatBinary.FileSize;
|
||||
|
||||
if (!await fatBinary.Thin())
|
||||
return false;
|
||||
|
||||
logger.LogInformation(
|
||||
"Thinned {} ({} -> {})",
|
||||
fatBinary.Name,
|
||||
previousSize,
|
||||
fatBinary.FileSize
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal async Task CopyKnownFrameworksAsync()
|
||||
{
|
||||
var allDependencies = await _copiedBinaries.AllDependencies();
|
||||
|
||||
var entries = KnownFrameworkEntries.Where(framework =>
|
||||
allDependencies.Any(dependency => dependency.Contains(framework.Name))
|
||||
).ToList();
|
||||
|
||||
if (entries.Contains(OrionFramework) && !entries.Contains(SubstrateFramework))
|
||||
entries.Add(SubstrateFramework);
|
||||
|
||||
CopyEntries(entries);
|
||||
}
|
||||
|
||||
internal async Task FixCopiedDependenciesAsync()
|
||||
{
|
||||
var copiedNames = _copiedBinaries.Select(binary => binary.Name);
|
||||
|
||||
foreach (var binary in _copiedBinaries.Select(binary => binary.Binary))
|
||||
{
|
||||
var dependencies = await binary.GetSharedLibraries();
|
||||
|
||||
var brokenDependencies = dependencies.Where(dependency =>
|
||||
!dependency.StartsWith('@') && copiedNames.Any(dependency.Contains)
|
||||
);
|
||||
|
||||
foreach (var dependency in brokenDependencies)
|
||||
{
|
||||
var copiedBinary = _copiedBinaries.Single(copiedBinary =>
|
||||
dependency.Contains(copiedBinary.Name)
|
||||
);
|
||||
|
||||
var newPath = copiedBinary.GetRunPath(_packageInfo.DirectoriesInfo);
|
||||
await binary.ChangeDependency(dependency, newPath);
|
||||
|
||||
logger.LogInformation(
|
||||
"Fixed dependency path in {} ({} -> {})",
|
||||
binary.Name,
|
||||
dependency,
|
||||
newPath
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task InsertLoadCommandsAsync()
|
||||
{
|
||||
var mainBinary = _packageInfo.MainBinary;
|
||||
|
||||
var copiedDependencies = (await _copiedBinaries.AllDependencies())
|
||||
.Where(dependency => dependency.StartsWith('@'));
|
||||
|
||||
var mainBinaryDependencies = await mainBinary.GetSharedLibraries();
|
||||
|
||||
// if (mainBinaryDependencies.All(dependency => !dependency.StartsWith("@rpath")))
|
||||
// {
|
||||
// await mainBinary.AddRunPath("@executable_path/Frameworks");
|
||||
// logger.LogInformation("Added Frameworks to {}'s run path", mainBinary.Name);
|
||||
// }
|
||||
|
||||
var dependenciesToInsert = _copiedBinaries
|
||||
.Where(binary =>
|
||||
binary.Type is IviInjectionEntryType.DynamicLibrary or IviInjectionEntryType.Framework
|
||||
)
|
||||
.Where(binary =>
|
||||
copiedDependencies.All(dependency => !dependency.Contains(binary.Name))
|
||||
);
|
||||
|
||||
foreach (var dependency in dependenciesToInsert)
|
||||
{
|
||||
if (mainBinaryDependencies.Contains(dependency.Name))
|
||||
continue;
|
||||
|
||||
var runPath = dependency.GetRunPath(_packageInfo.DirectoriesInfo);
|
||||
|
||||
await mainBinary.InsertDependency(runPath);
|
||||
logger.LogInformation("Inserted load command {} into {}", runPath, mainBinary.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Text;
|
||||
using ivinject.Common.Models;
|
||||
using ivinject.Features.Packaging.Models;
|
||||
|
||||
namespace ivinject.Features.Injection.Models;
|
||||
|
||||
internal class IviCopiedBinary
|
||||
{
|
||||
internal required IviInjectionEntryType Type { get; init; }
|
||||
internal required IviMachOBinary Binary { get; init; }
|
||||
|
||||
internal string Name => Binary.Name;
|
||||
|
||||
internal string GetRunPath(IviDirectoriesInfo directoriesInfo)
|
||||
{
|
||||
var builder = new StringBuilder("@rpath/");
|
||||
|
||||
builder.Append(
|
||||
Type switch
|
||||
{
|
||||
IviInjectionEntryType.Framework => Path.GetRelativePath(
|
||||
directoriesInfo.FrameworksDirectory,
|
||||
Binary.FullName
|
||||
),
|
||||
IviInjectionEntryType.PlugIn => Path.GetRelativePath(
|
||||
directoriesInfo.PlugInsDirectory,
|
||||
Binary.FullName
|
||||
),
|
||||
_ => Binary.Name
|
||||
}
|
||||
);
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using ivinject.Features.Packaging.Models;
|
||||
|
||||
namespace ivinject.Features.Injection.Models;
|
||||
|
||||
internal class IviInjectionEntry
|
||||
{
|
||||
private readonly FileInfo _fileInfo;
|
||||
internal string FullName => _fileInfo.FullName;
|
||||
internal string Name => _fileInfo.Name;
|
||||
internal IviInjectionEntryType Type => _fileInfo.Extension switch
|
||||
{
|
||||
".dylib" => IviInjectionEntryType.DynamicLibrary,
|
||||
".deb" => IviInjectionEntryType.DebianPackage,
|
||||
".bundle" => IviInjectionEntryType.Bundle,
|
||||
".framework" => IviInjectionEntryType.Framework,
|
||||
".appex" => IviInjectionEntryType.PlugIn,
|
||||
_ => IviInjectionEntryType.Unknown
|
||||
};
|
||||
|
||||
internal IviInjectionEntry(FileInfo fileInfo)
|
||||
=> _fileInfo = fileInfo;
|
||||
|
||||
internal IviInjectionEntry(string filePath)
|
||||
=> _fileInfo = new FileInfo(filePath);
|
||||
|
||||
internal string GetPathInBundle(IviDirectoriesInfo directoriesInfo) =>
|
||||
Type switch
|
||||
{
|
||||
IviInjectionEntryType.DynamicLibrary or IviInjectionEntryType.Unknown =>
|
||||
Path.Combine(
|
||||
Type is IviInjectionEntryType.DynamicLibrary
|
||||
? directoriesInfo.FrameworksDirectory
|
||||
: directoriesInfo.BundleDirectory,
|
||||
Name
|
||||
),
|
||||
IviInjectionEntryType.Framework => Path.Combine(
|
||||
directoriesInfo.FrameworksDirectory,
|
||||
Name
|
||||
),
|
||||
IviInjectionEntryType.PlugIn => Path.Combine(
|
||||
directoriesInfo.PlugInsDirectory,
|
||||
Name
|
||||
),
|
||||
_ => Path.Combine(directoriesInfo.BundleDirectory, Name)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ivinject.Features.Injection.Models;
|
||||
|
||||
internal enum IviInjectionEntryType
|
||||
{
|
||||
DynamicLibrary,
|
||||
DebianPackage,
|
||||
Bundle,
|
||||
Framework,
|
||||
PlugIn,
|
||||
Unknown
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ivinject.Features.Injection.Models;
|
||||
|
||||
internal static partial class RegularExpressions
|
||||
{
|
||||
[GeneratedRegex(@"([\/@].*) \(.*\)", RegexOptions.Compiled)]
|
||||
internal static partial Regex OToolSharedLibrary();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Claunia.PropertyList;
|
||||
using static ivinject.Features.Packaging.Models.InfoPlistDictionaryKeys;
|
||||
|
||||
namespace ivinject.Features.Packaging;
|
||||
|
||||
internal static class InfoPlistDictionaryExtensions
|
||||
{
|
||||
internal static string? BundleIdentifier(this NSDictionary dictionary) =>
|
||||
dictionary.TryGetValue(CoreFoundationBundleIdentifierKey, out var bundleIdObject)
|
||||
? ((NSString)bundleIdObject).Content
|
||||
: null;
|
||||
|
||||
internal static string? WatchKitCompanionAppBundleIdentifier(this NSDictionary dictionary) =>
|
||||
dictionary.TryGetValue(WatchKitCompanionAppBundleIdentifierKey, out var bundleIdObject)
|
||||
? ((NSString)bundleIdObject).Content
|
||||
: null;
|
||||
internal static NSDictionary? Extension(this NSDictionary dictionary) =>
|
||||
dictionary.TryGetValue(NextStepExtensionKey, out var extension)
|
||||
? (NSDictionary)extension
|
||||
: null;
|
||||
|
||||
internal static string ExtensionPointIdentifier(this NSDictionary dictionary) =>
|
||||
((NSString)dictionary[NextStepExtensionPointIdentifierKey]).Content;
|
||||
|
||||
internal static NSDictionary ExtensionAttributes(this NSDictionary dictionary) =>
|
||||
(NSDictionary)dictionary[NextStepExtensionAttributesKey];
|
||||
|
||||
internal static string WatchKitAppBundleIdentifier(this NSDictionary dictionary) =>
|
||||
((NSString)dictionary[WatchKitAppBundleIdentifierKey]).Content;
|
||||
|
||||
internal static string BundleExecutable(this NSDictionary dictionary) =>
|
||||
((NSString)dictionary[CoreFoundationBundleExecutableKey]).Content;
|
||||
|
||||
internal static async Task SaveToFile(this NSDictionary dictionary, string filePath) =>
|
||||
await File.WriteAllTextAsync(filePath, dictionary.ToXmlPropertyList());
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ivinject.Features.Packaging.Models;
|
||||
|
||||
internal class DirectoryNames
|
||||
{
|
||||
internal const string FrameworksDirectoryName = "Frameworks";
|
||||
internal const string PlugInsDirectoryName = "PlugIns";
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ivinject.Features.Packaging.Models;
|
||||
|
||||
internal static class InfoPlistDictionaryKeys
|
||||
{
|
||||
internal const string UiKitSupportedDevicesKey = "UISupportedDevices";
|
||||
internal const string UiKitSupportsDocumentBrowserKey = "UISupportsDocumentBrowser";
|
||||
internal const string UiKitFileSharingEnabledKey = "UIFileSharingEnabled";
|
||||
|
||||
internal const string CoreFoundationBundleIdentifierKey = "CFBundleIdentifier";
|
||||
internal const string CoreFoundationBundleExecutableKey = "CFBundleExecutable";
|
||||
internal const string WatchKitCompanionAppBundleIdentifierKey = "WKCompanionAppBundleIdentifier";
|
||||
internal const string NextStepExtensionKey = "NSExtension";
|
||||
internal const string NextStepExtensionPointIdentifierKey = "NSExtensionPointIdentifier";
|
||||
internal const string NextStepExtensionAttributesKey = "NSExtensionAttributes";
|
||||
internal const string WatchKitAppBundleIdentifierKey = "WKAppBundleIdentifier";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using static ivinject.Features.Packaging.Models.DirectoryNames;
|
||||
|
||||
namespace ivinject.Features.Packaging.Models;
|
||||
|
||||
internal class IviDirectoriesInfo(string bundleDirectory)
|
||||
{
|
||||
internal string BundleDirectory { get; } = bundleDirectory;
|
||||
internal string FrameworksDirectory { get; } = Path.Combine(bundleDirectory, FrameworksDirectoryName);
|
||||
internal string PlugInsDirectory { get; } = Path.Combine(bundleDirectory, PlugInsDirectoryName);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using ivinject.Common.Models;
|
||||
|
||||
namespace ivinject.Features.Packaging.Models;
|
||||
|
||||
internal class IviPackageInfo(string mainBinary, string bundleIdentifier, IviDirectoriesInfo directoriesInfo)
|
||||
{
|
||||
internal IviMachOBinary MainBinary { get; } = new(
|
||||
Path.Combine(directoriesInfo.BundleDirectory, mainBinary)
|
||||
);
|
||||
internal string BundleIdentifier { get; } = bundleIdentifier;
|
||||
internal IviDirectoriesInfo DirectoriesInfo { get; } = directoriesInfo;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.IO.Compression;
|
||||
using Claunia.PropertyList;
|
||||
using ivinject.Features.Packaging.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static ivinject.Common.DirectoryExtensions;
|
||||
|
||||
namespace ivinject.Features.Packaging;
|
||||
|
||||
internal partial class PackageManager(ILogger logger) : IDisposable
|
||||
{
|
||||
private readonly string _tempDirectory = TempDirectoryPath();
|
||||
private string TempPayloadDirectory => Path.Combine(_tempDirectory, "Payload");
|
||||
private string _bundleDirectory = null!;
|
||||
|
||||
private FileInfo _infoDictionaryFile = null!;
|
||||
private NSDictionary _infoDictionary = null!;
|
||||
|
||||
internal IviPackageInfo PackageInfo { get; private set; } = null!;
|
||||
|
||||
private void LoadPackageInfo()
|
||||
{
|
||||
_infoDictionaryFile = new FileInfo(
|
||||
Path.Combine(_bundleDirectory, "Info.plist")
|
||||
);
|
||||
|
||||
_infoDictionary = (NSDictionary)PropertyListParser.Parse(_infoDictionaryFile);
|
||||
|
||||
PackageInfo = new IviPackageInfo(
|
||||
_infoDictionary.BundleExecutable(),
|
||||
((NSString)_infoDictionary["CFBundleIdentifier"]).Content,
|
||||
new IviDirectoriesInfo(_bundleDirectory)
|
||||
);
|
||||
}
|
||||
|
||||
private void ProcessAppPackage(string targetAppPackage)
|
||||
{
|
||||
var directoryInfo = new DirectoryInfo(targetAppPackage);
|
||||
|
||||
if (directoryInfo.Exists)
|
||||
{
|
||||
var packageName = directoryInfo.Name;
|
||||
|
||||
_bundleDirectory = Path.Combine(TempPayloadDirectory, packageName);
|
||||
|
||||
CopyDirectory(targetAppPackage, _bundleDirectory, true);
|
||||
logger.LogInformation("Copied {}", packageName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(targetAppPackage);
|
||||
var fileName = fileInfo.Name;
|
||||
|
||||
ZipFile.ExtractToDirectory(targetAppPackage, _tempDirectory);
|
||||
logger.LogInformation("Extracted {}", fileName);
|
||||
|
||||
_bundleDirectory = Directory.GetDirectories(TempPayloadDirectory)[0];
|
||||
}
|
||||
|
||||
internal void LoadAppPackage(string targetAppPackage)
|
||||
{
|
||||
ProcessAppPackage(targetAppPackage);
|
||||
LoadPackageInfo();
|
||||
}
|
||||
|
||||
public void Dispose() => Directory.Delete(_tempDirectory, true);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Claunia.PropertyList;
|
||||
using ivinject.Features.Command.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static ivinject.Features.Packaging.Models.InfoPlistDictionaryKeys;
|
||||
|
||||
namespace ivinject.Features.Packaging;
|
||||
|
||||
internal partial class PackageManager
|
||||
{
|
||||
private void RemoveSupportedDevices()
|
||||
{
|
||||
if (_infoDictionary.Remove(UiKitSupportedDevicesKey))
|
||||
logger.LogInformation("Removed supported devices property");
|
||||
else
|
||||
logger.LogWarning("Unable to remove supported devices property. The key is likely not present.");
|
||||
}
|
||||
|
||||
private void EnableDocumentSupport()
|
||||
{
|
||||
_infoDictionary[UiKitSupportsDocumentBrowserKey] = new NSNumber(true);
|
||||
_infoDictionary[UiKitFileSharingEnabledKey] = new NSNumber(true);
|
||||
|
||||
logger.LogInformation("Enabled documents support for the application");
|
||||
}
|
||||
|
||||
private void RemoveDirectories(IEnumerable<string> directories)
|
||||
{
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
Directory.Delete(Path.Combine(_bundleDirectory, directory), true);
|
||||
logger.LogInformation("Removed {} directory from the app package", directory);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReplaceWatchKitIdentifiers(
|
||||
NSDictionary dictionary,
|
||||
string customBundleId,
|
||||
string packageBundleId
|
||||
)
|
||||
{
|
||||
if (dictionary.WatchKitCompanionAppBundleIdentifier() is not null)
|
||||
dictionary[WatchKitCompanionAppBundleIdentifierKey] = new NSString(customBundleId);
|
||||
|
||||
if (dictionary.Extension() is not { } extension
|
||||
|| extension.ExtensionPointIdentifier() != "com.apple.watchkit")
|
||||
return;
|
||||
|
||||
var attributes = extension.ExtensionAttributes();
|
||||
var watchKitAppBundleId = attributes.WatchKitAppBundleIdentifier();
|
||||
|
||||
attributes[WatchKitAppBundleIdentifierKey] = new NSString(
|
||||
watchKitAppBundleId.Replace(packageBundleId, customBundleId)
|
||||
);
|
||||
}
|
||||
|
||||
private async Task ReplaceBundleIdentifiers(string customBundleId)
|
||||
{
|
||||
var packageBundleId = PackageInfo.BundleIdentifier;
|
||||
var replacedCount = 0;
|
||||
|
||||
var infoPlistFiles = Directory.EnumerateFiles(
|
||||
_bundleDirectory,
|
||||
"Info.plist",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
|
||||
foreach (var file in infoPlistFiles)
|
||||
{
|
||||
var dictionary = (NSDictionary)PropertyListParser.Parse(file);
|
||||
|
||||
ReplaceWatchKitIdentifiers(dictionary, customBundleId, packageBundleId);
|
||||
|
||||
if (dictionary.BundleIdentifier() is not { } bundleId
|
||||
|| !bundleId.Contains(packageBundleId))
|
||||
continue;
|
||||
|
||||
var newBundleId = bundleId.Replace(packageBundleId, customBundleId);
|
||||
dictionary[CoreFoundationBundleIdentifierKey] = new NSString(newBundleId);
|
||||
|
||||
await dictionary.SaveToFile(file);
|
||||
replacedCount++;
|
||||
}
|
||||
|
||||
logger.LogInformation("Replaced bundle identifier of {} bundles", replacedCount);
|
||||
}
|
||||
|
||||
internal async Task PerformPackageModifications(IviPackagingInfo packagingInfo)
|
||||
{
|
||||
RemoveDirectories(packagingInfo.DirectoriesToRemove);
|
||||
|
||||
if (packagingInfo.RemoveSupportedDevices)
|
||||
RemoveSupportedDevices();
|
||||
|
||||
if (packagingInfo.EnableDocumentsSupport)
|
||||
EnableDocumentSupport();
|
||||
|
||||
await _infoDictionary.SaveToFile(_infoDictionaryFile.FullName);
|
||||
|
||||
if (packagingInfo.CustomBundleId is not { } customBundleId)
|
||||
return;
|
||||
|
||||
await ReplaceBundleIdentifiers(customBundleId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.IO.Compression;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static ivinject.Common.DirectoryExtensions;
|
||||
|
||||
namespace ivinject.Features.Packaging;
|
||||
|
||||
internal partial class PackageManager
|
||||
{
|
||||
private bool CopyAppPackage(string outputAppPackage, bool overwrite, ref bool isOverwritten)
|
||||
{
|
||||
var packageDirectory = new DirectoryInfo(outputAppPackage);
|
||||
|
||||
if (packageDirectory.Exists)
|
||||
{
|
||||
if (overwrite)
|
||||
{
|
||||
packageDirectory.Delete(true);
|
||||
isOverwritten = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(_bundleDirectory, packageDirectory.FullName, true);
|
||||
logger.LogInformation("{} {}", isOverwritten ? "Replaced" : "Copied", packageDirectory.Name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CreateAppArchive(
|
||||
string outputAppPackage,
|
||||
bool overwrite,
|
||||
CompressionLevel compressionLevel,
|
||||
ref bool isOverwritten
|
||||
)
|
||||
{
|
||||
var packageFile = new FileInfo(outputAppPackage);
|
||||
|
||||
if (packageFile.Exists)
|
||||
{
|
||||
if (overwrite)
|
||||
{
|
||||
packageFile.Delete();
|
||||
isOverwritten = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dotFile in Directory.EnumerateFiles(TempPayloadDirectory, ".*"))
|
||||
{
|
||||
var fileInfo = new FileInfo(dotFile);
|
||||
|
||||
File.Delete(fileInfo.FullName);
|
||||
logger.LogWarning("Removed {} from the app package", fileInfo.Name);
|
||||
}
|
||||
|
||||
ZipFile.CreateFromDirectory(
|
||||
TempPayloadDirectory,
|
||||
packageFile.FullName,
|
||||
compressionLevel,
|
||||
true
|
||||
);
|
||||
logger.LogInformation("{} {}", isOverwritten ? "Replaced" : "Created", packageFile.Name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool CreateAppPackage(string outputAppPackage, bool overwrite, CompressionLevel compressionLevel)
|
||||
{
|
||||
var isOverwritten = false;
|
||||
|
||||
return outputAppPackage.EndsWith(".app")
|
||||
? CopyAppPackage(outputAppPackage, overwrite, ref isOverwritten)
|
||||
: CreateAppArchive(outputAppPackage, overwrite, compressionLevel, ref isOverwritten);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user