diff --git a/DebUOS/Packaging.DebUOS.NuGet/Build/package.props b/DebUOS/Packaging.DebUOS.NuGet/Build/package.props
new file mode 100644
index 0000000..4de98b5
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS.NuGet/Build/package.props
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS.NuGet/Build/package.targets b/DebUOS/Packaging.DebUOS.NuGet/Build/package.targets
new file mode 100644
index 0000000..094356f
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS.NuGet/Build/package.targets
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+
+
+
+
+
+ $([MSBuild]::NormalizePath($(IntermediateOutputPath), 'DebUOSPacking'))
+
+
+ $([MSBuild]::NormalizePath($(DebUOSPackingWorkFolder), 'DebUOSPackingArgs.coin'))
+
+
+ $(Product)
+ $(Description)
+ $(Authors)
+ $(Author)
+ $(Company)
+ $(Publisher)
+ $(PackageProjectUrl)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS.NuGet/Packaging.DebUOS.NuGet.csproj b/DebUOS/Packaging.DebUOS.NuGet/Packaging.DebUOS.NuGet.csproj
new file mode 100644
index 0000000..4a46b00
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS.NuGet/Packaging.DebUOS.NuGet.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net6.0
+
+
+
+ false
+
+
+ true
+
+
+
+ true
+ MIT
+ true
+ Packaging.DebUOS
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DebUOS/Packaging.DebUOS.Tool/Options.cs b/DebUOS/Packaging.DebUOS.Tool/Options.cs
new file mode 100644
index 0000000..a45d0d0
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS.Tool/Options.cs
@@ -0,0 +1,25 @@
+using dotnetCampus.Cli;
+
+namespace Packaging.DebUOS.Tool;
+
+///
+/// 命令行参数
+///
+public class Options
+{
+ ///
+ /// 将给定路径文件夹打包为 UOS 的 deb 包
+ ///
+ /// 和 二选一,如果同时存在,优先使用 参数
+ [Option('b', "Build", Description = "Build path", LocalizableDescription = "将符合 UOS 安装包组织规范的文件夹打包为 deb 包,和 -p 参数二选一")]
+ public string? BuildPath { set; get; }
+
+ ///
+ /// 将根据给定的打包参数文件打包为 UOS 的 deb 包
+ ///
+ [Option('p', "Pack", Description = "Package argument file path", LocalizableDescription = "使用给定的 coin 格式参数文件制作 deb 包")]
+ public string? PackageArgumentFilePath { set; get; }
+
+ [Option('o', "Output", Description = "Output path", LocalizableDescription = "输出的 deb 文件路径")]
+ public string? OutputPath { set; get; }
+}
diff --git a/DebUOS/Packaging.DebUOS.Tool/Packaging.DebUOS.Tool.csproj b/DebUOS/Packaging.DebUOS.Tool/Packaging.DebUOS.Tool.csproj
new file mode 100644
index 0000000..8cd1c49
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS.Tool/Packaging.DebUOS.Tool.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+ true
+
+
+
+ true
+ MIT
+ true
+
+ true
+ True
+ dotnet-dpkg-debuos
+
+
+
+
+ all
+
+
+
diff --git a/DebUOS/Packaging.DebUOS.Tool/Program.cs b/DebUOS/Packaging.DebUOS.Tool/Program.cs
new file mode 100644
index 0000000..d194608
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS.Tool/Program.cs
@@ -0,0 +1,75 @@
+// See https://aka.ms/new-console-template for more information
+
+using System.Reflection;
+using System.Text;
+using dotnetCampus.Cli;
+using dotnetCampus.Configurations.Core;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+using Packaging.DebUOS;
+using Packaging.DebUOS.Contexts.Configurations;
+using Packaging.DebUOS.Tool;
+
+var options = CommandLine.Parse(args).As();
+
+var loggerFactory = LoggerFactory.Create(builder =>
+{
+ builder.AddSimpleConsole(simpleConsoleFormatterOptions =>
+ {
+ simpleConsoleFormatterOptions.ColorBehavior = LoggerColorBehavior.Disabled;
+ simpleConsoleFormatterOptions.SingleLine = true;
+ });
+});
+
+var logger = loggerFactory.CreateLogger("");
+
+if (!string.IsNullOrEmpty(options.BuildPath))
+{
+ var packingFolder = new DirectoryInfo(options.BuildPath);
+ var outputPath = options.OutputPath ?? Path.Join(packingFolder.FullName,$"{packingFolder.Name}.deb");
+ var outputDebFile = new FileInfo(outputPath);
+
+ var debUosPackageCreator = new DebUOSPackageCreator(logger);
+ //var packingFolder = new DirectoryInfo(@"C:\lindexi\Work\");
+ //var outputDebFile = new FileInfo(@"C:\lindexi\Work\Downloader.deb");
+ debUosPackageCreator.PackageDeb(packingFolder, outputDebFile);
+}
+else if (!string.IsNullOrEmpty(options.PackageArgumentFilePath))
+{
+ logger.LogInformation($"开始根据配置创建 UOS 的 deb 包。配置文件:{options.PackageArgumentFilePath}");
+ if (!File.Exists(options.PackageArgumentFilePath))
+ {
+ logger.LogError($"配置文件 '{options.PackageArgumentFilePath}' 不创建");
+ return;
+ }
+
+ var fileConfigurationRepo = ConfigurationFactory.FromFile(options.PackageArgumentFilePath, RepoSyncingBehavior.Static);
+ var appConfigurator = fileConfigurationRepo.CreateAppConfigurator();
+ var configuration = appConfigurator.Of();
+
+ var fileStructCreator = new DebUOSPackageFileStructCreator(logger);
+ fileStructCreator.CreatePackagingFolder(configuration);
+
+ var packingFolder = new DirectoryInfo(configuration.PackingFolder!);
+ var outputDebFile = new FileInfo(configuration.DebUOSOutputFilePath!);
+ var workingFolder = new DirectoryInfo(configuration.WorkingFolder!);
+
+ var debUosPackageCreator = new DebUOSPackageCreator(logger);
+ debUosPackageCreator.PackageDeb(packingFolder, outputDebFile,workingFolder);
+}
+else
+{
+ // Show Help
+ var stringBuilder = new StringBuilder()
+ .AppendLine($"用法:[options] [arguments]");
+ foreach (var propertyInfo in typeof(Options).GetProperties())
+ {
+ var optionAttribute = propertyInfo.GetCustomAttribute();
+ if (optionAttribute != null)
+ {
+ stringBuilder.AppendLine($"-{optionAttribute.ShortName} {(optionAttribute.LongName ?? string.Empty).PadRight(10)} {optionAttribute.Description} {optionAttribute.LocalizableDescription}");
+ }
+ }
+
+ Console.WriteLine(stringBuilder.ToString());
+}
diff --git a/DebUOS/Packaging.DebUOS.Tool/Properties/launchSettings.json b/DebUOS/Packaging.DebUOS.Tool/Properties/launchSettings.json
new file mode 100644
index 0000000..47e7f7c
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS.Tool/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Packing.DebUOS.Tool": {
+ "commandName": "Project",
+ "commandLineArgs": "-p C:\\lindexi\\Work\\DebUOSPackingArgs.coin"
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS/Contexts/ApplicationInfoFileData.cs b/DebUOS/Packaging.DebUOS/Contexts/ApplicationInfoFileData.cs
new file mode 100644
index 0000000..90b9cc2
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS/Contexts/ApplicationInfoFileData.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Packaging.DebUOS.Contexts;
+
+///
+/// 用于写入到 opt\apps\${AppId}\info 文件的数据内容
+///
+/// 将使用 json 格式写入
+class ApplicationInfoFileData
+{
+ [JsonPropertyName("appid")]
+ public string? AppId { init; get; }
+
+ [JsonPropertyName("name")]
+ public string? ApplicationName { init; get; }
+
+ [JsonPropertyName("version")]
+ public string? Version { init; get; }
+
+ [JsonPropertyName("arch")]
+ public IList? Architecture { init; get; }
+
+ [JsonPropertyName("permissions")]
+ public ApplicationInfoPermissions? Permissions { set; get; }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS/Contexts/ApplicationInfoPermissions.cs b/DebUOS/Packaging.DebUOS/Contexts/ApplicationInfoPermissions.cs
new file mode 100644
index 0000000..77f4c02
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS/Contexts/ApplicationInfoPermissions.cs
@@ -0,0 +1,25 @@
+using System.Text.Json.Serialization;
+
+namespace Packaging.DebUOS.Contexts;
+
+class ApplicationInfoPermissions
+{
+ [JsonPropertyName("autostart")]
+ public bool Autostart { get; set; }
+ [JsonPropertyName("notification")]
+ public bool Notification { get; set; }
+ [JsonPropertyName("trayicon")]
+ public bool TrayIcon { get; set; }
+ [JsonPropertyName("clipboard")]
+ public bool Clipboard { get; set; }
+ [JsonPropertyName("account")]
+ public bool Account { get; set; }
+ [JsonPropertyName("bluetooth")]
+ public bool Bluetooth { get; set; }
+ [JsonPropertyName("camera")]
+ public bool Camera { get; set; }
+ [JsonPropertyName("audio_record")]
+ public bool AudioRecord { get; set; }
+ [JsonPropertyName("installed_apps")]
+ public bool InstalledApps { get; set; }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS/Contexts/Configurations/DebUOSConfiguration.cs b/DebUOS/Packaging.DebUOS/Contexts/Configurations/DebUOSConfiguration.cs
new file mode 100644
index 0000000..bddf578
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS/Contexts/Configurations/DebUOSConfiguration.cs
@@ -0,0 +1,420 @@
+// ReSharper disable InconsistentNaming
+
+using System.IO;
+using dotnetCampus.Configurations;
+
+namespace Packaging.DebUOS.Contexts.Configurations;
+
+public class DebUOSConfiguration : Configuration
+{
+ public DebUOSConfiguration() : base("")
+ {
+ }
+
+ public string? AssemblyName
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 自定义的 DEBIAN\control 文件路径,将直接使用该文件作为 control 文件,忽略其他配置。这是比较高级的配置,一般不需要使用,可以用来满足更多的定制化需求
+ ///
+ public string? DebControlFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 自定义的 opt\apps\${AppId}\info 文件路径,将直接使用该文件作为 info 文件,忽略其他配置。这是比较高级的配置,一般不需要使用,可以用来满足更多的定制化需求
+ ///
+ public string? DebInfoFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 自定义的 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件路径,将直接使用该文件作为 desktop 文件,忽略其他配置。这是比较高级的配置,一般不需要使用,可以用来满足更多的定制化需求
+ ///
+ public string? DebDesktopFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 应用的 AppId 值,用来组织应用的安装路径,同时也是应用的唯一标识。按照 UOS 的规范,请务必使用厂商的倒置域名+产品名作为应用包名,如 `com.example.demo` 格式,前半部分为厂商域名倒置,后半部分为产品名,如果使用非拥有者的域名作为前缀,可能会引起该域名拥有者进行申诉,导致软件被申诉下架或者删除,只允许小写字母。不写默认和 AssemblyName 属性相同
+ ///
+ public string? AppId
+ {
+ set => SetValue(value);
+ get => GetString() ?? AssemblyName;
+ }
+
+ ///
+ /// 应用的 AppId 值,用来组织应用的安装路径,同时也是应用的唯一标识。按照 UOS 的规范,请务必使用厂商的倒置域名+产品名作为应用包名,如 `com.example.demo` 格式,前半部分为厂商域名倒置,后半部分为产品名,如果使用非拥有者的域名作为前缀,可能会引起该域名拥有者进行申诉,导致软件被申诉下架或者删除,只允许小写字母。不写默认和 AppId 属性相同
+ ///
+ /// 与 属性不同的是,该属性明确给制作 UOS 的包使用,不会和其他的逻辑的 AppId 混淆
+ ///
+ public string? UOSAppId
+ {
+ set => SetValue(value);
+ get => GetString() ?? AppId;
+ }
+
+ ///
+ /// 版本号,默认是 1.0.0 版本
+ ///
+ public string Version
+ {
+ set => SetValue(value);
+ get => GetString() ?? "1.0.0";
+ }
+
+ ///
+ /// 专门给制作 UOS 的包使用的版本号,不写将使用 属性的值。可使用 a.b.c 格式,也可以比较复杂的语义版本号格式,如 `1.2.3-2+b1` 等格式
+ ///
+ public string UOSDebVersion
+ {
+ set => SetValue(value);
+ get => GetString() ?? Version;
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Section 属性,可以选用 utils,admin, devel, doc, libs, net, 或者 unknown 等等,代表着该软件包在 Debian 仓库中将被归属到什么样的逻辑子分类中。默认是 utils 类型
+ ///
+ public string DebControlSection
+ {
+ set => SetValue(value);
+ get => GetString() ?? "utils";
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Priority 属性,可以选用 required, important, standard, optional, extra 等等,代表着该软件包在 Debian 仓库中的优先级,optional 优先级适用于与优先级为 required、important 或 standard 的软件包不冲突的新软件包。也可以做其它取值。若是不明了,请使用 optional。默认是 optional 类型
+ ///
+ public string DebControlPriority
+ {
+ set => SetValue(value);
+ get => GetString() ?? "optional";
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Architecture 属性,以及 opt\apps\${AppId}\info 文件的 arch 属性。可以选用 amd64, i386, arm64, armel, armhf, mips, mips64el, mipsel, ppc64el, s390x, 或者 all 等等,代表着该软件包在 Debian 仓库中的架构,amd64 代表着 64 位的 x86 架构,i386 代表着 32 位的 x86 架构,arm64 代表着 64 位的 ARM 架构,armel 代表着 32 位的 ARM 架构,armhf 代表着 32 位的 ARM 架构,mips 代表着 32 位的 MIPS 架构,mips64el 代表着 64 位的 MIPS 架构,mipsel 代表着 32 位的 MIPS 架构,ppc64el 代表着 64 位的 PowerPC 架构,s390x 代表着 64 位的 IBM S/390 架构,all 代表着所有架构。目前商店支持以下的 amd64, mips64el, arm64, sw_64, loongarch64 几种架构。默认是 amd64 类型
+ ///
+ public string Architecture
+ {
+ set => SetValue(value);
+ get => GetString() ?? "amd64";
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Multi-Arch 属性。默认是 foreign 类型
+ ///
+ public string DebControlMultiArch
+ {
+ set => SetValue(value);
+ get => GetString() ?? "foreign";
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Build-Depends 属性。默认是 debhelper (>=9) 类型
+ ///
+ public string DebControlBuildDepends
+ {
+ set => SetValue(value);
+ get => GetString() ?? "debhelper (>=9)";
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Standards-Version 属性。默认是 3.9.6 的值
+ ///
+ public string DebControlStandardsVersion
+ {
+ set => SetValue(value);
+ get => GetString() ?? "3.9.6";
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Maintainer 属性。如不填写,默认将会按照 Authors Author Company Publisher 的顺序,找到第一个不为空的值,作为 Maintainer 的值。如最终依然为空,可能导致打出来的安装包在用户端安装之后,不能在开始菜单中找到应用的图标
+ ///
+ public string? DebControlMaintainer
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Homepage 属性。如不填写,将尝试使用 PackageProjectUrl 属性,如依然为空则采用默认值。默认是 https://www.uniontech.com 的值
+ ///
+ public string DebControlHomepage
+ {
+ set => SetValue(value);
+ get => GetString() ?? "https://www.uniontech.com";
+ }
+
+ ///
+ /// 配置放入到 DEBIAN\control 文件的 Description 属性。如不填写,默认将使用 Description 属性的值
+ ///
+ public string? DebControlDescription
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 应用名,英文名。将作为 opt\apps\${AppId}\entries\applications\${AppId}.desktop 和 opt\apps\${AppId}\info 的 Name 属性的值,不写默认和 AssemblyName 属性相同
+ ///
+ public string? AppName
+ {
+ set => SetValue(value);
+ get => GetString() ?? AssemblyName;
+ }
+
+ ///
+ /// 应用名,中文名,可不写。将在开始菜单中显示
+ ///
+ public string? AppNameZhCN
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\info 文件的 permissions 属性,可不写,可开启的属性之间使用分号 ; 分割。可选值有:autostart, notification, trayicon, clipboard, account, bluetooth, camera, audio_record, installed_apps 等。默认为不开启任何权限
+ ///
+ public string? InfoPermissions
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Categories 属性,可选值有:AudioVideo, Audio, Video, Development, Education, Game, Graphics, Network, Office, Science, Settings, System, Utility, Other 等。默认为 Other 的值
+ ///
+ public string DesktopCategories
+ {
+ set => SetValue(value);
+ get => GetString() ?? "Other";
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Keywords 属性,作为程序的通用关键搜索词,当在启动器中搜索该词而非程序名称时,即可索引出该程序的快捷方式。多个关键词之间使用分号 ; 分割,关键词使用英文。如需添加中文关键词,请设置 属性。默认为 deepin 的值
+ ///
+ public string DesktopKeywords
+ {
+ set => SetValue(value);
+ get => GetString() ?? "deepin";
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Keywords[zh_CN] 属性,可不填,作为程序的通用关键搜索词,当在启动器中搜索该词而非程序名称时,即可索引出该程序的快捷方式。多个关键词之间使用分号 ; 分割,关键词使用中文
+ ///
+ public string? DesktopKeywordsZhCN
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Comment 属性,作为关于本程序的通用简述,在没有单独设置语言参数的情况下,默认显示该段内容。不填将使用 属性的值
+ ///
+ public string DesktopComment
+ {
+ set => SetValue(value);
+ get => GetString() ?? UOSAppId;
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Comment[zh_CN] 属性,作为关于本程序的通用中文简述,可不填
+ ///
+ public string? DesktopCommentZhCN
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Exec 属性,作为程序的启动命令,可不填,且推荐不填,除非有特殊需求。默认为 /opt/apps/${AppId}/files/bin/${AssemblyName} 的值
+ ///
+ public string? DesktopExec
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Icon 属性,作为程序的图标,可不填,且推荐不填,除非有特殊需求。默认为 的值
+ ///
+ public string? DesktopIcon
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Type 属性,作为程序的类型,按照 UOS 的规范,必须为 Application 的值,推荐不更改,即不填
+ ///
+ public string DesktopType
+ {
+ set => SetValue(value);
+ get => GetString() ?? "Application";
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 Terminal 属性,用来决定程序是否以终端的形式运行,默认是 false 关闭状态
+ ///
+ public bool DesktopTerminal
+ {
+ set => SetValue(value);
+ get => GetBoolean() ?? false;
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 StartupNotify 属性,用来决定程序是否允许桌面环境跟踪应用程序的启动,提供用户反馈和其他功能。例如鼠标的等待动画等,按照 UOS 规范建议,为保障应用使用体验,默认是 true 开启状态,推荐不更改,即不填
+ ///
+ public bool DesktopStartupNotify
+ {
+ set => SetValue(value);
+ get => GetBoolean() ?? true;
+ }
+
+ ///
+ /// 配置放入到 opt\apps\${AppId}\entries\applications\${AppId}.desktop 文件的 MimeType 属性,用来配置程序支持的关联文件类型,根据实际需求来填写。如果没有需要支持关联文件,则不填。多个文件类型之间使用分号 ; 分割
+ ///
+ public string? DesktopMimeType
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 进行打包的文件夹,用来组织打包的文件
+ ///
+ public string? PackingFolder
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 工作文件夹,用来存放打包过程中的临时文件
+ ///
+ public string? WorkingFolder
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 项目的发布输出文件夹
+ ///
+ public string? ProjectPublishFolder
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 打包输出文件路径
+ ///
+ public string? DebUOSOutputFilePath
+ {
+ set => SetValue(value);
+ get
+ {
+ var outputFilePath = GetString();
+ if (outputFilePath is null)
+ {
+ if (!string.IsNullOrEmpty(ProjectPublishFolder))
+ {
+ var name = UOSAppId;
+
+ if (string.IsNullOrEmpty(name))
+ {
+ name = Path.GetDirectoryName(ProjectPublishFolder);
+ }
+
+ return Path.Join(ProjectPublishFolder, $"{name}.deb");
+ }
+ }
+
+ return outputFilePath;
+ }
+ }
+
+ ///
+ /// 表示图标文件夹路径,文件夹里面按照 UOS 的 deb 规范组织图标文件,文件夹里面存放的内容将会被原原本本拷贝到 opt/apps/${AppId}/entries/icons/ 文件夹里面。此属性属于高级配置,一般不需要使用,可以用来满足更多的定制化需求。默认不填,且推荐在充分理解 UOS 的 deb 规范的情况下再进行使用。此属性存在值时,将会忽略 和 等属性的设置
+ ///
+ ///
+ /// 如果此属性配置不正确或图标文件夹的组织不正确,将会导致安装完成之后,无法从开始菜单中找到应用的图标
+ ///
+ public string? UOSDebIconFolder
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 应用图标文件,表示矢量图标文件。将被放入到 opt/apps/${AppId}/entries/icons/hicolor/scalable/apps/${appid}.svg 里面。矢量图标文件与 png 非矢量格式二选一,如果同时存在,优先使用矢量图标文件。
+ /// 当 属性存在值时,本属性设置无效
+ ///
+ public string? SvgIconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ /// 应用图标文件,表示 png 非矢量格式文件。将被放入到 opt/apps/${AppId}/entries/icons/hicolor/${分辨率}/apps/${appid}.png 里面。请确保实际图片分辨率正确,且是 png 格式。矢量图标文件与 png 非矢量格式二选一,如果同时存在,优先使用矢量图标文件。
+ /// 当 属性存在值时,本属性设置无效
+ ///
+ public string? Png16x16IconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ public string? Png24x24IconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ public string? Png32x32IconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ public string? Png48x48IconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ public string? Png128x128IconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ public string? Png256x256IconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+
+ ///
+ public string? Png512x512IconFile
+ {
+ set => SetValue(value);
+ get => GetString();
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS/DebUOSPackageCreator.cs b/DebUOS/Packaging.DebUOS/DebUOSPackageCreator.cs
new file mode 100644
index 0000000..b97bbbc
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS/DebUOSPackageCreator.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Packaging.Targets;
+using Packaging.Targets.IO;
+
+namespace Packaging.DebUOS;
+
+///
+/// 打出 UOS 的 deb 包
+///
+/// 打包细节请参阅 [一步步教你在 Windows 上构建 dotnet 系应用的 UOS 软件安装包](https://blog.lindexi.com/post/%E4%B8%80%E6%AD%A5%E6%AD%A5%E6%95%99%E4%BD%A0%E5%9C%A8-Windows-%E4%B8%8A%E6%9E%84%E5%BB%BA-dotnet-%E7%B3%BB%E5%BA%94%E7%94%A8%E7%9A%84-UOS-%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85%E5%8C%85.html )
+// ReSharper disable once InconsistentNaming
+public class DebUOSPackageCreator
+{
+ public DebUOSPackageCreator(ILogger logger)
+ {
+ Logger = logger;
+ }
+
+ public ILogger Logger { get; }
+
+ public void PackageDeb(DirectoryInfo packingFolder, FileInfo outputDebFile, DirectoryInfo? workingFolder = null)
+ {
+ Logger.LogInformation($"开始打包。Start packaging UOS deb from '{packingFolder.FullName}' to '{outputDebFile.FullName}'");
+
+ ArchiveBuilder archiveBuilder = new ArchiveBuilder();
+
+ workingFolder ??= packingFolder;
+ var debTarFilePath = Path.Combine(workingFolder.FullName, "deb.tar");
+ var debTarXzPath = Path.Combine(workingFolder.FullName, "deb.tar.xz");
+
+ var optFolder = Path.Combine(packingFolder.FullName, "opt");
+
+ var archiveEntries = archiveBuilder.FromDirectory(
+ optFolder,
+ null,
+ "/opt");
+
+ EnsureDirectories(archiveEntries);
+
+ archiveEntries = archiveEntries
+ .OrderBy(e => e.TargetPathWithFinalSlash, StringComparer.Ordinal)
+ .ToList();
+
+ using (var targetStream = outputDebFile.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ using (var tarStream = File.Open(debTarFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ {
+ TarFileCreator.FromArchiveEntries(archiveEntries, tarStream);
+ tarStream.Position = 0;
+
+ // 由于 XZOutputStream 类质量低劣(连当前位置都不知道,需要 Dispose 才能完成压缩等等)
+ // 因此需要先 Dispose 再重新打开
+ // XZOutputStream class has low quality (doesn't even know it's current position,
+ // needs to be disposed to finish compression, etc),
+ // So we are doing compression in a separate step
+ using (var tarXzStream = File.Open(debTarXzPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ using (var xzStream = new XZOutputStream(tarXzStream))
+ {
+ tarStream.CopyTo(xzStream);
+ }
+
+ // 重新打开
+ using (var tarXzStream = File.Open(debTarXzPath, FileMode.Open, FileAccess.Read, FileShare.None))
+ {
+ var pkgPackageFormatVersion = new Version(2, 0);
+
+ ArFileCreator.WriteMagic(targetStream);
+ ArFileCreator.WriteEntry(targetStream, "debian-binary", ArFileMode, pkgPackageFormatVersion + "\n");
+ WriteControl(packingFolder, targetStream);
+ ArFileCreator.WriteEntry(targetStream, "data.tar.xz", ArFileMode, tarXzStream);
+ }
+ }
+
+ Logger.LogInformation($"打包完成 '{outputDebFile.FullName}'");
+ }
+
+ private void WriteControl(DirectoryInfo packingFolder, Stream targetStream)
+ {
+ var controlTar = new MemoryStream();
+ WriteControlEntry(controlTar, "./");
+
+ var controlFile = Path.Combine(packingFolder.FullName, "DEBIAN", "control");
+ var controlFileText = File.ReadAllText(controlFile);
+
+ WriteControlEntry(controlTar, "./control", controlFileText);
+ TarFileCreator.WriteTrailer(controlTar);
+ controlTar.Seek(0, SeekOrigin.Begin);
+
+ var controlTarGz = new MemoryStream();
+ using (var gzStream = new GZipStream(controlTarGz, CompressionMode.Compress, true))
+ {
+ controlTar.CopyTo(gzStream);
+ }
+
+ controlTarGz.Seek(0, SeekOrigin.Begin);
+ ArFileCreator.WriteEntry(targetStream, "control.tar.gz", ArFileMode, controlTarGz);
+ }
+
+ private static void WriteControlEntry(Stream tar, string name, string data = null, LinuxFileMode? fileMode = null)
+ {
+ var s = (data != null) ? new MemoryStream(Encoding.UTF8.GetBytes(data)) : new MemoryStream();
+ var mode = fileMode ?? LinuxFileMode.S_IRUSR | LinuxFileMode.S_IWUSR |
+ LinuxFileMode.S_IRGRP | LinuxFileMode.S_IROTH;
+ mode |= data == null
+ ? LinuxFileMode.S_IFDIR | LinuxFileMode.S_IXUSR | LinuxFileMode.S_IXGRP | LinuxFileMode.S_IXOTH
+ : LinuxFileMode.S_IFREG;
+ var hdr = new TarHeader
+ {
+ FileMode = mode,
+ FileName = name,
+ FileSize = (uint) s.Length,
+ GroupName = "root",
+ UserName = "root",
+ LastModified = DateTimeOffset.UtcNow,
+ Magic = "ustar",
+ TypeFlag = data == null ? TarTypeFlag.DirType : TarTypeFlag.RegType,
+ };
+ TarFileCreator.WriteEntry(tar, hdr, s);
+ }
+
+ internal static void EnsureDirectories(List entries, bool includeRoot = true)
+ {
+ var dirs = new HashSet(entries.Where(x => x.Mode.HasFlag(LinuxFileMode.S_IFDIR))
+ .Select(d => d.TargetPathWithoutFinalSlash));
+
+ var toAdd = new List();
+
+ string GetDirPath(string path)
+ {
+ path = path.TrimEnd('/');
+ if (path == string.Empty)
+ {
+ return "/";
+ }
+
+ if (!path.Contains("/"))
+ {
+ return string.Empty;
+ }
+
+ return path.Substring(0, path.LastIndexOf('/'));
+ }
+
+ void EnsureDir(string dirPath)
+ {
+ if (dirPath == string.Empty || dirPath == ".")
+ {
+ return;
+ }
+
+ if (!dirs.Contains(dirPath))
+ {
+ if (dirPath != "/")
+ {
+ EnsureDir(GetDirPath(dirPath));
+ }
+
+ dirs.Add(dirPath);
+ toAdd.Add(new ArchiveEntry()
+ {
+ Mode = LinuxFileMode.S_IXOTH | LinuxFileMode.S_IROTH | LinuxFileMode.S_IXGRP |
+ LinuxFileMode.S_IRGRP | LinuxFileMode.S_IXUSR | LinuxFileMode.S_IWUSR |
+ LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFDIR,
+ Modified = DateTime.Now,
+ Group = "root",
+ Owner = "root",
+ TargetPath = dirPath,
+ LinkTo = string.Empty,
+ });
+ }
+ }
+
+ foreach (var entry in entries)
+ {
+ EnsureDir(GetDirPath(entry.TargetPathWithFinalSlash));
+ }
+
+ if (includeRoot)
+ {
+ EnsureDir("/");
+ }
+
+ entries.AddRange(toAdd);
+ }
+
+ private const LinuxFileMode ArFileMode = LinuxFileMode.S_IRUSR | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IRGRP |
+ LinuxFileMode.S_IROTH | LinuxFileMode.S_IFREG;
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS/DebUOSPackageFileStructCreator.cs b/DebUOS/Packaging.DebUOS/DebUOSPackageFileStructCreator.cs
new file mode 100644
index 0000000..39b1950
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS/DebUOSPackageFileStructCreator.cs
@@ -0,0 +1,321 @@
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.Logging;
+using Packaging.DebUOS.Contexts;
+using Packaging.DebUOS.Contexts.Configurations;
+using Walterlv.IO.PackageManagement;
+
+namespace Packaging.DebUOS;
+
+///
+/// 创建符合 UOS 安装包制作规范的打包文件夹
+///
+/// 打包细节请参阅 [一步步教你在 Windows 上构建 dotnet 系应用的 UOS 软件安装包](https://blog.lindexi.com/post/%E4%B8%80%E6%AD%A5%E6%AD%A5%E6%95%99%E4%BD%A0%E5%9C%A8-Windows-%E4%B8%8A%E6%9E%84%E5%BB%BA-dotnet-%E7%B3%BB%E5%BA%94%E7%94%A8%E7%9A%84-UOS-%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85%E5%8C%85.html )
+// ReSharper disable once InconsistentNaming
+public class DebUOSPackageFileStructCreator
+{
+ public DebUOSPackageFileStructCreator(ILogger logger)
+ {
+ Logger = logger;
+ }
+
+ public ILogger Logger { get; }
+
+ public void CreatePackagingFolder(DebUOSConfiguration configuration)
+ {
+ var projectPublishFolder = configuration.ProjectPublishFolder;
+ if (!Directory.Exists(projectPublishFolder))
+ {
+ Logger.LogError($"Project publish folder '{projectPublishFolder}' not exist");
+ return;
+ }
+
+ var workingFolder = configuration.WorkingFolder;
+ if (string.IsNullOrEmpty(workingFolder))
+ {
+ workingFolder = Path.Join(Path.GetTempPath(), "DebUOSPacking",
+ $"{configuration.AssemblyName}_{Path.GetRandomFileName()}");
+ }
+
+ Directory.CreateDirectory(workingFolder);
+ configuration.WorkingFolder = workingFolder;
+
+ var packingFolder = configuration.PackingFolder;
+ if (string.IsNullOrEmpty(packingFolder))
+ {
+ packingFolder = Path.Join(workingFolder, "Packing");
+ }
+
+ // 删除旧的文件夹,防止打包使用到旧文件
+ if (Directory.Exists(packingFolder))
+ {
+ Directory.Delete(packingFolder, true);
+ }
+
+ Directory.CreateDirectory(packingFolder);
+ configuration.PackingFolder = packingFolder;
+
+ var appId = configuration.UOSAppId;
+ if (string.IsNullOrEmpty(appId))
+ {
+ Logger.LogError($"找不到 UOS 的 AppId 内容,请确保已经配置 UOSAppId 属性");
+ return;
+ }
+
+ var match = Regex.Match(appId, @"[a-z\.]+");
+ if (!match.Success || match.Value != appId)
+ {
+ Logger.LogError(
+ $"UOS 的 AppId 内容不符合规范,请确保配置的 UOSAppId 属性符合规范。请务必使用厂商的倒置域名+产品名作为应用包名,如 `com.example.demo` 格式,且只允许使用小写字母");
+ return;
+ }
+
+ // opt\apps\AppId\
+ // opt\apps\AppId\files
+ var appIdFolder = Path.Join(packingFolder, "opt", "apps", appId);
+ var filesFolder = Path.Join(appIdFolder, "files");
+ Directory.CreateDirectory(filesFolder);
+ var applicationBin = Path.Join(filesFolder, "bin");
+ // 符号比拷贝速度快
+ var symbol = Directory.CreateSymbolicLink(applicationBin, projectPublishFolder);
+ if (!symbol.Exists)
+ {
+ Logger.LogError($"创建符号链接失败,从 '{projectPublishFolder}' 到 '{applicationBin}'");
+ return;
+ }
+
+ // opt\apps\AppId\entries
+ // opt\apps\AppId\entries\applications
+ var entriesFolder = Path.Join(appIdFolder, "entries");
+ var applicationsFolder = Path.Join(entriesFolder, "applications");
+ var desktopFile = Path.Join(applicationsFolder, $"{appId}.desktop");
+ Directory.CreateDirectory(applicationsFolder);
+
+ // 不能使用 Encoding.UTF8 编码,因为默认会写入 BOM 导致 deb 打包失败
+ var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+
+ if (File.Exists(configuration.DebDesktopFile))
+ {
+ // 开发者配置了自定义的文件,则使用开发者的文件
+ File.Copy(configuration.DebDesktopFile, desktopFile);
+ }
+ else
+ {
+ var stringBuilder = new StringBuilder();
+ // 这里不能使用 AppendLine 方法,保持换行使用 \n 字符
+ stringBuilder
+ .Append("[Desktop Entry]\n")
+ .Append($"Categories={configuration.DesktopCategories}\n")
+ .Append($"Name={configuration.AppName}\n")
+ .Append($"Keywords={configuration.DesktopKeywords}\n")
+ .Append($"Comment={configuration.DesktopComment}\n")
+ .Append($"Type={configuration.DesktopType}\n")
+ .Append($"Terminal={configuration.DesktopTerminal.ToString().ToLowerInvariant()}\n")
+ .Append($"StartupNotify={configuration.DesktopStartupNotify.ToString().ToLowerInvariant()}\n");
+
+ if (!string.IsNullOrEmpty(configuration.AppNameZhCN))
+ {
+ stringBuilder.Append($"Name[zh_CN]={configuration.AppNameZhCN}\n");
+ }
+
+ if (!string.IsNullOrEmpty(configuration.DesktopKeywordsZhCN))
+ {
+ stringBuilder.Append($"Keywords[zh_CN]={configuration.DesktopKeywordsZhCN}\n");
+ }
+
+ if (!string.IsNullOrEmpty(configuration.DesktopCommentZhCN))
+ {
+ stringBuilder.Append($"Comment[zh_CN]={configuration.DesktopCommentZhCN}\n");
+ }
+
+ if (!string.IsNullOrEmpty(configuration.DesktopExec))
+ {
+ stringBuilder.Append($"Exec={configuration.DesktopExec}\n");
+ }
+ else
+ {
+ // 这里不能使用 Path.Join 方法,因为如果在 Windows 上进行打包,会将 \ 替换为 /,导致打包失败
+ //var exec = Path.Join("/opt/apps", appId, "files", "bin", configuration.AssemblyName);
+ var exec = $"/opt/apps/{appId}/files/bin/{configuration.AssemblyName}";
+ stringBuilder.Append($"Exec={exec}\n");
+ }
+
+ if (!string.IsNullOrEmpty(configuration.DesktopIcon))
+ {
+ stringBuilder.Append($"Icon={configuration.DesktopIcon}\n");
+ }
+ else
+ {
+ stringBuilder.Append($"Icon={appId}\n");
+ }
+
+ if (!string.IsNullOrEmpty(configuration.DesktopMimeType))
+ {
+ stringBuilder.Append($"MimeType={configuration.DesktopMimeType}\n");
+ }
+
+ File.WriteAllText(desktopFile, stringBuilder.ToString(), encoding);
+ }
+
+ // opt\apps\AppId\entries\icons
+ var iconsFolder = Path.Join(entriesFolder, "icons");
+ if (!string.IsNullOrEmpty(configuration.UOSDebIconFolder))
+ {
+ // 如果开发者配置了自定义的图标文件夹,则使用开发者的文件夹
+ if (!Directory.Exists(configuration.UOSDebIconFolder))
+ {
+ Logger.LogError($"配置了 Icon 文件夹的 UOSDebIconFolder 属性,但文件夹不存在 UOSDebIconFolder={configuration.UOSDebIconFolder} FullPath={Path.GetFullPath(configuration.UOSDebIconFolder)}");
+ return;
+ }
+
+ PackageDirectory.Copy(configuration.UOSDebIconFolder, iconsFolder);
+ }
+ else if (File.Exists(configuration.SvgIconFile))
+ {
+ // 如果开发者配置了自定义的矢量图标文件,则优先使用矢量图标
+ var svgFile = Path.Join(iconsFolder, "hicolor", "scalable", "apps", $"{appId}.svg");
+ Directory.CreateDirectory(Path.GetDirectoryName(svgFile)!);
+ File.Copy(configuration.SvgIconFile, svgFile);
+ }
+ else
+ {
+ bool anyIconFileExist = false;
+ foreach (var (iconFile, resolution) in new (string? iconFile, string resolution)[]
+ {
+ (configuration.Png16x16IconFile, "16x16"),
+ (configuration.Png24x24IconFile, "24x24"),
+ (configuration.Png32x32IconFile, "32x32"),
+ (configuration.Png48x48IconFile, "48x48"),
+ (configuration.Png128x128IconFile, "128x128"),
+ (configuration.Png256x256IconFile, "256x256"),
+ (configuration.Png512x512IconFile, "512x512"),
+ })
+ {
+ if (File.Exists(iconFile))
+ {
+ var pngFile = Path.Join(iconsFolder, "hicolor", resolution, "apps", $"{appId}.png");
+ Directory.CreateDirectory(Path.GetDirectoryName(pngFile)!);
+ File.Copy(iconFile, pngFile);
+
+ anyIconFileExist = true;
+ }
+ }
+
+ if (!anyIconFileExist)
+ {
+ Logger.LogWarning("找不到任何的图标文件,将导致应用无法在开始菜单显示。可通过 SvgIconFile 配置矢量图,可通过 Png16x16IconFile 等属性配置不同分辨率的图标");
+ }
+ }
+
+ // opt\apps\AppId\info
+ var infoJsonFile = Path.Join(appIdFolder, "info");
+ if (File.Exists(configuration.DebInfoFile))
+ {
+ File.Copy(configuration.DebInfoFile, infoJsonFile);
+ }
+ else
+ {
+ var infoPermissions = configuration.InfoPermissions;
+
+ var data = new ApplicationInfoFileData()
+ {
+ AppId = appId,
+ ApplicationName = configuration.AppName,
+ Version = configuration.UOSDebVersion,
+ Architecture = configuration.Architecture.Split(';')
+ };
+
+ var permissions = infoPermissions?.Split(';');
+ if (permissions != null && permissions.Length > 0)
+ {
+ var applicationInfoPermissions = new ApplicationInfoPermissions();
+ foreach (var permission in permissions)
+ {
+ switch (permission)
+ {
+ case "autostart":
+ applicationInfoPermissions.Autostart = true;
+ break;
+ case "notification":
+ applicationInfoPermissions.Notification = true;
+ break;
+ case "trayicon":
+ applicationInfoPermissions.TrayIcon = true;
+ break;
+ case "clipboard":
+ applicationInfoPermissions.Clipboard = true;
+ break;
+ case "account":
+ applicationInfoPermissions.Account = true;
+ break;
+ case "bluetooth":
+ applicationInfoPermissions.Bluetooth = true;
+ break;
+ case "camera":
+ applicationInfoPermissions.Camera = true;
+ break;
+ case "audio_record":
+ applicationInfoPermissions.AudioRecord = true;
+ break;
+ case "installed_apps":
+ applicationInfoPermissions.InstalledApps = true;
+ break;
+ }
+ }
+
+ data.Permissions = applicationInfoPermissions;
+ }
+
+ var json = JsonSerializer.Serialize(data, new JsonSerializerOptions()
+ {
+ WriteIndented = true,
+ }).Replace("\r\n", "\n");
+ File.WriteAllText(infoJsonFile, json, encoding);
+ }
+
+ // 创建 control 文件
+ var controlFile = Path.Join(packingFolder, "DEBIAN", "control");
+ Directory.CreateDirectory(Path.GetDirectoryName(controlFile)!);
+ if (File.Exists(configuration.DebControlFile))
+ {
+ File.Copy(configuration.DebControlFile, controlFile);
+ }
+ else
+ {
+ var stringBuilder = new StringBuilder();
+ stringBuilder
+ .Append($"Package: {appId}\n")
+ .Append($"Version: {configuration.UOSDebVersion}\n")
+ .Append($"Section: {configuration.DebControlSection}\n")
+ .Append($"Priority: {configuration.DebControlPriority}\n")
+ .Append($"Architecture: {configuration.Architecture}\n")
+ .Append($"Multi-Arch: {configuration.DebControlMultiArch}\n")
+ .Append($"Build-Depends: {configuration.DebControlBuildDepends}\n")
+ .Append($"Standards-Version: {configuration.DebControlStandardsVersion}\n")
+ .Append($"Homepage: {configuration.DebControlHomepage}\n")
+ ;
+ if (!string.IsNullOrEmpty(configuration.DebControlMaintainer))
+ {
+ stringBuilder.Append($"Maintainer: {configuration.DebControlMaintainer}\n");
+ }
+ else
+ {
+ Logger.LogWarning(
+ $"没有找到 DebControlMaintainer 属性配置。安装完成可能在开始菜单找不到应用。请配置 deb 包的维护者,如 lindexi 格式");
+ }
+
+ if (!string.IsNullOrEmpty(configuration.DebControlDescription))
+ {
+ stringBuilder.Append($"Description: {configuration.DebControlDescription}\n");
+ }
+ else
+ {
+ Logger.LogWarning($"没有找到 DebControlDescription 属性配置。安装完成可能在开始菜单找不到应用。请配置 deb 包的描述,描述可使用中文");
+ }
+
+ File.WriteAllText(controlFile, stringBuilder.ToString(), encoding);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.DebUOS/Packaging.DebUOS.csproj b/DebUOS/Packaging.DebUOS/Packaging.DebUOS.csproj
new file mode 100644
index 0000000..d05ca82
--- /dev/null
+++ b/DebUOS/Packaging.DebUOS/Packaging.DebUOS.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net6.0
+ enable
+
+ false
+
+ true
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DebUOS/Packaging.Targets/ArchiveBuilder.cs b/DebUOS/Packaging.Targets/ArchiveBuilder.cs
new file mode 100644
index 0000000..54156ec
--- /dev/null
+++ b/DebUOS/Packaging.Targets/ArchiveBuilder.cs
@@ -0,0 +1,449 @@
+//using Microsoft.Build.Framework;
+//using Microsoft.Build.Utilities;
+using Packaging.Targets.IO;
+using Packaging.Targets.Rpm;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Packaging.Targets
+{
+ ///
+ /// Creates a list of objects based on the publish directory of a .NET Core application.
+ ///
+ internal class ArchiveBuilder
+ {
+ private IFileAnalyzer _fileAnayzer;
+ private uint _inode = 0;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ArchiveBuilder()
+ {
+ this._fileAnayzer = new FileAnalyzer();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A which can extract item metadata.
+ ///
+ public ArchiveBuilder(IFileAnalyzer analyzer)
+ {
+ if (analyzer == null)
+ {
+ throw new ArgumentNullException(nameof(analyzer));
+ }
+
+ this._fileAnayzer = analyzer;
+ }
+
+ /////
+ ///// Gets or sets a to which status messages can be written.
+ /////
+ //public TaskLoggingHelper Log
+ //{
+ // get;
+ // set;
+ //}
+
+ ///
+ /// Extracts the objects from a CPIO file.
+ ///
+ ///
+ /// The CPIO file from which to extract the entries.
+ ///
+ ///
+ /// A list of objects representing the data in the CPIO file.
+ ///
+ public List FromCpio(CpioFile file)
+ {
+ List value = new List();
+ byte[] buffer = new byte[1024];
+ byte[] fileHeader = null;
+
+ while (file.Read())
+ {
+ fileHeader = null;
+
+ ArchiveEntry entry = new ArchiveEntry()
+ {
+ FileSize = file.EntryHeader.FileSize,
+ Group = "root",
+ Owner = "root",
+ Inode = file.EntryHeader.Ino,
+ Mode = file.EntryHeader.FileMode,
+ Modified = file.EntryHeader.LastModified,
+ TargetPath = file.FileName,
+ Type = ArchiveEntryType.None,
+ LinkTo = string.Empty,
+ Sha256 = Array.Empty(),
+ SourceFilename = null,
+ IsAscii = true
+ };
+
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFREG) && !entry.Mode.HasFlag(LinuxFileMode.S_IFLNK))
+ {
+ using (var fileStream = file.Open())
+ using (var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
+ {
+ int read;
+
+ while (true)
+ {
+ read = fileStream.Read(buffer, 0, buffer.Length);
+
+ if (fileHeader == null)
+ {
+ fileHeader = new byte[read];
+ Buffer.BlockCopy(buffer, 0, fileHeader, 0, read);
+ }
+
+ hasher.AppendData(buffer, 0, read);
+ entry.IsAscii = entry.IsAscii && fileHeader.All(c => c < 128);
+
+ if (read < buffer.Length)
+ {
+ break;
+ }
+ }
+
+ entry.Sha256 = hasher.GetHashAndReset();
+ }
+
+ entry.Type = this.GetArchiveEntryType(fileHeader);
+ }
+ else if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK))
+ {
+ using (var fileStream = file.Open())
+ using (var reader = new StreamReader(fileStream, Encoding.UTF8))
+ {
+ entry.LinkTo = reader.ReadToEnd();
+ }
+ }
+ else
+ {
+ file.Skip();
+ }
+
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFDIR))
+ {
+ entry.FileSize = 0x1000;
+ }
+
+ if (entry.TargetPath.StartsWith("."))
+ {
+ entry.TargetPath = entry.TargetPath.Substring(1);
+ }
+
+ value.Add(entry);
+ }
+
+ return value;
+ }
+
+ //public Collection FromLinuxFolders(ITaskItem[] metadata)
+ //{
+ // Collection value = new Collection();
+
+ // // This can be null if the user did not define any folders.
+ // // In that case: nothing to do.
+ // if (metadata != null)
+ // {
+ // foreach (var folder in metadata)
+ // {
+ // var path = folder.ItemSpec.Replace("\\", "/");
+
+ // // Default file mode
+ // LinuxFileMode mode = LinuxFileMode.S_IXOTH | LinuxFileMode.S_IROTH | LinuxFileMode.S_IXGRP | LinuxFileMode.S_IRGRP | LinuxFileMode.S_IXUSR | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFDIR;
+ // mode = this.GetFileMode(path, folder, mode);
+
+ // // Write out an entry for the directory
+ // ArchiveEntry directoryEntry = new ArchiveEntry()
+ // {
+ // FileSize = 0x00001000,
+ // Sha256 = Array.Empty(),
+ // Mode = mode,
+ // Modified = DateTime.Now,
+ // Group = folder.GetGroup(),
+ // Owner = folder.GetOwner(),
+ // Inode = this.inode++,
+ // TargetPath = path,
+ // LinkTo = string.Empty,
+ // RemoveOnUninstall = folder.GetRemoveOnUninstall()
+ // };
+
+ // value.Add(directoryEntry);
+ // }
+ // }
+
+ // return value;
+ //}
+
+ ///
+ /// Extracts the objects from a directory.
+ ///
+ ///
+ /// The directory from which to extract the entries.
+ ///
+ ///
+ /// The prefix of the target directory.
+ ///
+ ///
+ /// Additional metadata to use.
+ ///
+ ///
+ /// A list of objects representing the data in the directory.
+ ///
+ public List FromDirectory(string directory, string? appHost, string prefix/*, ITaskItem[] metadata*/)
+ {
+ List value = new List();
+ this.AddDirectory(directory, string.Empty, prefix, value/*, metadata*/);
+
+ // Add a symlink to appHost, if available
+ if (appHost != null)
+ {
+ value.Add(
+ new ArchiveEntry()
+ {
+ Mode = LinuxFileMode.S_IXOTH | LinuxFileMode.S_IROTH | LinuxFileMode.S_IXGRP | LinuxFileMode.S_IRGRP | LinuxFileMode.S_IXUSR | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFLNK,
+ Modified = DateTimeOffset.UtcNow,
+ Group = "root",
+ Owner = "root",
+ TargetPath = $"/usr/local/bin/{appHost}",
+ LinkTo = $"{prefix}/{appHost}",
+ Inode = this._inode++,
+ Sha256 = Array.Empty(),
+ });
+ }
+
+ return value;
+ }
+
+ protected void AddDirectory(string directory, string relativePath, string prefix, List value/*, ITaskItem[] metadata*/)
+ {
+ this._inode++;
+
+ // The order in which the files appear in the cpio archive is important; if this is not respected xzdio
+ // will report errors like:
+ // error: unpacking of archive failed on file ./usr/share/quamotion/mscorlib.dll: cpio: Archive file not in header
+ var entries = Directory.GetFileSystemEntries(directory).OrderBy(e => Directory.Exists(e) ? e + "/" : e, StringComparer.Ordinal).ToArray();
+
+ foreach (var entry in entries)
+ {
+ if (File.Exists(entry))
+ {
+ this.AddFile(entry, relativePath + Path.GetFileName(entry), prefix, value/*, metadata*/);
+ }
+ else
+ {
+ this.AddDirectory(entry, relativePath + Path.GetFileName(entry) + "/", prefix + "/" + Path.GetFileName(entry), value/*, metadata*/);
+ }
+ }
+ }
+
+ protected void AddFile(string entry, string relativePath, string prefix, List value/*, ITaskItem[] metadata*/)
+ {
+ var fileName = Path.GetFileName(entry);
+
+ byte[] fileHeader = null;
+ byte[] hash = null;
+ byte[] md5hash = null;
+ byte[] buffer = new byte[1024];
+ bool isAscii = true;
+
+ //var fileMetadata = metadata.SingleOrDefault(m => m.IsPublished() && string.Equals(relativePath, m.GetPublishedPath()));
+
+ using (Stream fileStream = File.OpenRead(entry))
+ {
+ // Skip hidden and empty files - this would case rpmlint errors.
+ if (fileName.StartsWith("."))
+ {
+ //this.Log.LogWarning($"Ignoring file {relativePath} because it starts with the '.' character and is considered a hidden file.");
+ return;
+ }
+
+ // 可能有一些空文件存在,这是合理的
+ //if (fileStream.Length == 0)
+ //{
+ // //this.Log.LogWarning($"Ignoring file {relativePath} because it is empty.");
+ // return;
+ //}
+
+ //using (var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
+ //using (var md5hasher = IncrementalHash.CreateHash(HashAlgorithmName.MD5))
+ //{
+ int read;
+
+ // while (true)
+ // {
+ read = fileStream.Read(buffer, 0, buffer.Length);
+
+ if (fileHeader == null)
+ {
+ fileHeader = new byte[read];
+ Buffer.BlockCopy(buffer, 0, fileHeader, 0, read);
+ }
+
+ // hasher.AppendData(buffer, 0, read);
+ // md5hasher.AppendData(buffer, 0, read);
+ // isAscii = isAscii && buffer.All(c => c < 128);
+
+ // if (read < buffer.Length)
+ // {
+ // break;
+ // }
+ // }
+
+ // hash = hasher.GetHashAndReset();
+ // md5hash = md5hasher.GetHashAndReset();
+ //}
+
+ // Only support ELF32 and ELF64 colors; otherwise default to BLACK.
+ ArchiveEntryType entryType = this.GetArchiveEntryType(fileHeader);
+
+ var mode = LinuxFileMode.S_IROTH | LinuxFileMode.S_IRGRP | LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFREG;
+
+ if (entryType == ArchiveEntryType.Executable32 || entryType == ArchiveEntryType.Executable64)
+ {
+ mode |= LinuxFileMode.S_IXOTH | LinuxFileMode.S_IXGRP | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IXUSR;
+ }
+
+ // If a Linux path has been specified, use that one, else, use the default one based on the prefix
+ // + current file name.
+ string name = null; //fileMetadata?.GetLinuxPath();
+
+ if (name == null)
+ {
+ if (!string.IsNullOrEmpty(prefix))
+ {
+ name = prefix + "/" + fileName;
+ }
+ else
+ {
+ name = fileName;
+ }
+ }
+
+ string linkTo = string.Empty;
+
+ if (mode.HasFlag(LinuxFileMode.S_IFLNK))
+ {
+ // Find the link text
+ int stringEnd = 0;
+
+ while (stringEnd < fileHeader.Length - 1 && fileHeader[stringEnd] != 0)
+ {
+ stringEnd++;
+ }
+
+ linkTo = Encoding.UTF8.GetString(fileHeader, 0, stringEnd + 1);
+ hash = new byte[] { };
+ }
+
+ mode = this.GetFileMode(name, /*fileMetadata,*/ mode);
+
+ ArchiveEntry archiveEntry = new ArchiveEntry()
+ {
+ FileSize = (uint)fileStream.Length,
+ //Group = fileMetadata.GetGroup(),
+ //Owner = fileMetadata.GetOwner(),
+ Modified = File.GetLastWriteTimeUtc(entry),
+ SourceFilename = entry,
+ TargetPath = name,
+ // 可以忽略这两个的计算
+ //Sha256 = hash,
+ //Md5Hash = md5hash,
+ Type = entryType,
+ LinkTo = linkTo,
+ Inode = this._inode++,
+ IsAscii = isAscii,
+ Mode = mode
+ };
+
+ value.Add(archiveEntry);
+ }
+ }
+
+ private ArchiveEntryType GetArchiveEntryType(byte[] fileHeader)
+ {
+ if (ElfFile.IsElfFile(fileHeader))
+ {
+ ElfHeader elfHeader = ElfFile.ReadHeader(fileHeader);
+
+ if (elfHeader.@class == ElfClass.Elf32)
+ {
+ return ArchiveEntryType.Executable32;
+ }
+ else
+ {
+ return ArchiveEntryType.Executable64;
+ }
+ }
+
+ return ArchiveEntryType.None;
+ }
+
+ ///
+ /// Gets the file mode for a file or directory entry, based on a default value
+ /// and the value of the LinuxFileMode attribute, if set.
+ ///
+ ///
+ /// The name (path) of the entry. Only used in error messages.
+ ///
+ ///
+ /// The metadata for the current entry.
+ ///
+ ///
+ /// The default mode. This mode is used to determine the file type (directory/file),
+ /// and when no LinuxFileMode value is specified.
+ ///
+ ///
+ /// The for the current entry.
+ ///
+ private LinuxFileMode GetFileMode(string name, /*ITaskItem metadata, */LinuxFileMode defaultMode)
+ {
+ return defaultMode;
+
+ //LinuxFileMode mode = defaultMode;
+ //LinuxFileMode defaultFileTypeMask = defaultMode & LinuxFileMode.FileTypeMask;
+
+ //// If the user has chosen to override the file node, respect that
+ //var overridenFileMode = metadata?.GetLinuxFileMode();
+
+ //if (overridenFileMode != null)
+ //{
+ // // We expect the user to specify the file mode in its octal representation,
+ // // such as 755. We don't expect users to specify the higher bits (e.g.
+ // // S_IFREG).
+ // try
+ // {
+ // mode = (LinuxFileMode)Convert.ToUInt32(overridenFileMode, 8);
+
+ // LinuxFileMode fileType = mode & LinuxFileMode.FileTypeMask;
+
+ // if (fileType != LinuxFileMode.None && fileType != defaultFileTypeMask)
+ // {
+ // //this.Log.LogWarning($"An invalid file type of '{fileType}' has been set for file '{name}'. The file type will be reset to {defaultFileTypeMask}.");
+ // }
+
+ // // Override the file type mask and hardcode it to the file type mask from the default mode.
+ // // In practice this will ensure the S_IFREG or S_IFDIR flag is set.
+ // mode = (mode & ~LinuxFileMode.FileTypeMask) | defaultFileTypeMask;
+ // }
+ // catch (Exception)
+ // {
+ // throw new Exception($"Could not parse the file mode '{overridenFileMode}' for file '{name}'. Make sure to set the LinuxFileMode attriubute to an octal representation of a Unix file mode.");
+ // }
+ //}
+
+ //return mode;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/AssemblyAttributes.cs b/DebUOS/Packaging.Targets/AssemblyAttributes.cs
new file mode 100644
index 0000000..9a1ba6f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/AssemblyAttributes.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Packaging.DebUOS")]
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/CopyToDirectoryValue.cs b/DebUOS/Packaging.Targets/CopyToDirectoryValue.cs
new file mode 100644
index 0000000..576d7fb
--- /dev/null
+++ b/DebUOS/Packaging.Targets/CopyToDirectoryValue.cs
@@ -0,0 +1,23 @@
+namespace Packaging.Targets
+{
+ ///
+ /// Determines whether a file is copied to its output directory or not.
+ ///
+ internal enum CopyToDirectoryValue
+ {
+ ///
+ /// The file is never copied.
+ ///
+ DoNotCopy,
+
+ ///
+ /// The file is always copied.
+ ///
+ Always,
+
+ ///
+ /// The file is copied only if it is newer.
+ ///
+ PreserveNewest
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Deb/ControlFileParser.cs b/DebUOS/Packaging.Targets/Deb/ControlFileParser.cs
new file mode 100644
index 0000000..884fe11
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Deb/ControlFileParser.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Packaging.Targets.Deb
+{
+ ///
+ /// Supports reading and writing Debian control files.
+ ///
+ ///
+ internal static class ControlFileParser
+ {
+ ///
+ /// Reads a control file from a .
+ ///
+ ///
+ /// A which represents the control file.
+ ///
+ ///
+ /// A which represents the contents of the control file.
+ ///
+ internal static Dictionary Read(Stream stream)
+ {
+ Dictionary values = new Dictionary();
+
+ using (StreamReader reader = new StreamReader(stream, Encoding.UTF8, false, bufferSize: 1024, leaveOpen: true))
+ {
+ string line;
+ string currentKey = null;
+
+ while (reader.Peek() > 0)
+ {
+ line = reader.ReadLine();
+
+ if (line.StartsWith("#"))
+ {
+ continue;
+ }
+
+ if (line.StartsWith(" ") || line.StartsWith("\t"))
+ {
+ // Continuation line
+ var value = values[currentKey];
+ value += '\n';
+
+ if (line.Trim() != ".")
+ {
+ value += line.Trim();
+ }
+
+ values[currentKey] = value;
+ }
+ else
+ {
+ string[] parts = line.Split(new char[] { ':' }, 2);
+ currentKey = parts[0].Trim();
+ string value = parts[1].Trim();
+
+ values.Add(currentKey, value);
+ }
+ }
+ }
+
+ return values;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Deb/DebPackage.cs b/DebUOS/Packaging.Targets/Deb/DebPackage.cs
new file mode 100644
index 0000000..89e4538
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Deb/DebPackage.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+
+namespace Packaging.Targets.Deb
+{
+ ///
+ /// Represents a Debian installer package.
+ ///
+ internal class DebPackage
+ {
+ ///
+ /// Gets or sets the Debian installer file format used.
+ ///
+ public Version PackageFormatVersion
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the value of the control file.
+ ///
+ public Dictionary ControlFile
+ {
+ get;
+ set;
+ }
+
+ public Dictionary ControlExtras { get; set; }
+
+ public Dictionary Md5Sums { get; set; }
+
+ public string PreInstallScript { get; set; }
+
+ public string PostInstallScript { get; set; }
+
+ public string PreRemoveScript { get; set; }
+
+ public string PostRemoveScript { get; set; }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Deb/DebPackageControlFileData.cs b/DebUOS/Packaging.Targets/Deb/DebPackageControlFileData.cs
new file mode 100644
index 0000000..2de2b6e
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Deb/DebPackageControlFileData.cs
@@ -0,0 +1,15 @@
+using Packaging.Targets.IO;
+
+namespace Packaging.Targets.Deb
+{
+ ///
+ /// Needed for entries we don't particularry care about
+ /// during package generation, such as `shlibs`
+ ///
+ public class DebPackageControlFileData
+ {
+ public LinuxFileMode Mode { get; set; }
+
+ public string Contents { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/Deb/DebPackageCreator.cs b/DebUOS/Packaging.Targets/Deb/DebPackageCreator.cs
new file mode 100644
index 0000000..2ac3d31
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Deb/DebPackageCreator.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text;
+using Packaging.Targets.IO;
+
+namespace Packaging.Targets.Deb
+{
+ internal class DebPackageCreator
+ {
+ private const LinuxFileMode ArFileMode = LinuxFileMode.S_IRUSR | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IRGRP |
+ LinuxFileMode.S_IROTH | LinuxFileMode.S_IFREG;
+
+ public static DebPackage BuildDebPackage(
+ List archiveEntries,
+ string name,
+ string description,
+ string maintainer,
+ string version,
+ string arch,
+ bool createUser,
+ string userName,
+ bool installService,
+ string serviceName,
+ string prefix,
+ string section,
+ string priority,
+ string homepage,
+ string preInstallScript,
+ string postInstallScript,
+ string preRemoveScript,
+ string postRemoveScript,
+ IEnumerable additionalDependencies,
+ IEnumerable recommends,
+ Action additionalMetadata)
+ {
+ var pkg = new DebPackage
+ {
+ Md5Sums = archiveEntries.Where(e => e.Md5Hash != null)
+ .ToDictionary(
+ e => e.TargetPath.TrimStart('.', '/'),
+ e => BitConverter.ToString(e.Md5Hash).ToLower().Replace("-", string.Empty)),
+ PackageFormatVersion = new Version(2, 0),
+ ControlFile = new Dictionary
+ {
+ ["Package"] = name,
+ ["Version"] = version,
+ ["Architecture"] = arch,
+ ["Maintainer"] = maintainer,
+ ["Description"] = description,
+ ["Installed-Size"] = (archiveEntries.Sum(e => e.FileSize) / 1024).ToString()
+ }
+ };
+
+ if (!string.IsNullOrEmpty(section))
+ {
+ pkg.ControlFile["Section"] = section;
+ }
+
+ if (!string.IsNullOrEmpty(priority))
+ {
+ pkg.ControlFile["Priority"] = priority;
+ }
+
+ if (!string.IsNullOrEmpty(homepage))
+ {
+ pkg.ControlFile["Homepage"] = homepage;
+ }
+
+ if (createUser)
+ {
+ // Add the user and group, under which the service runs.
+ // These users are never removed because UIDs are re-used on Linux.
+ pkg.PreInstallScript += $"/usr/sbin/groupadd -r {userName} 2>/dev/null || :\n" +
+ $"/usr/sbin/useradd -g {userName} -s /sbin/nologin -r {userName} 2>/dev/null || :\n";
+ }
+
+ if (installService)
+ {
+ // Install and activate the service.
+ pkg.PostInstallScript += $"systemctl daemon-reload\n";
+ pkg.PostInstallScript += $"systemctl enable --now {serviceName}.service\n";
+ pkg.PreRemoveScript += $"systemctl --no-reload disable --now {serviceName}.service\n";
+ }
+
+ // Remove all directories marked as such (these are usually directories which contain temporary files)
+ foreach (var entryToRemove in archiveEntries.Where(e => e.RemoveOnUninstall))
+ {
+ pkg.PostRemoveScript += $"/bin/rm -rf {entryToRemove.TargetPath}\n";
+ }
+
+ if (!string.IsNullOrEmpty(preInstallScript))
+ {
+ pkg.PreInstallScript += preInstallScript;
+
+ if (!preInstallScript.EndsWith("\n"))
+ {
+ pkg.PreInstallScript += "\n";
+ }
+ }
+
+ if (!string.IsNullOrEmpty(postInstallScript))
+ {
+ pkg.PostInstallScript += postInstallScript;
+
+ if (!postInstallScript.EndsWith("\n"))
+ {
+ pkg.PostInstallScript += "\n";
+ }
+ }
+
+ if (!string.IsNullOrEmpty(preRemoveScript))
+ {
+ pkg.PreRemoveScript += preRemoveScript;
+
+ if (!preRemoveScript.EndsWith("\n"))
+ {
+ pkg.PreRemoveScript += "\n";
+ }
+ }
+
+ if (!string.IsNullOrEmpty(postRemoveScript))
+ {
+ pkg.PostRemoveScript += postRemoveScript;
+
+ if (!postRemoveScript.EndsWith("\n"))
+ {
+ pkg.PostRemoveScript += "\n";
+ }
+ }
+
+ if (additionalDependencies != null && additionalDependencies.Any())
+ {
+ pkg.ControlFile["Depends"] = string.Join(", ", additionalDependencies);
+ }
+
+ if (recommends != null && recommends.Any())
+ {
+ pkg.ControlFile["Recommends"] = string.Join(", ", recommends);
+ }
+
+ additionalMetadata?.Invoke(pkg);
+
+ return pkg;
+ }
+
+ public static void WriteDebPackage(
+ List archiveEntries,
+ Stream tarXzStream,
+ Stream targetStream,
+ DebPackage pkg)
+ {
+ ArFileCreator.WriteMagic(targetStream);
+ ArFileCreator.WriteEntry(targetStream, "debian-binary", ArFileMode, pkg.PackageFormatVersion + "\n");
+ WriteControl(targetStream, pkg, archiveEntries);
+ ArFileCreator.WriteEntry(targetStream, "data.tar.xz", ArFileMode, tarXzStream);
+ }
+
+ private static void WriteControl(Stream targetStream, DebPackage pkg, List entries)
+ {
+ var controlTar = new MemoryStream();
+ WriteControlEntry(controlTar, "./");
+ WriteControlEntry(
+ controlTar,
+ "./control",
+ string.Join("\n", pkg.ControlFile
+ .OrderByDescending(x => x.Key == "Package").ThenBy(x => x.Key)
+ .Select(x => $"{x.Key}: {x.Value}")) + "\n");
+
+ WriteControlEntry(
+ controlTar,
+ "./md5sums",
+ string.Join("\n", pkg.Md5Sums.Select(x => $"{x.Value} {x.Key}")) + "\n");
+
+ var execMode = LinuxFileMode.S_IRUSR | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IXUSR |
+ LinuxFileMode.S_IRGRP | LinuxFileMode.S_IROTH;
+
+ if (!string.IsNullOrWhiteSpace(pkg.PreInstallScript))
+ {
+ WriteControlEntry(controlTar, "./preinst", $"#!/bin/sh\n{pkg.PreInstallScript}\n", execMode);
+ }
+
+ if (!string.IsNullOrWhiteSpace(pkg.PostInstallScript))
+ {
+ WriteControlEntry(controlTar, "./postinst", $"#!/bin/sh\n{pkg.PostInstallScript}\n", execMode);
+ }
+
+ if (!string.IsNullOrWhiteSpace(pkg.PreRemoveScript))
+ {
+ WriteControlEntry(controlTar, "./prerm", $"#!/bin/sh\n{pkg.PreRemoveScript}\n", execMode);
+ }
+
+ if (!string.IsNullOrWhiteSpace(pkg.PostRemoveScript))
+ {
+ WriteControlEntry(controlTar, "./postrm", $"#!/bin/sh\n{pkg.PostRemoveScript}\n", execMode);
+ }
+
+ var confFiles = entries
+ .Where(e => e.Mode.HasFlag(LinuxFileMode.S_IFREG) && e.TargetPath.StartsWith("/etc/"))
+ .Select(e => e.TargetPath).ToList();
+ if (confFiles.Any())
+ {
+ WriteControlEntry(controlTar, "./conffiles", string.Join("\n", confFiles) + "\n");
+ }
+
+ TarFileCreator.WriteTrailer(controlTar);
+ controlTar.Seek(0, SeekOrigin.Begin);
+
+ var controlTarGz = new MemoryStream();
+ using (var gzStream = new GZipStream(controlTarGz, CompressionMode.Compress, true))
+ {
+ controlTar.CopyTo(gzStream);
+ }
+
+ controlTarGz.Seek(0, SeekOrigin.Begin);
+ ArFileCreator.WriteEntry(targetStream, "control.tar.gz", ArFileMode, controlTarGz);
+ }
+
+ private static void WriteControlEntry(Stream tar, string name, string data = null, LinuxFileMode? fileMode = null)
+ {
+ var s = (data != null) ? new MemoryStream(Encoding.UTF8.GetBytes(data)) : new MemoryStream();
+ var mode = fileMode ?? LinuxFileMode.S_IRUSR | LinuxFileMode.S_IWUSR |
+ LinuxFileMode.S_IRGRP | LinuxFileMode.S_IROTH;
+ mode |= data == null
+ ? LinuxFileMode.S_IFDIR | LinuxFileMode.S_IXUSR | LinuxFileMode.S_IXGRP | LinuxFileMode.S_IXOTH
+ : LinuxFileMode.S_IFREG;
+ var hdr = new TarHeader
+ {
+ FileMode = mode,
+ FileName = name,
+ FileSize = (uint)s.Length,
+ GroupName = "root",
+ UserName = "root",
+ LastModified = DateTimeOffset.UtcNow,
+ Magic = "ustar",
+ TypeFlag = data == null ? TarTypeFlag.DirType : TarTypeFlag.RegType,
+ };
+ TarFileCreator.WriteEntry(tar, hdr, s);
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Deb/DebPackageReader.cs b/DebUOS/Packaging.Targets/Deb/DebPackageReader.cs
new file mode 100644
index 0000000..20a08a3
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Deb/DebPackageReader.cs
@@ -0,0 +1,157 @@
+using Packaging.Targets.IO;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+
+namespace Packaging.Targets.Deb
+{
+ ///
+ /// Reads objects from a .
+ ///
+ internal static class DebPackageReader
+ {
+ ///
+ /// Reads a from a .
+ ///
+ ///
+ /// The from which to read the package.
+ ///
+ ///
+ /// A which represents the package.
+ ///
+ internal static DebPackage Read(Stream stream)
+ {
+ DebPackage package = new DebPackage();
+ using (ArFile archive = new ArFile(stream, leaveOpen: true))
+ {
+ while (archive.Read())
+ {
+ if (archive.FileName == "debian-binary")
+ {
+ ReadDebianBinary(archive, package);
+ }
+ else if (archive.FileName == "control.tar.gz")
+ {
+ ReadControlArchive(archive, package);
+ }
+ }
+ }
+
+ return package;
+ }
+
+ internal static Stream GetPayloadStream(Stream stream)
+ {
+ using (ArFile archive = new ArFile(stream, leaveOpen: true))
+ {
+ while (archive.Read())
+ {
+ if (archive.FileName.StartsWith("data.tar."))
+ {
+ var ext = Path.GetExtension(archive.FileName);
+ if (ext == ".gz")
+ {
+ return new GZipDecompressor(archive.Open(), false);
+ }
+
+ if (ext == ".xz")
+ {
+ // For some reason it complains about corrupted data when we try to read using smaller chunks
+ var payload = new MemoryStream();
+ using (var xz = new XZInputStream(archive.Open()))
+ {
+ xz.CopyTo(payload);
+ payload.Seek(0, SeekOrigin.Begin);
+ return payload;
+ }
+ }
+
+ throw new InvalidDataException("Don't know how to decompress " + archive.FileName);
+ }
+ }
+
+ throw new InvalidDataException("data.tar.?? not found");
+ }
+ }
+
+ ///
+ /// Reads and parses the debian-binary file in the Debian archive.
+ ///
+ ///
+ /// The archive to update with the data read from the debian-binary file.
+ ///
+ ///
+ /// The package to update.
+ ///
+ private static void ReadDebianBinary(ArFile archive, DebPackage package)
+ {
+ using (Stream stream = archive.Open())
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ var content = reader.ReadToEnd();
+ package.PackageFormatVersion = new Version(content);
+ }
+ }
+
+ private static void ReadControlArchive(ArFile archive, DebPackage package)
+ {
+ package.ControlExtras = new Dictionary();
+ package.Md5Sums = new Dictionary();
+ using (Stream stream = archive.Open())
+ using (GZipDecompressor decompressedStream = new GZipDecompressor(stream, leaveOpen: true))
+ using (TarFile tarFile = new TarFile(decompressedStream, leaveOpen: true))
+ {
+ while (tarFile.Read())
+ {
+ switch (tarFile.FileName)
+ {
+ case "./control":
+ using (Stream controlFile = tarFile.Open())
+ {
+ package.ControlFile = ControlFileParser.Read(controlFile);
+ }
+
+ break;
+ case "./md5sums":
+ using (var sums = new StreamReader(tarFile.Open()))
+ {
+ string line;
+ while ((line = sums.ReadLine()) != null)
+ {
+ var s = line.Split(new[] { " " }, 2, StringSplitOptions.None);
+ package.Md5Sums[s[1]] = s[0];
+ }
+ }
+
+ break;
+ case "./preinst":
+ package.PreInstallScript = tarFile.ReadAsUtf8String();
+ break;
+ case "./postinst":
+ package.PostInstallScript = tarFile.ReadAsUtf8String();
+ break;
+ case "./prerm":
+ package.PreRemoveScript = tarFile.ReadAsUtf8String();
+ break;
+ case "./postrm":
+ package.PostRemoveScript = tarFile.ReadAsUtf8String();
+ break;
+
+ case "./":
+ tarFile.Skip();
+ break;
+ default:
+ package.ControlExtras[tarFile.FileName] = new DebPackageControlFileData
+ {
+ Mode = tarFile.FileHeader.FileMode,
+ Contents = tarFile.ReadAsUtf8String()
+ };
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/ArFile.cs b/DebUOS/Packaging.Targets/IO/ArFile.cs
new file mode 100644
index 0000000..2b91fe6
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/ArFile.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents an ar archive, the format used by Debian installer packages.
+ ///
+ ///
+ public class ArFile : ArchiveFile
+ {
+ ///
+ /// The magic used to identify ar files.
+ ///
+ private const string Magic = "!\n";
+
+ ///
+ /// Tracks whether the magic has been read or not.
+ ///
+ private bool magicRead;
+
+ ///
+ /// The header of the current entry.
+ ///
+ private ArHeader entryHeader;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A which represents the CPIO data.
+ ///
+ ///
+ /// to leave the underlying open when this
+ /// is disposed of; otherwise, .
+ ///
+ public ArFile(Stream stream, bool leaveOpen)
+ : base(stream, leaveOpen)
+ {
+ }
+
+ ///
+ public override bool Read()
+ {
+ this.EnsureMagicRead();
+
+ if (this.EntryStream != null)
+ {
+ this.EntryStream.Dispose();
+ }
+
+ this.Align(2);
+
+ if (this.Stream.Position == this.Stream.Length)
+ {
+ return false;
+ }
+
+ this.entryHeader = this.Stream.ReadStruct();
+ this.FileHeader = this.entryHeader;
+ this.FileName = this.entryHeader.FileName;
+
+ if (this.entryHeader.EndChar != "`\n")
+ {
+ throw new InvalidDataException("The magic for the file entry is invalid");
+ }
+
+ this.EntryStream = new SubStream(this.Stream, this.Stream.Position, this.entryHeader.FileSize, leaveParentOpen: true);
+
+ return true;
+ }
+
+ ///
+ /// Reads the magic if it has not been read previously.
+ ///
+ protected void EnsureMagicRead()
+ {
+ if (!this.magicRead)
+ {
+ byte[] buffer = new byte[Magic.Length];
+ this.Stream.Read(buffer, 0, buffer.Length);
+ var magic = Encoding.ASCII.GetString(buffer);
+
+ if (!string.Equals(magic, Magic, StringComparison.Ordinal))
+ {
+ throw new InvalidDataException("The .ar file did not start with the expected magic");
+ }
+
+ this.magicRead = true;
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/ArFileCreator.cs b/DebUOS/Packaging.Targets/IO/ArFileCreator.cs
new file mode 100644
index 0000000..e41e5de
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/ArFileCreator.cs
@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ public static class ArFileCreator
+ {
+ public static void WriteMagic(Stream output)
+ {
+ var wr = new StreamWriter(output);
+ wr.Write("!\n");
+ wr.Flush();
+ }
+
+ public static void WriteEntry(Stream output, string name, LinuxFileMode mode, string data)
+ => WriteEntry(output, name, mode, new MemoryStream(Encoding.UTF8.GetBytes(data)));
+
+ public static void WriteEntry(Stream output, string name, LinuxFileMode mode, Stream data)
+ {
+ var hdr = new ArHeader
+ {
+ EndChar = "`\n",
+ FileMode = mode,
+ FileName = name,
+ FileSize = (uint)data.Length,
+ GroupId = 0,
+ OwnerId = 0,
+ LastModified = DateTimeOffset.UtcNow
+ };
+ WriteEntry(output, hdr, data);
+ }
+
+ public static void WriteEntry(Stream output, ArHeader header, Stream data)
+ {
+ output.WriteStruct(header);
+ data.CopyTo(output);
+ if (output.Position % 2 != 0)
+ {
+ output.WriteByte(0);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/IO/ArHeader.cs b/DebUOS/Packaging.Targets/IO/ArHeader.cs
new file mode 100644
index 0000000..7e09a6f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/ArHeader.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// The header of an entry in an .
+ ///
+ public struct ArHeader : IArchiveHeader
+ {
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 16)]
+ private byte[] fileName;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 12)]
+ private byte[] lastModified;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 6)]
+ private byte[] ownerId;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 6)]
+ private byte[] groupId;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)]
+ private byte[] fileMode;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 10)]
+ private byte[] fileSize;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 2)]
+ private byte[] endChar;
+
+ ///
+ /// Gets or sets the name of the current file.
+ ///
+ public string FileName
+ {
+ get => this.GetString(this.fileName, 16).Trim();
+ set => this.fileName = this.CreateString(value, 16);
+ }
+
+ ///
+ /// Gets or sets the date at which the current file was last modified.
+ ///
+ public DateTimeOffset LastModified
+ {
+ get => DateTimeOffset.FromUnixTimeSeconds(int.Parse(this.GetString(this.lastModified, 12).Trim()));
+ set => this.lastModified = this.CreateString(value.ToUnixTimeSeconds().ToString(), 12);
+ }
+
+ ///
+ /// Gets or sets the user ID of the owner of the file.
+ ///
+ public uint OwnerId
+ {
+ get => this.ReadUInt(this.ownerId);
+ set => this.ownerId = this.CreateString(value.ToString(), 6);
+ }
+
+ ///
+ /// Gets or sets group ID of the owner of the file.
+ ///
+ public uint GroupId
+ {
+ get => this.ReadUInt(this.groupId);
+ set => this.groupId = this.CreateString(value.ToString(), 6);
+ }
+
+ ///
+ public LinuxFileMode FileMode
+ {
+ get => (LinuxFileMode)Convert.ToUInt32(this.GetString(this.fileMode, 8).Trim(), 8);
+ set => this.fileMode = this.CreateString(Convert.ToString((uint)value, 8), 8);
+ }
+
+ ///
+ public uint FileSize
+ {
+ get => this.ReadUInt(this.fileSize);
+ set => this.fileSize = this.CreateString(value.ToString(), 10);
+ }
+
+ ///
+ /// Gets or sets the value of the endchar field. This field is used as a checksum.
+ ///
+ public string EndChar
+ {
+ get => this.GetString(this.endChar, 2);
+ set => this.endChar = this.CreateString(value, 2);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return this.FileName;
+ }
+
+ private string GetString(byte[] data, int maxLen)
+ {
+ int len;
+ for (len = 0; len < maxLen; len++)
+ {
+ if (data[len] == 0)
+ {
+ break;
+ }
+ }
+
+ if (len == 0)
+ {
+ return null;
+ }
+
+ return Encoding.UTF8.GetString(data, 0, len);
+ }
+
+ private uint ReadUInt(byte[] data) => Convert.ToUInt32(this.GetString(data, data.Length).Trim());
+
+ private byte[] CreateString(string s, int len)
+ {
+ var target = new byte[len];
+ if (s == null)
+ {
+ return target;
+ }
+
+ var buffer = Encoding.UTF8.GetBytes(s);
+ if (buffer.Length > len)
+ {
+ throw new Exception($"String {s} exceeds the limit of {len}");
+ }
+
+ for (var c = 0; c < len; c++)
+ {
+ target[c] = (c < buffer.Length) ? buffer[c] : (byte)0x20;
+ }
+
+ return target;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/ArchiveEntry.cs b/DebUOS/Packaging.Targets/IO/ArchiveEntry.cs
new file mode 100644
index 0000000..2704259
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/ArchiveEntry.cs
@@ -0,0 +1,132 @@
+using System;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents an entry in a Unix archive (usually a CPIO or tar archive).
+ ///
+ public class ArchiveEntry
+ {
+ ///
+ /// Gets or sets the full path of the file or directory on the target file system.
+ ///
+ public string TargetPath
+ { get; set; }
+
+ ///
+ /// Gets or sets the name of the owner of the file or directory.
+ ///
+ public string Owner
+ { get; set; }
+
+ ///
+ /// Gets or sets the name of the group of the file or directory.
+ ///
+ public string Group
+ { get; set; }
+
+ ///
+ /// Gets or sets the file mode of the file.
+ ///
+ public LinuxFileMode Mode
+ { get; set; }
+
+ ///
+ /// Gets or sets the full path to the file on the source operating system.
+ ///
+ public string SourceFilename
+ { get; set; }
+
+ ///
+ /// Gets or sets the total file size, in bytes.
+ ///
+ public uint FileSize
+ { get; set; }
+
+ ///
+ /// Gets or sets the date at which the file was last modified.
+ ///
+ public DateTimeOffset Modified
+ { get; set; }
+
+ ///
+ /// Gets or sets the SHA256 of the file contents.
+ ///
+ public byte[] Sha256
+ { get; set; }
+
+ ///
+ /// Gets or sets the MD5 of the file contents.
+ ///
+ public byte[] Md5Hash
+ { get; set; }
+
+ ///
+ /// Gets or sets the file type (executable or not).
+ ///
+ public ArchiveEntryType Type
+ { get; set; }
+
+ ///
+ /// Gets or sets, if the file is a link, the link target.
+ ///
+ public string LinkTo
+ { get; set; }
+
+ ///
+ /// Gets or sets the inode number for the file.
+ ///
+ public uint Inode
+ { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the file only contains ASCII characters.
+ ///
+ public bool IsAscii
+ { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this entry (and any child entries) is forcefully
+ /// removed when the application is uninstalled.
+ ///
+ public bool RemoveOnUninstall
+ { get; set; }
+
+ ///
+ /// Gets the file name, including a final slash if the file is a directory.
+ /// Used when sorting entries to make sure a directory entry immediately preceeds
+ /// all of its children.
+ ///
+ public string TargetPathWithFinalSlash
+ {
+ get
+ {
+ if (this.Mode.HasFlag(LinuxFileMode.S_IFDIR) && !this.TargetPath.EndsWith("/"))
+ {
+ return this.TargetPath + "/";
+ }
+ else
+ {
+ return this.TargetPath;
+ }
+ }
+ }
+
+ ///
+ /// Gets the file name, excluding a final slash even if the file is a directory.
+ ///
+ public string TargetPathWithoutFinalSlash
+ {
+ get
+ {
+ return this.TargetPath?.TrimEnd('/');
+ }
+ }
+
+ ///
+ public override string ToString()
+ {
+ return this.TargetPath;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/ArchiveEntryType.cs b/DebUOS/Packaging.Targets/IO/ArchiveEntryType.cs
new file mode 100644
index 0000000..4010595
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/ArchiveEntryType.cs
@@ -0,0 +1,23 @@
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents the type of entry in an archive.
+ ///
+ public enum ArchiveEntryType
+ {
+ ///
+ /// The file as no special type.
+ ///
+ None = 0,
+
+ ///
+ /// The file is a 32-bit executable.
+ ///
+ Executable32 = 1,
+
+ ///
+ /// The file is a 64-bit executable.
+ ///
+ Executable64 = 2
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/ArchiveFile.cs b/DebUOS/Packaging.Targets/IO/ArchiveFile.cs
new file mode 100644
index 0000000..6245b07
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/ArchiveFile.cs
@@ -0,0 +1,175 @@
+using System;
+using System.IO;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Base class for archive files, such as CPIO files or ar files.
+ ///
+ public abstract class ArchiveFile : IDisposable
+ {
+ ///
+ /// The around which this wraps.
+ ///
+ private readonly Stream stream;
+
+ ///
+ /// Indicates whether should be disposed of when this is disposed of.
+ ///
+ private readonly bool leaveOpen;
+
+ ///
+ /// Tracks whether this has been disposed of or not.
+ ///
+ private bool disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A which represents the archive data.
+ ///
+ ///
+ /// to leave the underlying open when this
+ /// is disposed of; otherwise, .
+ ///
+ public ArchiveFile(Stream stream, bool leaveOpen)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ this.stream = stream;
+ this.leaveOpen = leaveOpen;
+ }
+
+ ///
+ /// Gets or sets a which represents the current entry.
+ ///
+ public SubStream EntryStream
+ {
+ get;
+ protected set;
+ }
+
+ ///
+ /// Gets or sets the name of the current entry.
+ ///
+ public string FileName
+ {
+ get;
+ protected set;
+ }
+
+ ///
+ /// Gets or sets the header for the current file.
+ ///
+ public IArchiveHeader FileHeader
+ {
+ get;
+ protected set;
+ }
+
+ ///
+ /// Gets the which underpins this .
+ ///
+ protected Stream Stream
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return this.stream;
+ }
+ }
+
+ public static int PaddingSize(int multiple, int value)
+ {
+ if (value % multiple == 0)
+ {
+ return 0;
+ }
+ else
+ {
+ return multiple - value % multiple;
+ }
+ }
+
+ ///
+ /// Returns a which represents the content of the current entry in the .
+ ///
+ ///
+ /// A which represents the content of the current entry in the .
+ ///
+ public Stream Open()
+ {
+ return this.EntryStream;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!this.leaveOpen)
+ {
+ this.Stream.Dispose();
+ }
+
+ this.disposed = true;
+ }
+
+ ///
+ /// Reads the next entry in the archive.
+ ///
+ ///
+ /// if an entry is present, if no more entries
+ /// could be found in the archive.
+ ///
+ public abstract bool Read();
+
+ ///
+ /// Skips the current entry, without reading the entry data.
+ ///
+ public void Skip()
+ {
+ byte[] buffer = new byte[60 * 1024];
+
+ while (this.EntryStream.Read(buffer, 0, buffer.Length) > 0)
+ {
+ // Keep reading until we're at the end of the stream.
+ }
+ }
+
+ ///
+ /// Throws a if has been previously called.
+ ///
+ protected void EnsureNotDisposed()
+ {
+ if (this.disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().Name);
+ }
+ }
+
+ ///
+ /// Aligns the current position.
+ ///
+ ///
+ /// The value to which to align.
+ ///
+ protected void Align(int alignmentBase)
+ {
+ var currentIndex =
+ (int)(this.EntryStream != null ? (this.EntryStream.Offset + this.EntryStream.Length) : this.Stream.Position);
+
+ if (this.Stream.CanSeek)
+ {
+ this.Stream.Seek(currentIndex + PaddingSize(alignmentBase, currentIndex), SeekOrigin.Begin);
+ }
+ else
+ {
+ byte[] buffer = new byte[PaddingSize(alignmentBase, currentIndex)];
+ this.Stream.Read(buffer, 0, buffer.Length);
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/ConcatStream.cs b/DebUOS/Packaging.Targets/IO/ConcatStream.cs
new file mode 100644
index 0000000..77eebee
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/ConcatStream.cs
@@ -0,0 +1,271 @@
+// Copyright (c) 2008-2011, Kenneth Bell
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+
+namespace DiscUtils.Internal
+{
+ ///
+ /// The concatenation of multiple streams (read-only, for now).
+ ///
+ internal class ConcatStream : Stream
+ {
+ private readonly bool canWrite;
+ private readonly bool leaveOpen;
+
+ private long position;
+ private Stream[] streams;
+
+ public ConcatStream(params Stream[] streams)
+ : this(false, streams)
+ {
+ }
+
+ public ConcatStream(bool leaveOpen, params Stream[] streams)
+ {
+ this.streams = streams;
+ this.leaveOpen = leaveOpen;
+
+ // Only allow writes if all streams can be written
+ this.canWrite = true;
+ foreach (Stream stream in streams)
+ {
+ if (!stream.CanWrite)
+ {
+ this.canWrite = false;
+ }
+ }
+ }
+
+ public override bool CanRead
+ {
+ get
+ {
+ this.CheckDisposed();
+ return true;
+ }
+ }
+
+ public override bool CanSeek
+ {
+ get
+ {
+ this.CheckDisposed();
+ return true;
+ }
+ }
+
+ public override bool CanWrite
+ {
+ get
+ {
+ this.CheckDisposed();
+ return this.canWrite;
+ }
+ }
+
+ public override long Length
+ {
+ get
+ {
+ this.CheckDisposed();
+ long length = 0;
+ for (int i = 0; i < this.streams.Length; ++i)
+ {
+ length += this.streams[i].Length;
+ }
+
+ return length;
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ this.CheckDisposed();
+ return this.position;
+ }
+
+ set
+ {
+ this.CheckDisposed();
+ this.position = value;
+ }
+ }
+
+ public override void Flush()
+ {
+ this.CheckDisposed();
+ for (int i = 0; i < this.streams.Length; ++i)
+ {
+ this.streams[i].Flush();
+ }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ this.CheckDisposed();
+
+ int totalRead = 0;
+ int numRead = 0;
+
+ do
+ {
+ long activeStreamStartPos;
+ int activeStream = this.GetActiveStream(out activeStreamStartPos);
+
+ this.streams[activeStream].Position = this.position - activeStreamStartPos;
+
+ numRead = this.streams[activeStream].Read(buffer, offset + totalRead, count - totalRead);
+
+ totalRead += numRead;
+ this.position += numRead;
+ }
+ while (numRead != 0);
+
+ return totalRead;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ this.CheckDisposed();
+
+ long effectiveOffset = offset;
+ if (origin == SeekOrigin.Current)
+ {
+ effectiveOffset += this.position;
+ }
+ else if (origin == SeekOrigin.End)
+ {
+ effectiveOffset += this.Length;
+ }
+
+ if (effectiveOffset < 0)
+ {
+ throw new IOException("Attempt to move before beginning of disk");
+ }
+
+ this.Position = effectiveOffset;
+ return this.Position;
+ }
+
+ public override void SetLength(long value)
+ {
+ this.CheckDisposed();
+
+ long lastStreamOffset;
+ int lastStream = this.GetStream(this.Length, out lastStreamOffset);
+ if (value < lastStreamOffset)
+ {
+ throw new IOException($"Unable to reduce stream length to less than {lastStreamOffset}");
+ }
+
+ this.streams[lastStream].SetLength(value - lastStreamOffset);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ this.CheckDisposed();
+
+ int totalWritten = 0;
+ while (totalWritten != count)
+ {
+ // Offset of the stream = streamOffset
+ long streamOffset;
+ int streamIdx = this.GetActiveStream(out streamOffset);
+
+ // Offset within the stream = streamPos
+ long streamPos = this.position - streamOffset;
+ this.streams[streamIdx].Position = streamPos;
+
+ // Write (limited to the stream's length), except for final stream - that may be
+ // extendable
+ int numToWrite;
+ if (streamIdx == this.streams.Length - 1)
+ {
+ numToWrite = count - totalWritten;
+ }
+ else
+ {
+ numToWrite = (int)Math.Min(count - totalWritten, this.streams[streamIdx].Length - streamPos);
+ }
+
+ this.streams[streamIdx].Write(buffer, offset + totalWritten, numToWrite);
+
+ totalWritten += numToWrite;
+ this.position += numToWrite;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ try
+ {
+ if (disposing && this.streams != null)
+ {
+ if (!this.leaveOpen)
+ {
+ foreach (Stream stream in this.streams)
+ {
+ stream.Dispose();
+ }
+ }
+
+ this.streams = null;
+ }
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+
+ private int GetActiveStream(out long startPos)
+ {
+ return this.GetStream(this.position, out startPos);
+ }
+
+ private int GetStream(long targetPos, out long streamStartPos)
+ {
+ // Find the stream that _position is within
+ streamStartPos = 0;
+ int focusStream = 0;
+ while (focusStream < this.streams.Length - 1 && streamStartPos + this.streams[focusStream].Length <= targetPos)
+ {
+ streamStartPos += this.streams[focusStream].Length;
+ focusStream++;
+ }
+
+ return focusStream;
+ }
+
+ private void CheckDisposed()
+ {
+ if (this.streams == null)
+ {
+ throw new ObjectDisposedException(nameof(ConcatStream));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/IO/CpioFile.cs b/DebUOS/Packaging.Targets/IO/CpioFile.cs
new file mode 100644
index 0000000..3d25007
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/CpioFile.cs
@@ -0,0 +1,187 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// The cpio archive format collects any number of files, directories, and
+ /// other file system objects(symbolic links, device nodes, etc.) into a
+ /// single stream of bytes.
+ ///
+ ///
+ /// Each file system object in a cpio archive comprises a header record with
+ /// basic numeric metadata followed by the full pathname of the entry and the
+ /// file data.The header record stores a series of integer values that generally
+ /// follow the fields in struct stat. (See stat(2) for details.) The
+ /// variants differ primarily in how they store those integers(binary,
+ /// octal, or hexadecimal). The header is followed by the pathname of the
+ /// entry(the length of the pathname is stored in the header) and any file
+ /// data.The end of the archive is indicated by a special record with the
+ /// pathname TRAILER!!!.
+ ///
+ ///
+ public class CpioFile : ArchiveFile, IDisposable
+ {
+ ///
+ /// The entry which is currently available.
+ ///
+ private CpioHeader entryHeader;
+
+ ///
+ /// The position at which the data for the current entry starts.
+ ///
+ private long entryDataOffset;
+
+ ///
+ /// The length of the data for the current entry.
+ ///
+ private long entryDataLength;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A which represents the CPIO data.
+ ///
+ ///
+ /// to leave the underlying open when this
+ /// is disposed of; otherwise, .
+ ///
+ public CpioFile(Stream stream, bool leaveOpen)
+ : base(stream, leaveOpen)
+ {
+ }
+
+ ///
+ /// Gets the header of the current entry.
+ ///
+ public CpioHeader EntryHeader
+ {
+ get { return this.entryHeader; }
+ }
+
+ ///
+ /// Gets a value indicating whether the current entry is a directory.
+ ///
+ public bool IsDirectory
+ {
+ get { return this.entryDataLength == 0; }
+ }
+
+ ///
+ /// Adds an entry to the
+ ///
+ ///
+ /// A with the item metaata. The ,
+ /// and values are overwritten.
+ ///
+ ///
+ /// The file name of the entry.
+ ///
+ ///
+ /// A which contains the file data.
+ ///
+ public void Write(CpioHeader header, string name, Stream data)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+
+ byte[] nameBytes = Encoding.UTF8.GetBytes(name);
+
+ // We make sure the magic and size fields have the correct values. All other fields
+ // are the responsibility of the caller.
+ header.Signature = "070701";
+ header.NameSize = (uint)(nameBytes.Length + 1);
+ header.FileSize = (uint)data.Length;
+
+ this.Stream.WriteStruct(header);
+ this.Stream.Write(nameBytes, 0, nameBytes.Length);
+ this.Stream.WriteByte(0); // Trailing 0
+
+ // The pathname is followed by NUL bytes so that the total size of the fixed
+ // header plus pathname is a multiple of four.
+ var headerSize = Marshal.SizeOf() + (int)header.NameSize;
+ var paddingSize = PaddingSize(4, headerSize);
+
+ for (int i = 0; i < paddingSize; i++)
+ {
+ this.Stream.WriteByte(0);
+ }
+
+ data.Position = 0;
+ data.CopyTo(this.Stream);
+
+ // The file data is padded to a multiple of four bytes.
+ paddingSize = PaddingSize(4, (int)data.Length);
+
+ for (int i = 0; i < paddingSize; i++)
+ {
+ this.Stream.WriteByte(0);
+ }
+ }
+
+ ///
+ /// Writes the trailer entry.
+ ///
+ public void WriteTrailer()
+ {
+ this.Write(CpioHeader.Empty, "TRAILER!!!", new MemoryStream(Array.Empty()));
+ }
+
+ ///
+ /// Reads the next entry in the .
+ ///
+ ///
+ /// if more data is available; otherwise, .
+ ///
+ public override bool Read()
+ {
+ if (this.EntryStream != null)
+ {
+ this.EntryStream.Dispose();
+ }
+
+ this.Align(4);
+
+ this.entryHeader = this.Stream.ReadStruct();
+ this.FileHeader = this.entryHeader;
+
+ if (this.entryHeader.Signature != "070701")
+ {
+ throw new InvalidDataException("The magic for the file entry is invalid");
+ }
+
+ byte[] nameBytes = new byte[this.entryHeader.NameSize];
+ this.Stream.Read(nameBytes, 0, nameBytes.Length);
+
+ this.FileName = Encoding.UTF8.GetString(nameBytes, 0, (int)this.entryHeader.NameSize - 1);
+
+ // The pathname is followed by NUL bytes so that the total size of the fixed
+ // header plus pathname is a multiple of four.
+ var headerSize = Marshal.SizeOf() + nameBytes.Length;
+ var paddingSize = PaddingSize(4, headerSize);
+
+ if (nameBytes.Length < paddingSize)
+ {
+ nameBytes = new byte[paddingSize];
+ }
+
+ this.Stream.Read(nameBytes, 0, paddingSize);
+
+ this.entryDataOffset = this.Stream.Position;
+ this.entryDataLength = this.entryHeader.FileSize;
+ this.EntryStream = new SubStream(this.Stream, this.entryDataOffset, this.entryDataLength, leaveParentOpen: true);
+
+ return this.FileName != "TRAILER!!!";
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/CpioFileCreator.cs b/DebUOS/Packaging.Targets/IO/CpioFileCreator.cs
new file mode 100644
index 0000000..ada0014
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/CpioFileCreator.cs
@@ -0,0 +1,181 @@
+using Packaging.Targets.Rpm;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Supports generating CPIO files.
+ ///
+ public class CpioFileCreator
+ {
+ ///
+ /// The used to fetch file metadata.
+ ///
+ private IFileAnalyzer fileAnayzer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CpioFileCreator()
+ {
+ this.fileAnayzer = new FileAnalyzer();
+ }
+
+ ///
+ /// Generates a based on a list of
+ /// values.
+ ///
+ ///
+ /// The values based on which to generate the .
+ ///
+ ///
+ /// The which will hold the .
+ ///
+ public void FromArchiveEntries(List archiveEntries, Stream targetStream)
+ {
+ using (CpioFile cpioFile = new CpioFile(targetStream, leaveOpen: true))
+ {
+ foreach (var entry in archiveEntries)
+ {
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFDIR))
+ {
+ this.AddDirectory(entry, cpioFile);
+ }
+ else if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK))
+ {
+ this.AddSymlink(entry, cpioFile);
+ }
+ else
+ {
+ this.AddFile(entry, cpioFile);
+ }
+ }
+
+ cpioFile.WriteTrailer();
+ }
+ }
+
+ ///
+ /// Adds a directory entry to the .
+ ///
+ ///
+ /// The which represents the directory.
+ ///
+ ///
+ /// The to which to add the directory entry.
+ ///
+ public void AddDirectory(ArchiveEntry entry, CpioFile cpioFile)
+ {
+ // Write out an entry for the current directory
+ CpioHeader directoryHeader = new CpioHeader()
+ {
+ Check = 0,
+ DevMajor = 1,
+ DevMinor = 0,
+ FileSize = 0,
+ Gid = 0,
+ Ino = entry.Inode,
+ FileMode = entry.Mode,
+ LastModified = entry.Modified,
+ Nlink = 1,
+ RDevMajor = 0,
+ RDevMinor = 0,
+ Signature = "070701",
+ Uid = 0,
+ NameSize = 0
+ };
+
+ var targetPath = entry.TargetPath;
+ if (!targetPath.StartsWith("."))
+ {
+ targetPath = "." + targetPath;
+ }
+
+ cpioFile.Write(directoryHeader, targetPath, new MemoryStream(Array.Empty()));
+ }
+
+ ///
+ /// Adds a symlink entry to a .
+ ///
+ ///
+ /// The symlink entry to add.
+ ///
+ ///
+ /// The to which to add the entry.
+ ///
+ public void AddSymlink(ArchiveEntry entry, CpioFile cpioFile)
+ {
+ var targetPath = entry.TargetPath;
+
+ if (!targetPath.StartsWith("."))
+ {
+ targetPath = "." + targetPath;
+ }
+
+ CpioHeader cpioHeader = new CpioHeader()
+ {
+ Check = 0,
+ DevMajor = 1,
+ DevMinor = 0,
+ FileSize = entry.FileSize,
+ Gid = 0, // root
+ Uid = 0, // root
+ Ino = entry.Inode,
+ FileMode = entry.Mode,
+ LastModified = entry.Modified,
+ NameSize = (uint)entry.TargetPath.Length + 1,
+ Nlink = 1,
+ RDevMajor = 0,
+ RDevMinor = 0,
+ Signature = "070701",
+ };
+
+ cpioFile.Write(cpioHeader, targetPath, new MemoryStream(Encoding.UTF8.GetBytes(entry.LinkTo)));
+ }
+
+ ///
+ /// Adds a file entry to a .
+ ///
+ ///
+ /// The file entry to add.
+ ///
+ ///
+ /// The to which to add the entry.
+ ///
+ public void AddFile(ArchiveEntry entry, CpioFile cpioFile)
+ {
+ var targetPath = entry.TargetPath;
+
+ if (!targetPath.StartsWith("."))
+ {
+ targetPath = "." + targetPath;
+ }
+
+ using (Stream fileStream = File.OpenRead(entry.SourceFilename))
+ {
+ CpioHeader cpioHeader = new CpioHeader()
+ {
+ Check = 0,
+ DevMajor = 1,
+ DevMinor = 0,
+ FileSize = entry.FileSize,
+ Gid = 0, // root
+ Uid = 0, // root
+ Ino = entry.Inode,
+ FileMode = entry.Mode,
+ LastModified = entry.Modified,
+ NameSize = (uint)entry.TargetPath.Length + 1,
+ Nlink = 1,
+ RDevMajor = 0,
+ RDevMinor = 0,
+ Signature = "070701",
+ };
+
+ cpioFile.Write(cpioHeader, targetPath, fileStream);
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/CpioHeader.cs b/DebUOS/Packaging.Targets/IO/CpioHeader.cs
new file mode 100644
index 0000000..7c4f3b1
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/CpioHeader.cs
@@ -0,0 +1,262 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents the header of an individual entry in a CPIO file, in ASCII format.
+ ///
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
+ public struct CpioHeader : IArchiveHeader
+ {
+ ///
+ /// The integer value octal 070701. This value can be used to determine
+ /// whether this archive is written with little-endian or big-endian integers,
+ /// or ASCII.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 6)] // 0 through 5
+ private char[] signature;
+
+ ///
+ /// The inode number from the disk. These are used by
+ /// programs that read cpio archives to determine when two entries
+ /// refer to the same file. Programs that synthesize cpio archives
+ /// should be careful to set these to distinct values for each entry.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 6 through 13
+ private char[] ino;
+
+ ///
+ /// The mode specifies both the regular permissions and the file type.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 14 through 21
+ private char[] mode;
+
+ ///
+ /// The numeric user id of the owner.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 22 through 29
+ private char[] uid;
+
+ ///
+ /// The numeric group id of the owner.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 30 through 37
+ private char[] gid;
+
+ ///
+ /// The number of links to this file. Directories always have a
+ /// value of at least two here. Note that hardlinked files include
+ /// file data with every copy in the archive.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 38 through 45
+ private char[] nlink;
+
+ ///
+ /// Modification time of the file, indicated as the number of seconds
+ /// since the start of the epoch, 00:00:00 UTC January 1, 1970. The
+ /// four-byte integer is stored with the most-significant 16 bits
+ /// first followed by the least-significant 16 bits.Each of the two
+ /// 16 bit values are stored in machine-native byte order.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 46 through 53
+ private char[] mtime;
+
+ ///
+ /// The size of the file. Note that this archive format is limited
+ /// to four gigabyte file sizes.See mtime above for a description
+ /// of the storage of four-byte integers.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 54 through 61
+ private char[] filesize;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 62 through 69
+ private char[] devMajor;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 70 through 77
+ private char[] devMinor;
+
+ ///
+ /// For block special and character special entries, this field contains
+ /// the associated device number. For all other entry types,
+ /// it should be set to zero by writers and ignored by readers.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 78 through 85
+ private char[] rdevMajor;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 86 through 93
+ private char[] rdevMinor;
+
+ ///
+ /// The number of bytes in the pathname that follows the header.
+ /// This count includes the trailing NUL byte.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 94 through 101
+ private char[] namesize;
+
+ ///
+ /// This field is always set to zero by writers and ignored by readers.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 102 through 109
+ private char[] check;
+
+ ///
+ /// Gets an empty entry.
+ ///
+ public static CpioHeader Empty
+ {
+ get
+ {
+ return new CpioHeader()
+ {
+ Check = 0,
+ DevMajor = 0,
+ DevMinor = 0,
+ FileSize = 0,
+ Gid = 0,
+ Ino = 0,
+ FileMode = 0,
+ LastModified = DateTimeOffset.FromUnixTimeSeconds(0),
+ NameSize = 0,
+ Nlink = 1,
+ RDevMajor = 0,
+ RDevMinor = 0,
+ Signature = "070701",
+ Uid = 0
+ };
+ }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ /// The integer value octal 070701. This value can be used to determine
+ /// whether this archive is written with little-endian or big-endian integers,
+ /// or ASCII.
+ ///
+ public string Signature
+ {
+ get { return new string(this.signature); }
+ set { this.signature = value.ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint Ino
+ {
+ get { return Convert.ToUInt32(new string(this.ino), 16); }
+ set { this.ino = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public LinuxFileMode FileMode
+ {
+ get { return (LinuxFileMode)Convert.ToUInt32(new string(this.mode), 16); }
+ set { this.mode = ((uint)value).ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint Uid
+ {
+ get { return Convert.ToUInt32(new string(this.uid), 16); }
+ set { this.uid = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ /// The numeric group id of the owner.
+ ///
+ public uint Gid
+ {
+ get { return Convert.ToUInt32(new string(this.gid), 16); }
+ set { this.gid = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint Nlink
+ {
+ get { return Convert.ToUInt32(new string(this.nlink), 16); }
+ set { this.nlink = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public DateTimeOffset LastModified
+ {
+ get { return DateTimeOffset.FromUnixTimeSeconds((long)Convert.ToUInt32(new string(this.mtime), 16)); }
+ set { this.mtime = value.ToUnixTimeSeconds().ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint FileSize
+ {
+ get { return Convert.ToUInt32(new string(this.filesize), 16); }
+ set { this.filesize = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint DevMajor
+ {
+ get { return Convert.ToUInt32(new string(this.devMajor), 16); }
+ set { this.devMajor = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint DevMinor
+ {
+ get { return Convert.ToUInt32(new string(this.devMinor), 16); }
+ set { this.devMinor = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint RDevMajor
+ {
+ get { return Convert.ToUInt32(new string(this.rdevMajor), 16); }
+ set { this.rdevMajor = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint RDevMinor
+ {
+ get { return Convert.ToUInt32(new string(this.rdevMinor), 16); }
+ set { this.rdevMinor = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ ///
+ public uint NameSize
+ {
+ get { return Convert.ToUInt32(new string(this.namesize), 16); }
+ set { this.namesize = value.ToString("x8").ToCharArray(); }
+ }
+
+ ///
+ /// Gets or sets the value of the field as a .
+ /// This field is always set to zero by writers and ignored by readers.
+ ///
+ public uint Check
+ {
+ get { return Convert.ToUInt32(new string(this.check), 16); }
+ set { this.check = value.ToString("x8").ToCharArray(); }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/DlOpenFlags.cs b/DebUOS/Packaging.Targets/IO/DlOpenFlags.cs
new file mode 100644
index 0000000..c404e7d
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/DlOpenFlags.cs
@@ -0,0 +1,70 @@
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Specifies how the function loads
+ /// a dynamic link library.
+ ///
+ internal enum DlOpenFlags : int
+ {
+ ///
+ /// Perform lazy binding. Only resolve symbols as the code that references them is executed.
+ /// If the symbol is never referenced, then it is never resolved.
+ /// (Lazy binding is only performed for function references; references to variables are always
+ /// immediately bound when the library is loaded.)
+ ///
+ RTLD_LAZY = 0x00001,
+
+ ///
+ /// If this value is specified, or the environment variable LD_BIND_NOW is set to a nonempty string,
+ /// all undefined symbols in the library are resolved before returns.
+ /// If this cannot be done, an error is returned.
+ ///
+ RTLD_NOW = 0x00002,
+
+ ///
+ /// The symbols defined by this library will be made available for symbol resolution of subsequently
+ /// loaded libraries.
+ ///
+ RTLD_GLOBAL = 0x00100,
+
+ ///
+ /// This is the converse of , and the default if neither flag is specified.
+ /// Symbols defined in this library are not made available to resolve references in subsequently
+ /// loaded libraries.
+ ///
+ RTLD_LOCAL = 0,
+
+ ///
+ /// Do not unload the library during . Consequently, the library's
+ /// static variables are not reinitialized if the library is reloaded with
+ /// at a later time.
+ ///
+ ///
+ /// This flag is not specified in POSIX.1-2001.
+ ///
+ RTLD_NODELETE = 4096,
+
+ ///
+ /// Don't load the library. This can be used to test if the library is already resident
+ /// ( returns if it is not, or the library's
+ /// handle if it is resident). This flag can also be used to promote the flags on a library
+ /// that is already loaded. For example, a library that was previously loaded with
+ /// can be reopened with |
+ /// .
+ ///
+ ///
+ /// This flag is not specified in POSIX.1-2001.
+ ///
+ RTLD_NOLOAD = 4,
+
+ ///
+ /// Place the lookup scope of the symbols in this library ahead of the global scope.
+ /// This means that a self-contained library will use its own symbols in preference to global
+ /// symbols with the same name contained in libraries that have already been loaded.
+ ///
+ ///
+ /// This flag is not specified in POSIX.1-2001.
+ ///
+ RTLD_DEEPBIND = 8
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/Extensions.cs b/DebUOS/Packaging.Targets/IO/Extensions.cs
new file mode 100644
index 0000000..aeb94a5
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/Extensions.cs
@@ -0,0 +1,21 @@
+using System.IO;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ internal static class Extensions
+ {
+ public static string ReadAsUtf8String(this TarFile file)
+ {
+ // Use Encoding.UTF8 on a byte array instead of a StreamReader to make sure
+ // we stop reading when we encounter a null (\0) character.
+ using (var stream = file.Open())
+ {
+ byte[] data = new byte[stream.Length];
+ stream.Read(data, 0, data.Length);
+
+ return Encoding.UTF8.GetString(data);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/IO/GZipDecompressor.cs b/DebUOS/Packaging.Targets/IO/GZipDecompressor.cs
new file mode 100644
index 0000000..0b3be9f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/GZipDecompressor.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Provides read-only access do a decompressed , and keeps track of the current position.
+ ///
+ internal class GZipDecompressor : GZipStream
+ {
+ private long position = 0;
+
+ public GZipDecompressor(Stream stream, bool leaveOpen)
+ : base(stream, CompressionMode.Decompress, leaveOpen)
+ {
+ }
+
+ ///
+ public override long Position
+ {
+ get { return this.position; }
+ set { throw new NotSupportedException(); }
+ }
+
+ ///
+ public override int Read(byte[] array, int offset, int count)
+ {
+ var read = base.Read(array, offset, count);
+ this.position += read;
+ return read;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/IArchiveHeader.cs b/DebUOS/Packaging.Targets/IO/IArchiveHeader.cs
new file mode 100644
index 0000000..729d8f7
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/IArchiveHeader.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// A common interface for the header of a file entry in an archive file.
+ ///
+ public interface IArchiveHeader
+ {
+ ///
+ /// Gets the file mode.
+ ///
+ LinuxFileMode FileMode { get; }
+
+ ///
+ /// Gets the date at which the file was last modified.
+ ///
+ DateTimeOffset LastModified { get; }
+
+ ///
+ /// Gets the size of the file.
+ ///
+ uint FileSize { get; }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/LinuxFileMode.cs b/DebUOS/Packaging.Targets/IO/LinuxFileMode.cs
new file mode 100644
index 0000000..3e3144c
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LinuxFileMode.cs
@@ -0,0 +1,122 @@
+using System;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents the Unix-style file permission flags. Taken from <sys/stat.h>.
+ ///
+ ///
+ [Flags]
+ public enum LinuxFileMode : ushort
+ {
+ ///
+ /// No file mode has been specified.
+ ///
+ None = 0,
+
+ ///
+ /// Set user ID on execution
+ ///
+ S_ISUID = 0x0800,
+
+ ///
+ /// Set group ID on execution
+ ///
+ S_ISGID = 0x0400,
+
+ ///
+ /// Save swapped text after use (sticky).
+ ///
+ S_ISVTX = 0x0200,
+
+ ///
+ /// Read by owner
+ ///
+ S_IRUSR = 0x0100,
+
+ ///
+ /// Write by owner
+ ///
+ S_IWUSR = 0x0080,
+
+ ///
+ /// Execute by owner
+ ///
+ S_IXUSR = 0x0040,
+
+ ///
+ /// Read by group
+ ///
+ S_IRGRP = 0x0020,
+
+ ///
+ /// Write by group
+ ///
+ S_IWGRP = 0x0010,
+
+ ///
+ /// Execute by group
+ ///
+ S_IXGRP = 0x0008,
+
+ ///
+ /// Read by other
+ ///
+ S_IROTH = 0x0004,
+
+ ///
+ /// Write by other
+ ///
+ S_IWOTH = 0x0002,
+
+ ///
+ /// Execute by other
+ ///
+ S_IXOTH = 0x0001,
+
+ ///
+ /// The file is a named pipe (fifo).
+ ///
+ S_IFIFO = 0x1000, // 010000 in octal
+
+ ///
+ /// The file is a character special device.
+ ///
+ S_IFCHR = 0x2000, // 0020000 in octal
+
+ ///
+ /// The file is a directory.
+ ///
+ S_IFDIR = 0x4000, // 0040000 in octal
+
+ ///
+ /// The file is a block special device.
+ ///
+ S_IFBLK = 0x6000, // 0060000 in octal
+
+ ///
+ /// The file is a regular file.
+ ///
+ S_IFREG = 0x8000, // 0100000 in octal
+
+ ///
+ /// The file is a symbolic link.
+ ///
+ S_IFLNK = 0xA000, // 0120000 in octal
+
+ ///
+ /// The file is a Unix socket.
+ ///
+ S_IFSOCK = 0xC000, // 0140000 in octal
+
+ ///
+ /// A flag to get all permissions applied to this file.
+ ///
+ PermissionsMask = 0x0FFF,
+
+ ///
+ /// A flag to get the file type.
+ ///
+ FileTypeMask = 0xF000,
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/LzmaAction.cs b/DebUOS/Packaging.Targets/IO/LzmaAction.cs
new file mode 100644
index 0000000..51d7a68
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LzmaAction.cs
@@ -0,0 +1,136 @@
+namespace Packaging.Targets.IO
+{
+ ///
+ /// The `action' argument for lzma_code()
+ ///
+ ///
+ /// After the first use of , , ,
+ /// or , the same `action' must is used until returns
+ /// . Also, the amount of input (that is, strm->avail_in) must
+ /// not be modified by the application until returns
+ /// . Changing the `action' or modifying the amount of input
+ /// will make return .
+ ///
+ internal enum LzmaAction
+ {
+ ///
+ /// Continue coding
+ ///
+ ///
+ ///
+ /// Encoder: Encode as much input as possible. Some internal
+ /// buffering will probably be done(depends on the filter
+ /// chain in use), which causes latency: the input used won't
+ /// usually be decodeable from the output of the same
+ /// call.
+ ///
+ ///
+ /// Decoder: Decode as much input as possible and produce as
+ /// much output as possible.
+ ///
+ ///
+ Run = 0,
+
+ ///
+ /// Make all the input available at output
+ ///
+ ///
+ ///
+ /// Normally the encoder introduces some latency.
+ /// forces all the buffered data to be
+ /// available at output without resetting the internal
+ ///
+ ///
+ /// state of the encoder.This way it is possible to use
+ /// compressed stream for example for communication over
+ /// network.
+ ///
+ ///
+ /// Only some filters support . Trying to use
+ /// with filters that don't support it will
+ /// make return . For example,
+ /// LZMA1 doesn't support but LZMA2 does.
+ ///
+ ///
+ /// Using very often can dramatically reduce
+ /// the compression ratio. With some filters (for example,
+ /// LZMA2), fine-tuning the compression options may help
+ /// mitigate this problem significantly (for example,
+ /// match finder with LZMA2).
+ ///
+ ///
+ /// Decoders don't support .
+ ///
+ ///
+ SyncFlush = 1,
+
+ ///
+ /// Finish encoding of the current Block
+ ///
+ ///
+ ///
+ /// All the input data going to the current Block must have
+ /// been given to the encoder (the last bytes can still be
+ /// pending next_in). Call with
+ /// until it returns . Then continue normally
+ /// with or finish the Stream with .
+ ///
+ ///
+ /// This action is currently supported only by Stream encoder
+ /// and easy encoder (which uses Stream encoder). If there is
+ /// no unfinished Block, no empty Block is created.
+ ///
+ ///
+ FullFlush = 2,
+
+ ///
+ /// Finish the coding operation
+ ///
+ ///
+ ///
+ /// All the input data must have been given to the encoder
+ /// (the last bytes can still be pending in next_in).
+ /// Call with until it returns
+ /// . Once has been used,
+ /// the amount of input must no longer be changed by
+ /// the application.
+ ///
+ ///
+ /// When decoding, using is optional unless the
+ /// flag was used when the decoder was
+ /// initialized. When was not used, the only
+ /// effect of is that the amount of input must not
+ /// be changed just like in the encoder.
+ ///
+ ///
+ Finish = 3,
+
+ ///
+ /// Finish encoding of the current Block
+ ///
+ ///
+ ///
+ /// This is like except that this doesn't
+ /// necessarily wait until all the input has been made
+ /// available via the output buffer. That is,
+ /// might return as soon as all the input
+ /// has been consumed (avail_in == 0).
+ ///
+ ///
+ /// is useful with a threaded encoder if
+ /// one wants to split the .xz Stream into Blocks at specific
+ /// offsets but doesn't care if the output isn't flushed
+ /// immediately. Using allows keeping
+ /// the threads busy while would make
+ /// wait until all the threads have finished
+ /// until more data could be passed to the encoder.
+ ///
+ ///
+ /// With a initialized with the single-threaded
+ /// lzma_stream_encoder() or lzma_easy_encoder(),
+ /// is an alias for .
+ ///
+ ///
+ FullBarrier = 4
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/LzmaCheck.cs b/DebUOS/Packaging.Targets/IO/LzmaCheck.cs
new file mode 100644
index 0000000..d860308
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LzmaCheck.cs
@@ -0,0 +1,34 @@
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Type of the integrity check (Check ID)
+ ///
+ ///
+ /// The.xz format supports multiple types of checks that are calculated
+ /// from the uncompressed data. They vary in both speed and ability to
+ /// detect errors.
+ ///
+ ///
+ internal enum LzmaCheck
+ {
+ ///
+ /// No Check is calculated.
+ ///
+ None = 0,
+
+ ///
+ /// CRC32 using the polynomial from the IEEE 802.3 standard
+ ///
+ Crc32 = 1,
+
+ ///
+ /// CRC64 using the polynomial from the ECMA-182 standard
+ ///
+ Crc64 = 4,
+
+ ///
+ /// SHA-256
+ ///
+ Sha256 = 10
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/LzmaDecodeFlags.cs b/DebUOS/Packaging.Targets/IO/LzmaDecodeFlags.cs
new file mode 100644
index 0000000..418de7f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LzmaDecodeFlags.cs
@@ -0,0 +1,47 @@
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Flags that control the decoding behavior of liblzma.
+ ///
+ internal enum LzmaDecodeFlags : uint
+ {
+ ///
+ /// This flag makes return
+ /// if the input stream
+ /// being decoded has no integrity check.
+ ///
+ TellNoCheck = 0x01,
+
+ ///
+ /// This flag makes return
+ /// if the input
+ /// stream has an integrity check, but the type of the integrity check is not
+ /// supported by this liblzma version or build. Such files can still be
+ /// decoded, but the integrity check cannot be verified.
+ ///
+ TellUnsupportedCheck = 0x02,
+
+ ///
+ /// This flag makes return
+ /// as soon as the type
+ /// of the integrity check is known.
+ ///
+ TellAnyCheck = 0x04,
+
+ ///
+ ///
+ /// This flag enables decoding of concatenated files with file formats that
+ /// allow concatenating compressed files as is. From the formats currently
+ /// supported by liblzma, only the .xz format allows concatenated files.
+ /// Concatenated files are not allowed with the legacy .lzma format.
+ ///
+ ///
+ /// This flag also affects the usage of the `action' argument for
+ /// When is used, won't return
+ /// unless is used as `action'. Thus, the application has to set
+ /// in the same way as it does when encoding.
+ ///
+ ///
+ Concatenated = 0x08
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/LzmaMT.cs b/DebUOS/Packaging.Targets/IO/LzmaMT.cs
new file mode 100644
index 0000000..38b55e1
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LzmaMT.cs
@@ -0,0 +1,154 @@
+using System;
+
+namespace Packaging.Targets.IO
+{
+#pragma warning disable SA1307 // Accessible fields must begin with upper-case letter
+#pragma warning disable SA1310 // Field names must not contain underscore
+#pragma warning disable CS0169 // The field '' is never used
+#pragma warning disable IDE0051 // Remove unused private members
+ ///
+ /// Multithreading options
+ ///
+ internal struct LzmaMT
+ {
+ ///
+ /// Set this to zero if no flags are wanted.
+ ///
+ ///
+ /// No flags are currently supported.
+ ///
+ public uint flags;
+
+ ///
+ /// Number of worker threads to use
+ ///
+ public uint threads;
+
+ ///
+ /// Maximum uncompressed size of a Block
+ ///
+ ///
+ ///
+ /// The encoder will start a new .xz Block every block_size bytes.
+ /// Using LZMA_FULL_FLUSH or LZMA_FULL_BARRIER with lzma_code()
+ /// the caller may tell liblzma to start a new Block earlier.
+ ///
+ ///
+ /// With LZMA2, a recommended block size is 2-4 times the LZMA2
+ /// dictionary size. With very small dictionaries, it is recommended
+ /// to use at least 1 MiB block size for good compression ratio, even
+ /// if this is more than four times the dictionary size. Note that
+ /// these are only recommendations for typical use cases; feel free
+ /// to use other values. Just keep in mind that using a block size
+ /// less than the LZMA2 dictionary size is waste of RAM.
+ ///
+ ///
+ /// Set this to 0 to let liblzma choose the block size depending
+ /// on the compression options. For LZMA2 it will be 3*dict_size
+ /// or 1 MiB, whichever is more.
+ ///
+ ///
+ /// For each thread, about 3 * block_size bytes of memory will be
+ /// allocated. This may change in later liblzma versions. If so,
+ /// the memory usage will probably be reduced, not increased.
+ ///
+ ///
+ public ulong block_size;
+
+ ///
+ /// Timeout to allow lzma_code() to return early
+ ///
+ ///
+ ///
+ /// Multithreading can make liblzma to consume input and produce
+ /// output in a very bursty way: it may first read a lot of input
+ /// to fill internal buffers, then no input or output occurs for
+ /// a while.
+ ///
+ ///
+ /// In single-threaded mode, lzma_code() won't return until it has
+ /// either consumed all the input or filled the output buffer. If
+ /// this is done in multithreaded mode, it may cause a call
+ /// lzma_code() to take even tens of seconds, which isn't acceptable
+ /// in all applications.
+ ///
+ ///
+ /// To avoid very long blocking times in lzma_code(), a timeout
+ /// (in milliseconds) may be set here. If lzma_code() would block
+ /// longer than this number of milliseconds, it will return with
+ /// LZMA_OK. Reasonable values are 100 ms or more. The xz command
+ /// line tool uses 300 ms.
+ ///
+ ///
+ /// If long blocking times are fine for you, set timeout to a special
+ /// value of 0, which will disable the timeout mechanism and will make
+ /// lzma_code() block until all the input is consumed or the output
+ /// buffer has been filled.
+ ///
+ ///
+ /// Even with a timeout, lzma_code() might sometimes take
+ /// somewhat long time to return. No timing guarantees
+ /// are made.
+ ///
+ ///
+ public uint timeout;
+
+ ///
+ /// Compression preset (level and possible flags)
+ ///
+ ///
+ ///
+ /// The preset is set just like with lzma_easy_encoder().
+ ///
+ ///
+ /// The preset is ignored if filters below is non-NULL.
+ ///
+ ///
+ public uint preset;
+
+ ///
+ /// Filter chain (alternative to a preset).
+ ///
+ ///
+ /// If this is NULL, the preset above is used. Otherwise the preset
+ /// is ignored and the filter chain specified here is used.
+ ///
+ public IntPtr filters;
+
+ ///
+ /// Integrity check type.
+ ///
+ ///
+ /// The xz command line tool defaults to LZMA_CHECK_CRC64, which is
+ /// a good choice if you are unsure.
+ ///
+ public LzmaCheck check;
+
+ ///
+ /// Reserved space to allow possible future extensions without
+ /// breaking the ABI. You should not touch these, because the names
+ /// of these variables may change. These are and will never be used
+ /// with the currently supported options, so it is safe to leave these
+ /// uninitialized.
+ ///
+ private readonly int reserved_enum1;
+ private readonly int reserved_enum2;
+ private readonly int reserved_enum3;
+ private readonly int reserved_int1;
+ private readonly int reserved_int2;
+ private readonly int reserved_int3;
+ private readonly int reserved_int4;
+ private readonly ulong reserved_int5;
+ private readonly ulong reserved_int6;
+ private readonly ulong reserved_int7;
+ private readonly ulong reserved_int8;
+ private readonly IntPtr reserved_ptr1;
+ private readonly IntPtr reserved_ptr2;
+ private readonly IntPtr reserved_ptr3;
+ private readonly IntPtr reserved_ptr4;
+ }
+#pragma warning restore SA1307 // Accessible fields must begin with upper-case letter
+#pragma warning restore SA1310 // Field names must not contain underscore
+#pragma warning restore CS0169 // The field '' is never used
+#pragma warning restore IDE0051 // Remove unused private members
+}
diff --git a/DebUOS/Packaging.Targets/IO/LzmaResult.cs b/DebUOS/Packaging.Targets/IO/LzmaResult.cs
new file mode 100644
index 0000000..03f4cc0
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LzmaResult.cs
@@ -0,0 +1,230 @@
+using System;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Return values used by several functions in liblzma.
+ ///
+ internal enum LzmaResult : uint
+ {
+ ///
+ /// The operation completed successfully.
+ ///
+ OK = 0,
+
+ ///
+ /// End of stream was reached
+ ///
+ ///
+ ///
+ /// In encoder, , , or
+ /// was finished. In decoder, this indicates
+ /// that all the data was successfully decoded.
+ ///
+ ///
+ /// In all cases, when is returned, the last
+ /// output bytes should be picked from .
+ ///
+ ///
+ StreamEnd = 1,
+
+ ///
+ /// Input stream has no integrity check
+ ///
+ ///
+ ///
+ /// This return value can be returned only if the
+ /// flag was used when initializing
+ /// the decoder. is just a warning, and
+ /// the decoding can be continued normally.
+ ///
+ ///
+ /// It is possible to call lzma_get_check() immediately after
+ /// lzma_code has returned . The result will
+ /// naturally be , but the possibility to call
+ /// lzma_get_check() may be convenient in some applications.
+ ///
+ ///
+ NoCheck = 2,
+
+ ///
+ /// Cannot calculate the integrity check
+ ///
+ ///
+ ///
+ /// The usage of this return value is different in encoders
+ /// and decoders.
+ ///
+ ///
+ /// Encoders can return this value only from the initialization
+ /// function. If initialization fails with this value, the
+ /// encoding cannot be done, because there's no way to produce
+ /// output with the correct integrity check.
+ ///
+ ///
+ /// Decoders can return this value only from lzma_code() and
+ /// only if the flag was used when
+ /// initializing the decoder. The decoding can still be
+ /// continued normally even if the check type is unsupported,
+ /// but naturally the check will not be validated, and possible
+ /// errors may go undetected.
+ ///
+ ///
+ /// With decoder, it is possible to call lzma_get_check()
+ /// immediately after lzma_code() has returned
+ /// . This way it is possible to find
+ /// out what the unsupported Check ID was.
+ ///
+ ///
+ UnsupportedCheck = 3,
+
+ ///
+ /// Integrity check type is now available
+ ///
+ ///
+ /// This value can be returned only by the lzma_code() function
+ /// and only if the decoder was initialized with the
+ /// flag. tells the
+ /// application that it may now call lzma_get_check() to find
+ /// out the Check ID. This can be used, for example, to
+ /// implement a decoder that accepts only files that have
+ /// strong enough integrity check.
+ ///
+ GetCheck = 4,
+
+ ///
+ /// Cannot allocate memory
+ ///
+ ///
+ ///
+ /// Memory allocation failed, or the size of the allocation
+ /// would be greater than SIZE_MAX.
+ ///
+ ///
+ /// Due to internal implementation reasons, the coding cannot
+ /// be continued even if more memory were made available after
+ /// .
+ ///
+ ///
+ MemError = 5,
+
+ ///
+ /// Memory usage limit was reached
+ ///
+ ///
+ /// Decoder would need more memory than allowed by the
+ /// specified memory usage limit. To continue decoding,
+ /// the memory usage limit has to be increased with
+ /// lzma_memlimit_set().
+ ///
+ MemlimitError = 6,
+
+ ///
+ /// File format not recognized
+ ///
+ ///
+ /// The decoder did not recognize the input as supported file
+ /// format. This error can occur, for example, when trying to
+ /// decode .lzma format file with ,
+ /// because accepts only the .xz format.
+ ///
+ FormatError = 7,
+
+ ///
+ /// Invalid or unsupported options
+ ///
+ ///
+ ///
+ /// Invalid or unsupported options, for example
+ /// - unsupported filter(s) or filter options; or
+ /// - reserved bits set in headers (decoder only).
+ ///
+ ///
+ /// Rebuilding liblzma with more features enabled, or
+ /// upgrading to a newer version of liblzma may help.
+ ///
+ ///
+ OptionsError = 8,
+
+ ///
+ /// Data is corrupt
+ ///
+ ///
+ ///
+ /// The usage of this return value is different in encoders
+ /// and decoders. In both encoder and decoder, the coding
+ /// cannot continue after this error.
+ ///
+ ///
+ /// Encoders return this if size limits of the target file
+ /// format would be exceeded. These limits are huge, thus
+ /// getting this error from an encoder is mostly theoretical.
+ /// For example, the maximum compressed and uncompressed
+ /// size of a .xz Stream is roughly 8 EiB (2^63 bytes).
+ ///
+ ///
+ /// Decoders return this error if the input data is corrupt.
+ /// This can mean, for example, invalid CRC32 in headers
+ /// or invalid check of uncompressed data.
+ ///
+ ///
+ DataError = 9,
+
+ ///
+ /// No progress is possible
+ ///
+ ///
+ ///
+ /// This error code is returned when the coder cannot consume
+ /// any new input and produce any new output.The most common
+ /// reason for this error is that the input stream being
+ /// decoded is truncated or corrupt.
+ ///
+ ///
+ /// This error is not fatal. Coding can be continued normally
+ /// by providing more input and/or more output space, if
+ /// possible.
+ ///
+ ///
+ /// Typically the first call to that can do no
+ /// progress returns instead of . Only
+ /// the second consecutive call doing no progress will return
+ /// . This is intentional.
+ ///
+ ///
+ /// With zlib, Z_BUF_ERROR may be returned even if the
+ /// application is doing nothing wrong, so apps will need
+ /// to handle Z_BUF_ERROR specially. The above hack
+ /// guarantees that liblzma never returns
+ /// to properly written applications unless the input file
+ /// is truncated or corrupt. This should simplify the
+ /// applications a little.
+ ///
+ ///
+ BufferError = 10,
+
+ ///
+ /// Programming error
+ ///
+ ///
+ ///
+ /// This indicates that the arguments given to the function are
+ /// invalid or the internal state of the decoder is corrupt.
+ /// - Function arguments are invalid or the structures
+ /// pointed by the argument pointers are invalid
+ /// e.g. if has been set to and
+ /// > 0 when calling .
+ /// - lzma_/// functions have been called in wrong order
+ /// e.g. was called right after .
+ /// - If errors occur randomly, the reason might be flaky
+ /// hardware.
+ ///
+ ///
+ /// If you think that your code is correct, this error code
+ /// can be a sign of a bug in liblzma. See the documentation
+ /// how to report bugs.
+ ///
+ ///
+ ProgError = 11
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/LzmaStream.cs b/DebUOS/Packaging.Targets/IO/LzmaStream.cs
new file mode 100644
index 0000000..6ed09a7
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LzmaStream.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Passing data to and from liblzma
+ ///
+ ///
+ ///
+ /// The lzma_stream structure is used for
+ /// - passing pointers to input and output buffers to liblzma;
+ /// - defining custom memory hander functions; and
+ /// - holding a pointer to coder-specific internal data structures.
+ ///
+ ///
+ /// Typical usage:
+ ///
+ ///
+ /// - After allocating (on stack or with malloc()), it must be
+ /// initialized to LZMA_STREAM_INIT (see LZMA_STREAM_INIT for details).
+ ///
+ ///
+ /// - Initialize a coder to the lzma_stream, for example by using
+ /// lzma_easy_encoder() or lzma_auto_decoder(). Some notes:
+ /// - In contrast to zlib, and are
+ /// ignored by all initialization functions, thus it is safe
+ /// to not initialize them yet.
+ /// - The initialization functions always set strm->total_in and
+ /// strm->total_out to zero.
+ /// - If the initialization function fails, no memory is left allocated
+ /// that would require freeing with even if some memory was
+ /// associated with the structure when the initialization
+ /// function was called.
+ ///
+ ///
+ /// - Use to do the actual work.
+ ///
+ ///
+ /// - Once the coding has been finished, the existing lzma_stream can be
+ /// reused. It is OK to reuse with different initialization
+ /// function without calling first. Old allocations are
+ /// automatically freed.
+ ///
+ ///
+ /// - Finally, use to free the allocated memory. never
+ /// frees the structure itself.
+ ///
+ ///
+ /// Application may modify the values of and as it wants.
+ /// They are updated by liblzma to match the amount of data read and
+ /// written, but aren't used for anything else.
+ ///
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct LzmaStream
+ {
+ ///
+ /// Pointer to the next input byte.
+ ///
+ public IntPtr NextIn;
+
+ ///
+ /// Number of available input bytes in next_in.
+ ///
+ public uint AvailIn;
+
+ ///
+ /// Total number of bytes read by liblzma.
+ ///
+ public ulong TotalIn;
+
+ ///
+ /// Pointer to the next output position.
+ ///
+ public IntPtr NextOut;
+
+ ///
+ /// Amount of free space in next_out.
+ ///
+ public uint AvailOut;
+
+ ///
+ /// Total number of bytes written by liblzma.
+ ///
+ public ulong TotalOut;
+
+ ///
+ /// Custom memory allocation functions
+ ///
+ ///
+ /// In most cases this is NULL which makes liblzma use
+ /// the standard malloc() and free().
+ ///
+ public IntPtr Allocator;
+
+ ///
+ /// Internal state is not visible to applications.
+ ///
+#pragma warning disable SA1214 // Readonly fields must appear before non-readonly fields
+ public readonly IntPtr InternalState;
+#pragma warning restore SA1214 // Readonly fields must appear before non-readonly fields
+
+ ///
+ /// Reserved space to allow possible future extensions without
+ /// breaking the ABI. Excluding the initialization of this structure,
+ /// you should not touch these, because the names of these variables
+ /// may change.
+ ///
+ private readonly IntPtr reservedPtr1;
+ private readonly IntPtr reservedPtr2;
+ private readonly IntPtr reservedPtr3;
+ private readonly IntPtr reservedPtr4;
+ private readonly ulong reservedInt1;
+ private readonly ulong reservedInt2;
+ private readonly uint reservedInt3;
+ private readonly uint reservedInt4;
+ private readonly uint reservedEnum1;
+ private readonly uint reservedEnum2;
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/IO/LzmaStreamFlags.cs b/DebUOS/Packaging.Targets/IO/LzmaStreamFlags.cs
new file mode 100644
index 0000000..d235a3f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/LzmaStreamFlags.cs
@@ -0,0 +1,98 @@
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Options for encoding/decoding Stream Header and Stream Footer
+ ///
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct LzmaStreamFlags
+ {
+ ///
+ /// Stream Flags format version
+ ///
+ ///
+ ///
+ /// To prevent API and ABI breakages if new features are needed in
+ /// Stream Header or Stream Footer, a version number is used to
+ /// indicate which fields in this structure are in use.For now,
+ /// version must always be zero.With non-zero version, the
+ /// lzma_stream_header_encode() and lzma_stream_footer_encode()
+ /// will return .
+ ///
+ ///
+ ///
+ /// lzma_stream_header_decode() and
+ /// will always set this to the lowest value that supports all the
+ /// features indicated by the Stream Flags field.The application
+ /// must check that the version number set by the decoding functions
+ /// is supported by the application. Otherwise it is possible that
+ /// the application will decode the Stream incorrectly.
+ ///
+ ///
+ public readonly uint Version;
+
+ ///
+ /// Backward Size
+ ///
+ ///
+ ///
+ /// Backward Size must be a multiple of four bytes.In this Stream
+ /// format version, Backward Size is the size of the Index field.
+ ///
+ ///
+ ///
+ /// Backward Size isn't actually part of the Stream Flags field, but
+ /// it is convenient to include in this structure anyway. Backward
+ /// Size is present only in the Stream Footer.There is no need to
+ /// initialize backward_size when encoding Stream Header.
+ ///
+ ///
+ ///
+ /// lzma_stream_header_decode() always sets backward_size to
+ /// LZMA_VLI_UNKNOWN so that it is convenient to use
+ /// lzma_stream_flags_compare() when both Stream Header and Stream
+ /// Footer have been decoded.
+ ///
+ ///
+ public ulong BackwardSize;
+
+ ///
+ /// Check ID
+ ///
+ ///
+ /// This indicates the type of the integrity check calculated from
+ /// uncompressed data.
+ ///
+ public LzmaCheck Check;
+
+ ///
+ ///
+ /// Reserved space to allow possible future extensions without
+ /// breaking the ABI.You should not touch these, because the
+ /// names of these variables may change.
+ ///
+ ///
+ ///
+ /// (We will never be able to use all of these since Stream Flags
+ /// is just two bytes plus Backward Size of four bytes.But it's
+ /// nice to have the proper types when they are needed.)
+ ///
+ ///
+ private readonly int reservedEnum1;
+ private readonly int reservedEnum2;
+ private readonly int reservedEnum3;
+ private readonly int reservedEnum4;
+ private readonly char reservedBool1;
+ private readonly char reservedBool2;
+ private readonly char reservedBool3;
+ private readonly char reservedBool4;
+ private readonly char reservedBool5;
+ private readonly char reservedBool6;
+ private readonly char reservedBool7;
+ private readonly char reservedBool8;
+ private readonly uint reservedInt1;
+ private readonly uint reservedInt2;
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/NativeMethods.cs b/DebUOS/Packaging.Targets/IO/NativeMethods.cs
new file mode 100644
index 0000000..4ede7c2
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/NativeMethods.cs
@@ -0,0 +1,407 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using FunctionLoader = Packaging.Targets.Native.FunctionLoader;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Provides access to the liblzma API. liblzma is part of the xz suite.
+ ///
+ ///
+ /// You can download pre-built binaries from Windows from https://tukaani.org/xz/.
+ ///
+ ///
+ ///
+ internal static class NativeMethods
+ {
+ ///
+ /// The name of the lzma library.
+ ///
+ ///
+ /// You can fetch liblzma from https://github.com/RomanBelkov/XZ.NET/blob/master/XZ.NET/liblzma.dll
+ ///
+ private const string LibraryName = @"lzma";
+
+ private static lzma_stream_decoder_delegate lzma_stream_decoder_ptr;
+ private static lzma_code_delegate lzma_code_ptr;
+ private static lzma_stream_footer_decode_delegate lzma_stream_footer_decode_ptr;
+ private static lzma_index_uncompressed_size_delegate lzma_index_uncompressed_size_ptr;
+ private static lzma_index_buffer_decode_delegate lzma_index_buffer_decode_ptr;
+ private static lzma_index_end_delegate lzma_index_end_ptr;
+ private static lzma_end_delegate lzma_end_ptr;
+ private static lzma_easy_encoder_delegate lzma_easy_encoder_ptr;
+ private static lzma_stream_encoder_mt_delegate lzma_stream_encoder_mt_ptr;
+ private static lzma_stream_buffer_bound_delegate lzma_stream_buffer_bound_ptr;
+ private static lzma_easy_buffer_encode_delegate lzma_easy_buffer_encode_ptr;
+
+ static NativeMethods()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ if (RuntimeInformation.OSArchitecture != Architecture.X64)
+ {
+ throw new InvalidOperationException(".NET packaging only supports 64-bit Windows processes");
+ }
+ }
+
+ var libraryPath = Path.GetDirectoryName(typeof(NativeMethods).GetTypeInfo().Assembly.Location);
+ var lzmaWindowsPath = Path.GetFullPath(Path.Combine(libraryPath, "../../runtimes/win7-x64/native/lzma.dll"));
+
+ IntPtr library = FunctionLoader.LoadNativeLibrary(
+ new string[] { lzmaWindowsPath, Path.GetFullPath(Path.Combine(libraryPath, "lzma.dll")) }, // lzma.dll is used when running unit tests.
+ new string[] { "liblzma.so.5", "liblzma.so" },
+ new string[] { "liblzma.dylib" });
+
+ if (library == IntPtr.Zero)
+ {
+ throw new FileLoadException("Could not load liblzma. On Linux, make sure you've installed liblzma-dev or an equivalent package.");
+ }
+
+ lzma_stream_decoder_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_stream_decoder));
+ lzma_code_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_code));
+ lzma_stream_footer_decode_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_stream_footer_decode));
+ lzma_index_uncompressed_size_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_index_uncompressed_size));
+ lzma_index_buffer_decode_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_index_buffer_decode));
+ lzma_index_end_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_index_end));
+ lzma_end_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_end));
+ lzma_easy_encoder_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_easy_encoder));
+ lzma_stream_encoder_mt_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_stream_encoder_mt), throwOnError: false);
+ lzma_stream_buffer_bound_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_stream_buffer_bound));
+ lzma_easy_buffer_encode_ptr = FunctionLoader.LoadFunctionDelegate(library, nameof(lzma_easy_buffer_encode));
+ }
+
+ private delegate LzmaResult lzma_stream_decoder_delegate(ref LzmaStream stream, ulong memLimit, LzmaDecodeFlags flags);
+
+ private unsafe delegate LzmaResult lzma_easy_buffer_encode_delegate(uint preset, LzmaCheck check, void* allocator, byte[] @in, UIntPtr in_size, byte[] @out, UIntPtr* out_pos, UIntPtr out_size);
+
+ private delegate UIntPtr lzma_stream_buffer_bound_delegate(UIntPtr uncompressed_size);
+
+ private delegate LzmaResult lzma_stream_encoder_mt_delegate(ref LzmaStream stream, ref LzmaMT mt);
+
+ private delegate LzmaResult lzma_easy_encoder_delegate(ref LzmaStream stream, uint preset, LzmaCheck check);
+
+ private delegate void lzma_end_delegate(ref LzmaStream stream);
+
+ private delegate void lzma_index_end_delegate(IntPtr i, IntPtr allocator);
+
+ private delegate uint lzma_index_buffer_decode_delegate(ref IntPtr i, ref ulong memLimit, IntPtr allocator, byte[] indexBuffer, ref uint inPosition, ulong inSize);
+
+ private delegate ulong lzma_index_uncompressed_size_delegate(IntPtr i);
+
+ private delegate LzmaResult lzma_stream_footer_decode_delegate(ref LzmaStreamFlags options, byte[] inp);
+
+ private delegate LzmaResult lzma_code_delegate(ref LzmaStream stream, LzmaAction action);
+
+ ///
+ /// Gets a value indicating whether the underlying native library supports multithreading.
+ ///
+ public static bool SupportsMultiThreading
+ {
+ get { return lzma_stream_encoder_mt_ptr != null; }
+ }
+
+ ///
+ /// Initialize .xz Stream decoder
+ ///
+ ///
+ /// Pointer to properly prepared
+ ///
+ ///
+ /// Memory usage limit as bytes. Use UINT64_MAX
+ /// to effectively disable the limiter.
+ ///
+ ///
+ /// Bitwise-or of zero or more of the decoder flags:
+ /// , ,
+ /// , .
+ ///
+ ///
+ /// : Initialization was successful,
+ /// : Cannot allocate memory,
+ /// : Unsupported flags,
+ /// .
+ ///
+ ///
+ public static LzmaResult lzma_stream_decoder(ref LzmaStream stream, ulong memLimit, LzmaDecodeFlags flags) => lzma_stream_decoder_ptr(ref stream, memLimit, flags);
+
+ ///
+ /// Encode or decode data
+ ///
+ ///
+ /// The for which to read the data.
+ ///
+ ///
+ /// The action to perform.
+ ///
+ ///
+ /// A value.
+ ///
+ ///
+ ///
+ /// Once the has been successfully initialized (e.g. with
+ /// lzma_stream_encoder()), the actual encoding or decoding is done
+ /// using this function.The application has to update ,
+ /// , , and to pass input
+ /// to and get output from liblzma.
+ ///
+ ///
+ /// See the description of the coder-specific initialization function to find
+ /// out what values are supported by the coder.
+ ///
+ ///
+ public static LzmaResult lzma_code(ref LzmaStream stream, LzmaAction action) => lzma_code_ptr(ref stream, action);
+
+ ///
+ /// Decode Stream Footer
+ ///
+ ///
+ /// Target for the decoded Stream Header options.
+ ///
+ ///
+ /// Beginning of the input buffer of
+ /// LZMA_STREAM_HEADER_SIZE bytes.
+ ///
+ ///
+ /// : Decoding was successful.
+ /// : Magic bytes don't match, thus the given buffer cannot be Stream Footer.
+ /// : CRC32 doesn't match, thus the Stream Footer is corrupt.
+ /// : Unsupported options are present in Stream Footer.
+ ///
+ ///
+ /// If Stream Header was already decoded successfully, but
+ /// decoding Stream Footer returns , the
+ /// application should probably report some other error message
+ /// than "file format not recognized", since the file more likely
+ /// is corrupt(possibly truncated). Stream decoder in liblzma
+ /// uses in this situation.
+ ///
+ ///
+ public static LzmaResult lzma_stream_footer_decode(ref LzmaStreamFlags options, byte[] inp) => lzma_stream_footer_decode_ptr(ref options, inp);
+
+ ///
+ /// Get the uncompressed size of the file
+ ///
+ ///
+ public static ulong lzma_index_uncompressed_size(IntPtr i) => lzma_index_uncompressed_size_ptr(i);
+
+ ///
+ /// Single-call .xz Index decoder
+ ///
+ /// If decoding succeeds, will point to a new lzma_index, which the application has to
+ /// later free with lzma_index_end(). If an error occurs, will be . The old value of
+ /// is always ignored and thus doesn't need to be initialized by the caller.
+ ///
+ ///
+ /// Pointer to how much memory the resulting lzma_index is allowed to require. The value
+ /// pointed by this pointer is modified if and only if is returned.
+ ///
+ ///
+ /// Pointer to lzma_allocator, or to use malloc()
+ ///
+ ///
+ /// Beginning of the input buffer
+ ///
+ ///
+ /// The next byte will be read from in[*in_pos]. *in_pos is updated only if decoding succeeds.
+ ///
+ ///
+ /// Size of the input buffer; the first byte that won't be read is in[in_size].
+ ///
+ ///
+ /// : Decoding was successful.
+ /// ,
+ /// : Memory usage limit was reached. The minimum required memlimit value was stored to* memlimit.
+ /// ,
+ /// .
+ ///
+ ///
+ public static uint lzma_index_buffer_decode(ref IntPtr i, ref ulong memLimit, IntPtr allocator, byte[] indexBuffer, ref uint inPosition, ulong inSize)
+ => lzma_index_buffer_decode_ptr(ref i, ref memLimit, allocator, indexBuffer, ref inPosition, inSize);
+
+ ///
+ /// Deallocate lzma_index
+ ///
+ ///
+ /// The index to deallocate
+ ///
+ ///
+ /// The allocated used to allocate the memory.
+ ///
+ ///
+ /// If is , this does nothing.
+ ///
+ ///
+ public static void lzma_index_end(IntPtr i, IntPtr allocator) => lzma_index_end_ptr(i, allocator);
+
+ ///
+ /// Free memory allocated for the coder data structures
+ ///
+ ///
+ /// Pointer to lzma_stream that is at least initialized with LZMA_STREAM_INIT.
+ ///
+ ///
+ /// After , is guaranteed to be .
+ /// No other members of the structure are touched.
+ /// zlib indicates an error if application end()s unfinished stream structure.
+ /// liblzma doesn't do this, and assumes that
+ /// application knows what it is doing.
+ ///
+ ///
+ public static void lzma_end(ref LzmaStream stream) => lzma_end_ptr(ref stream);
+
+ ///
+ ///
+ /// Initialize .xz Stream encoder using a preset number.
+ ///
+ ///
+ /// This function is intended for those who just want to use the basic features if liblzma(that is, most developers out there).
+ ///
+ ///
+ /// If initialization fails(return value is not LZMA_OK), all the memory allocated for *strm by liblzma is always freed.Thus, there is no need to call lzma_end() after failed initialization.
+ ///
+ /// If initialization succeeds, use lzma_code() to do the actual encoding.Valid values for `action' (the second argument of lzma_code()) are LZMA_RUN, LZMA_SYNC_FLUSH, LZMA_FULL_FLUSH, and LZMA_FINISH. In future, there may be compression levels or flags that don't support LZMA_SYNC_FLUSH.
+ ///
+ ///
+ ///
+ /// Pointer to lzma_stream that is at least initialized with LZMA_STREAM_INIT.
+ ///
+ ///
+ /// Compression preset to use. A preset consist of level number and zero or more flags. Usually flags aren't used, so preset is simply a number [0, 9] which match the options -0 ... -9 of the xz command line tool. Additional flags can be be set using bitwise-or with the preset level number, e.g. 6 | LZMA_PRESET_EXTREME.
+ ///
+ ///
+ /// Integrity check type to use. See check.h for available checks. The xz command line tool defaults to LZMA_CHECK_CRC64, which is a good choice if you are unsure. LZMA_CHECK_CRC32 is good too as long as the uncompressed file is not many gigabytes.
+ ///
+ ///
+ ///
+ /// - LZMA_OK: Initialization succeeded. Use lzma_code() to encode your data.
+ ///
+ /// - LZMA_MEM_ERROR: Memory allocation failed.
+ ///
+ ///
+ /// - LZMA_OPTIONS_ERROR: The given compression preset is not supported by this build of liblzma.
+ ///
+ ///
+ /// - LZMA_UNSUPPORTED_CHECK: The given check type is not supported by this liblzma build.
+ ///
+ ///
+ /// - LZMA_PROG_ERROR: One or more of the parameters have values that will never be valid. For example, strm == NULL.
+ ///
+ ///
+ public static LzmaResult lzma_easy_encoder(ref LzmaStream stream, uint preset, LzmaCheck check) => lzma_easy_encoder_ptr(ref stream, preset, check);
+
+ ///
+ ///
+ /// Initialize multithreaded .xz Stream encoder
+ ///
+ ///
+ /// This provides the functionality of lzma_easy_encoder() and
+ /// lzma_stream_encoder() as a single function for multithreaded use.
+ ///
+ ///
+ /// The supported actions for lzma_code() are LZMA_RUN, LZMA_FULL_FLUSH,
+ /// LZMA_FULL_BARRIER, and LZMA_FINISH. Support for LZMA_SYNC_FLUSH might be
+ /// added in the future.
+ ///
+ ///
+ ///
+ /// Pointer to properly prepared lzma_stream
+ ///
+ ///
+ /// Pointer to multithreaded compression options
+ ///
+ ///
+ /// A value which indicates success or failure.
+ ///
+ public static LzmaResult lzma_stream_encoder_mt(ref LzmaStream stream, ref LzmaMT mt)
+ {
+ if (SupportsMultiThreading)
+ {
+ return lzma_stream_encoder_mt_ptr(ref stream, ref mt);
+ }
+ else
+ {
+ throw new PlatformNotSupportedException("lzma_stream_encoder_mt is not supported on this platform. Check SupportsMultiThreading to see whether you can use this functionality.");
+ }
+ }
+
+ ///
+ ///
+ /// Calculate output buffer size for single-call Stream encoder
+ ///
+ ///
+ /// When trying to compress uncompressible data, the encoded size will be slightly bigger than the input data.This function calculates how much output buffer space is required to be sure that lzma_stream_buffer_encode() doesn't return LZMA_BUF_ERROR.
+ ///
+ ///
+ /// The calculated value is not exact, but it is guaranteed to be big enough.The actual maximum output space required may be slightly smaller (up to about 100 bytes). This should not be a problem in practice.
+ ///
+ ///
+ /// If the calculated maximum size doesn't fit into size_t or would make the Stream grow past LZMA_VLI_MAX (which should never happen in practice), zero is returned to indicate the error.
+ ///
+ ///
+ ///
+ /// The uncompressed size.
+ ///
+ ///
+ /// The buffer output size.
+ ///
+ ///
+ /// The limit calculated by this function applies only to single-call encoding. Multi-call encoding may (and probably will) have larger maximum expansion when encoding uncompressible data. Currently there is no function to calculate the maximum expansion of multi-call encoding.
+ ///
+ public static UIntPtr lzma_stream_buffer_bound(UIntPtr uncompressed_size) => lzma_stream_buffer_bound_ptr(uncompressed_size);
+
+ ///
+ /// Single-call .xz Stream encoding using a preset number.
+ ///
+ ///
+ /// Compression preset to use. See the description in lzma_easy_encoder().
+ ///
+ ///
+ /// Type of the integrity check to calculate from uncompressed data.
+ ///
+ ///
+ /// lzma_allocator for custom allocator functions. Set to NULL to use malloc() and free().
+ ///
+ ///
+ /// Beginning of the input buffer
+ ///
+ ///
+ /// Size of the input buffer
+ ///
+ ///
+ /// Beginning of the output buffer
+ ///
+ ///
+ /// The next byte will be written to out[*out_pos]. *out_pos is updated only if encoding succeeds.
+ ///
+ ///
+ /// Size of the out buffer; the first byte into which no data is written to is out[out_size].
+ ///
+ ///
+ ///
+ /// - LZMA_OK: Encoding was successful.
+ ///
+ ///
+ /// - LZMA_BUF_ERROR: Not enough output buffer space.
+ ///
+ ///
+ /// - LZMA_OPTIONS_ERROR
+ ///
+ ///
+ /// - LZMA_MEM_ERROR
+ ///
+ ///
+ /// - LZMA_DATA_ERROR
+ ///
+ ///
+ /// - LZMA_PROG_ERROR
+ ///
+ ///
+ ///
+ /// The maximum required output buffer size can be calculated with lzma_stream_buffer_bound()
+ ///
+ public static unsafe LzmaResult lzma_easy_buffer_encode(uint preset, LzmaCheck check, void* allocator, byte[] @in, UIntPtr in_size, byte[] @out, UIntPtr* out_pos, UIntPtr out_size)
+ => lzma_easy_buffer_encode_ptr(preset, check, allocator, @in, in_size, @out, out_pos, out_size);
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/SubStream.cs b/DebUOS/Packaging.Targets/IO/SubStream.cs
new file mode 100644
index 0000000..edaf944
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/SubStream.cs
@@ -0,0 +1,380 @@
+using System;
+using System.IO;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents parts of a , from a start byte offset for a given length.
+ ///
+ public class SubStream : Stream
+ {
+ ///
+ /// The parent stream of this stream.
+ ///
+ private Stream stream;
+
+ ///
+ /// The offset, that is, the location at which the starts.
+ ///
+ private long subStreamOffset;
+
+ ///
+ /// The length of the .
+ ///
+ private long subStreamLength;
+
+ ///
+ /// A value indicating whether the parent stream should be closed when this
+ /// is closed, or not.
+ ///
+ private bool leaveParentOpen;
+
+ ///
+ /// A value indicating whether this should only support read-only operations.
+ ///
+ private bool readOnly;
+
+ ///
+ /// The current position of the . This allows us keep the
+ /// consistent accross multiple calls, even if the position of changes (e.g. because
+ /// another operates on it).
+ ///
+ private long position;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The parent stream of this stream.
+ ///
+ ///
+ /// The offset at which the stream starts.
+ ///
+ ///
+ /// The length of the .
+ ///
+ ///
+ /// A value indicating whether the parent stream should be closed when this
+ /// is closed, or not.
+ ///
+ ///
+ /// A value indicating whether the be opened in read-only mode or not.
+ ///
+ public SubStream(Stream stream, long offset, long length, bool leaveParentOpen = false, bool readOnly = false)
+ {
+ this.stream = stream;
+ this.subStreamOffset = offset;
+ this.subStreamLength = length;
+ this.leaveParentOpen = leaveParentOpen;
+ this.readOnly = readOnly;
+ this.position = 0;
+
+ if (this.stream.CanSeek)
+ {
+ this.Seek(0, SeekOrigin.Begin);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the current stream supports reading
+ ///
+ public override bool CanRead
+ {
+ get
+ {
+ return this.stream.CanRead;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the current stream supports seeking.
+ ///
+ public override bool CanSeek
+ {
+ get
+ {
+ return this.stream.CanSeek;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the current stream supports writing.
+ ///
+ public override bool CanWrite
+ {
+ get
+ {
+ return !this.readOnly && this.stream.CanWrite;
+ }
+ }
+
+ ///
+ /// Gets the length in bytes of the stream.
+ ///
+ public override long Length
+ {
+ get
+ {
+ return this.subStreamLength;
+ }
+ }
+
+ ///
+ /// Gets or sets the position within the current .
+ ///
+ public override long Position
+ {
+ get
+ {
+ return this.position;
+ }
+
+ set
+ {
+ lock (this.stream)
+ {
+ this.stream.Position = value + this.Offset;
+ this.position = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets the parent stream of this .
+ ///
+ internal Stream Stream
+ {
+ get
+ {
+ return this.stream;
+ }
+ }
+
+ ///
+ /// Gets the offset at which the starts.
+ ///
+ internal long Offset
+ {
+ get
+ {
+ return this.subStreamOffset;
+ }
+ }
+
+ ///
+ /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
+ ///
+ public override void Flush()
+ {
+ lock (this.stream)
+ {
+ this.stream.Flush();
+ }
+ }
+
+ ///
+ /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
+ ///
+ ///
+ /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset
+ /// and (offset + count - 1) replaced by the bytes read from the current source.
+ ///
+ ///
+ /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream.
+ ///
+ ///
+ /// The maximum number of bytes to be read from the current stream.
+ ///
+ ///
+ /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are
+ /// not currently available, or zero (0) if the end of the stream has been reached.
+ ///
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ lock (this.stream)
+ {
+ this.EnsurePosition();
+
+ // Make sure we don't pass the size of the substream
+ long bytesRemaining = this.Length - this.Position;
+ long bytesToRead = Math.Min(count, bytesRemaining);
+
+ if (bytesToRead < 0)
+ {
+ bytesToRead = 0;
+ }
+
+ var read = this.stream.Read(buffer, offset, (int)bytesToRead);
+ this.position += read;
+ return read;
+ }
+ }
+
+ ///
+ /// Writes a sequence of bytes to the current stream and advances the current position
+ /// within this stream by the number of bytes written.
+ ///
+ ///
+ /// An array of bytes. This method copies count bytes from buffer to the current stream.
+ ///
+ ///
+ /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream.
+ ///
+ ///
+ /// The number of bytes to be written to the current stream.
+ ///
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (this.readOnly)
+ {
+ throw new NotSupportedException();
+ }
+
+ lock (this.stream)
+ {
+ this.EnsurePosition();
+
+ if (this.Position + offset + count > this.Length || this.Position < 0)
+ {
+ throw new InvalidOperationException("This write operation would exceed the current length of the substream.");
+ }
+
+ this.stream.Write(buffer, offset, count);
+ this.position += count;
+ }
+ }
+
+ ///
+ /// Writes a byte to the current position in the stream and advances the position within the stream by one byte.
+ ///
+ ///
+ /// The byte to write to the stream.
+ ///
+ public override void WriteByte(byte value)
+ {
+ if (this.readOnly)
+ {
+ throw new NotSupportedException();
+ }
+
+ lock (this.stream)
+ {
+ this.EnsurePosition();
+
+ if (this.Position > this.Length || this.Position < 0)
+ {
+ throw new InvalidOperationException("This write operation would exceed the current length of the substream.");
+ }
+
+ this.stream.WriteByte(value);
+ this.position++;
+ }
+ }
+
+ ///
+ /// Sets the position within the current stream.
+ ///
+ ///
+ /// A byte offset relative to the origin parameter.
+ ///
+ ///
+ /// A value of type SeekOrigin indicating the reference point used to obtain the new position.
+ ///
+ ///
+ /// The new position within the current stream.
+ ///
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ lock (this.stream)
+ {
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ offset += this.subStreamOffset;
+ break;
+
+ case SeekOrigin.End:
+ long enddelta = this.subStreamOffset + this.subStreamLength - this.stream.Length;
+ offset += enddelta;
+ break;
+
+ case SeekOrigin.Current:
+ // Nothing to do, because we'll pass SeekOrigin.Current to the
+ // parent stream.
+ break;
+ }
+
+ // If we're doing an absolute seek, we don't care about the position,
+ // but if the seek is relative, make sure we start from the correct position
+ if (origin == SeekOrigin.Current)
+ {
+ this.EnsurePosition();
+ }
+
+ var parentPosition = this.stream.Seek(offset, origin);
+ this.position = parentPosition - this.Offset;
+ return this.position;
+ }
+ }
+
+ ///
+ /// Sets the length of the current stream.
+ ///
+ ///
+ /// The desired length of the current stream in bytes.
+ ///
+ public override void SetLength(long value)
+ {
+ if (this.readOnly)
+ {
+ throw new NotSupportedException();
+ }
+
+ this.subStreamLength = value;
+ }
+
+ ///
+ /// Updates the size of this relative to its parent stream.
+ ///
+ ///
+ /// The new offset.
+ ///
+ ///
+ /// The new length.
+ ///
+ public void UpdateWindow(long offset, long length)
+ {
+ this.subStreamOffset = offset;
+ this.subStreamLength = length;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (!this.leaveParentOpen)
+ {
+ this.stream.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ ///
+ /// Makes sure the position of the parent stream is aligned with the position we have stored locally.
+ ///
+ ///
+ /// Take a scenario where you have two objects that navigate the same .
+ /// They can both seek independently, so the parent's position will change without the other
+ /// knowing about it. Calling corrects that, enabling scenarios where you can synchronously
+ /// do I/O on both streams: things like should start working.
+ /// This will, however, not work for multi-threaded access.
+ ///
+ private void EnsurePosition()
+ {
+ if (this.stream.Position != this.position + this.Offset)
+ {
+ this.stream.Position = this.position + this.Offset;
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/TarFile.cs b/DebUOS/Packaging.Targets/IO/TarFile.cs
new file mode 100644
index 0000000..48f708a
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/TarFile.cs
@@ -0,0 +1,86 @@
+using System;
+using System.IO;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents a Tar archive.
+ ///
+ public class TarFile : ArchiveFile
+ {
+ private TarHeader entryHeader;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A which represents the tar file.
+ ///
+ ///
+ /// to leave the underlying open when this
+ /// is disposed of; otherwise, .
+ ///
+ public TarFile(Stream stream, bool leaveOpen)
+ : base(stream, leaveOpen)
+ {
+ }
+
+ ///
+ /// Gets the link target of this file.
+ ///
+ public string LinkName
+ {
+ get;
+ private set;
+ }
+
+ ///
+ public override bool Read()
+ {
+ this.Align(512);
+ this.EntryStream?.Dispose();
+ this.EntryStream = null;
+
+ this.entryHeader = this.Stream.ReadStruct();
+ this.FileHeader = this.entryHeader;
+ this.FileName = this.entryHeader.FileName;
+ this.LinkName = this.entryHeader.LinkName;
+
+ // There are two empty blocks at the end of the file.
+ if (string.IsNullOrEmpty(this.entryHeader.Magic))
+ {
+ return false;
+ }
+
+ if (this.entryHeader.Magic != "ustar")
+ {
+ throw new InvalidDataException("The magic for the file entry is invalid");
+ }
+
+ this.Align(512);
+
+ // TODO: Validate Checksum
+ this.EntryStream = new SubStream(this.Stream, this.Stream.Position, this.entryHeader.FileSize, leaveParentOpen: true);
+
+ if (this.entryHeader.TypeFlag == TarTypeFlag.LongName)
+ {
+ var longFileName = this.ReadAsUtf8String();
+ var read = this.Read();
+ this.FileName = longFileName;
+
+ return read;
+ }
+ else if (this.entryHeader.TypeFlag == TarTypeFlag.LongLink)
+ {
+ var longLinkName = this.ReadAsUtf8String();
+ var read = this.Read();
+ this.LinkName = longLinkName;
+ return read;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/TarFileCreator.cs b/DebUOS/Packaging.Targets/IO/TarFileCreator.cs
new file mode 100644
index 0000000..db2eebd
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/TarFileCreator.cs
@@ -0,0 +1,176 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ internal static class TarFileCreator
+ {
+ ///
+ /// Generates a based on a list of
+ /// values.
+ ///
+ ///
+ /// The values based on which to generate the .
+ ///
+ ///
+ /// The which will hold the .
+ ///
+ public static void FromArchiveEntries(List archiveEntries, Stream targetStream)
+ {
+ using (TarFile cpioFile = new TarFile(targetStream, leaveOpen: true))
+ {
+ foreach (var entry in archiveEntries)
+ {
+ WriteEntry(targetStream, entry);
+ }
+
+ WriteTrailer(targetStream);
+ }
+ }
+
+ public static void WriteTrailer(Stream stream)
+ {
+ // The stream should already be aligned; as it is aligned after every entry.
+ // As a safety measure, for streams which can report on .Position, align the
+ // stream again.
+ if (stream.CanSeek)
+ {
+ Align(stream);
+ }
+
+ var trailer = new byte[1024];
+ stream.Write(trailer, 0, trailer.Length);
+ }
+
+ public static void WriteEntry(Stream stream, TarHeader header, Stream data)
+ {
+ header.Checksum = header.ComputeChecksum();
+ int written = stream.WriteStruct(header);
+ Align(stream, written);
+ data.CopyTo(stream);
+ Align(stream, data.Length);
+ }
+
+ public static void WriteEntry(Stream stream, ArchiveEntry entry, Stream data = null)
+ {
+ var targetPath = entry.TargetPath;
+ if (!targetPath.StartsWith("."))
+ {
+ targetPath = "." + targetPath;
+ }
+
+ // Handle long file names (> 99 characters). If this is the case, add a "././@LongLink" pseudo-entry
+ // which contains the full name.
+ if (targetPath.Length > 99)
+ {
+ // Must include a trailing \0
+ var nameLength = Encoding.UTF8.GetByteCount(targetPath);
+ byte[] entryName = new byte[nameLength + 1];
+
+ Encoding.UTF8.GetBytes(targetPath, 0, targetPath.Length, entryName, 0);
+
+ ArchiveEntry nameEntry = new ArchiveEntry()
+ {
+ Mode = entry.Mode,
+ Modified = entry.Modified,
+ TargetPath = "././@LongLink",
+ Owner = entry.Owner,
+ Group = entry.Group
+ };
+
+ using (MemoryStream nameStream = new MemoryStream(entryName))
+ {
+ WriteEntry(stream, nameEntry, nameStream);
+ }
+
+ targetPath = targetPath.Substring(0, 99);
+ }
+
+ var isDir = entry.Mode.HasFlag(LinuxFileMode.S_IFDIR);
+ var isLink = !isDir && !string.IsNullOrWhiteSpace(entry.LinkTo);
+ var isFile = !isDir && !isLink;
+
+ TarTypeFlag type;
+
+ if (entry.TargetPath == "././@LongLink")
+ {
+ type = TarTypeFlag.LongName;
+ }
+ else if (isFile)
+ {
+ type = TarTypeFlag.RegType;
+ }
+ else if (isDir)
+ {
+ type = TarTypeFlag.DirType;
+ }
+ else
+ {
+ type = TarTypeFlag.SymType;
+ }
+
+ bool dispose = false;
+ if (data == null)
+ {
+ if (isFile)
+ {
+ dispose = true;
+ data = File.OpenRead(entry.SourceFilename);
+ }
+ else
+ {
+ data = new MemoryStream();
+ }
+ }
+
+ try
+ {
+ var hdr = new TarHeader()
+ {
+ // No need to set the file type, the tar header has a special field for that.
+ FileMode = entry.Mode & LinuxFileMode.PermissionsMask,
+ DevMajor = null,
+ DevMinor = null,
+ FileName = targetPath,
+ FileSize = (uint)data.Length,
+ GroupId = 0,
+ UserId = 0,
+ GroupName = entry.Group,
+ LinkName = isLink ? entry.LinkTo : string.Empty,
+ Prefix = string.Empty,
+ TypeFlag = type,
+ UserName = entry.Owner,
+ Version = null,
+ LastModified = entry.Modified,
+ Magic = "ustar"
+ };
+ WriteEntry(stream, hdr, data);
+ }
+ finally
+ {
+ if (dispose)
+ {
+ data.Dispose();
+ }
+ }
+ }
+
+ private static void Align(Stream stream)
+ {
+ Align(stream, stream.Position);
+ }
+
+ private static void Align(Stream stream, long position)
+ {
+ var spos = position % 512;
+ if (spos == 0)
+ {
+ return;
+ }
+
+ var align = new byte[512 - spos];
+ stream.Write(align, 0, align.Length);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/IO/TarHeader.cs b/DebUOS/Packaging.Targets/IO/TarHeader.cs
new file mode 100644
index 0000000..ca9bde8
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/TarHeader.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents the header for an individual entry in a .tar archive.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct TarHeader : IArchiveHeader
+ {
+ private static readonly string Empty8 = new string('\0', 8);
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 100)]
+ private byte[] name;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)]
+ private byte[] mode;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)]
+ private byte[] uid;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)]
+ private byte[] gid;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 12)]
+ private byte[] size;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 12)]
+ private byte[] mtime;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)]
+ private byte[] chksum;
+
+ private byte typeflag;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 100)]
+ private byte[] linkname;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 6)]
+ private byte[] magic;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 2)]
+ private byte[] version;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 32)]
+ private byte[] uname;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 32)]
+ private byte[] gname;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)]
+ private byte[] devmajor;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)]
+ private byte[] devminor;
+
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 155)]
+ private byte[] prefix;
+
+ ///
+ /// Gets or sets the name of the current file.
+ ///
+ public string FileName
+ {
+ get => this.GetString(this.name, 100);
+ set => this.name = this.CreateString(value, 100);
+ }
+
+ public LinuxFileMode FileMode
+ {
+ get => (LinuxFileMode)Convert.ToUInt32(this.GetString(this.mode, 8), 8);
+ set => this.mode = this.GetUIntTo8((uint)value);
+ }
+
+ public uint UserId
+ {
+ get => Convert.ToUInt32(this.GetString(this.uid, 8), 8);
+ set => this.uid = this.GetUIntTo8(value);
+ }
+
+ public uint GroupId
+ {
+ get => Convert.ToUInt32(this.GetString(this.gid, 8), 8);
+ set => this.gid = this.GetUIntTo8(value);
+ }
+
+ public uint FileSize
+ {
+ get => Convert.ToUInt32(this.GetString(this.size, 12), 8);
+ set => this.size = this.GetUIntTo8(value, 12);
+ }
+
+ public DateTimeOffset LastModified
+ {
+ get => DateTimeOffset.FromUnixTimeSeconds((long)Convert.ToUInt64(this.GetString(this.mtime, 12), 8));
+ set => this.mtime = this.GetUIntTo8((uint)value.ToUnixTimeSeconds(), 12);
+ }
+
+ public uint Checksum
+ {
+ get => Convert.ToUInt32(this.GetString(this.chksum, 8), 8);
+ set
+ {
+ // GNU tar does that, I have no idea why
+ var s = this.GetUIntTo8(value, 7);
+ var buffer = new byte[8];
+ Array.Copy(s, buffer, 7);
+ buffer[7] = 32;
+ this.chksum = buffer;
+ }
+ }
+
+ public TarTypeFlag TypeFlag
+ {
+ get => (TarTypeFlag)this.typeflag;
+ set => this.typeflag = (byte)value;
+ }
+
+ public string LinkName
+ {
+ get => this.GetString(this.linkname, 100);
+ set => this.linkname = this.CreateString(value, 100);
+ }
+
+ public string Magic
+ {
+ get => this.GetString(this.magic, 6)?.Trim();
+ set => this.magic = this.CreateString(value.PadRight(6), 6);
+ }
+
+ public uint? Version
+ {
+ get
+ {
+ var v = this.GetString(this.version, 2);
+ if (uint.TryParse(v, out uint rv))
+ {
+ return rv;
+ }
+
+ return null;
+ }
+
+ set
+ {
+ if (value == null)
+ {
+ this.version = new byte[] { 32, 0 };
+ }
+ else
+ {
+ this.version = this.GetUIntTo8(value, 2);
+ }
+ }
+ }
+
+ public string UserName
+ {
+ get => this.GetString(this.uname, 32);
+ set => this.uname = this.CreateString(value, 32);
+ }
+
+ public string GroupName
+ {
+ get => this.GetString(this.gname, 32);
+ set => this.gname = this.CreateString(value, 32);
+ }
+
+ public uint? DevMajor
+ {
+ get => this.devmajor[0] == 0 ? (uint?)null : Convert.ToUInt32(this.GetString(this.devmajor, 8), 8);
+ set => this.devmajor = this.GetUIntTo8(value);
+ }
+
+ public uint? DevMinor
+ {
+ get => this.devminor[0] == 0 ? (uint?)null : Convert.ToUInt32(this.GetString(this.devminor, 8), 8);
+ set => this.devminor = this.GetUIntTo8(value);
+ }
+
+ public string Prefix
+ {
+ get => this.GetString(this.prefix, 155);
+ set => this.prefix = this.CreateString(value, 155);
+ }
+
+ public uint ComputeChecksum()
+ {
+ var other = this;
+ other.chksum = new byte[8];
+ for (var c = 0; c < 8; c++)
+ {
+ other.chksum[c] = 32;
+ }
+
+ var data = new byte[Marshal.SizeOf()];
+ fixed (byte* ptr = data)
+ {
+ Marshal.StructureToPtr(other, new IntPtr(ptr), true);
+ }
+
+ uint sum = 0;
+ foreach (var b in data)
+ {
+ sum += b;
+ }
+
+ return sum;
+ }
+
+ private string GetString(byte[] data, int maxLen)
+ {
+ int len;
+ for (len = 0; len < maxLen; len++)
+ {
+ if (data[len] == 0)
+ {
+ break;
+ }
+ }
+
+ if (len == 0)
+ {
+ return null;
+ }
+
+ return Encoding.UTF8.GetString(data, 0, len);
+ }
+
+ private byte[] GetUIntTo8(uint? data, int len = 8)
+ {
+ if (data == null)
+ {
+ return new byte[len];
+ }
+
+ return this.CreateString(Convert.ToString(data.Value, 8).PadLeft(len - 1, '0'), len);
+ }
+
+ private byte[] CreateString(string s, int len)
+ {
+ var target = new byte[len];
+ if (s == null)
+ {
+ return target;
+ }
+
+ var buffer = Encoding.UTF8.GetBytes(s);
+ if (buffer.Length > len)
+ {
+ throw new Exception($"String {s} exceeds the limit of {len}");
+ }
+
+ for (var c = 0; c < len; c++)
+ {
+ target[c] = (c < buffer.Length) ? buffer[c] : (byte)0;
+ }
+
+ return target;
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/IO/TarTypeFlag.cs b/DebUOS/Packaging.Targets/IO/TarTypeFlag.cs
new file mode 100644
index 0000000..fc54f9f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/TarTypeFlag.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Packaging.Targets.IO
+{
+ internal enum TarTypeFlag : byte
+ {
+ RegType = (byte)'0',
+ ARegType = (byte)'\0',
+ LnkType = (byte)'1',
+ SymType = (byte)'2',
+ ChrType = (byte)'3',
+ BlkType = (byte)'4',
+ DirType = (byte)'5',
+ FifoType = (byte)'6',
+ ConttType = (byte)'7',
+ ExtendedHeader = (byte)'x',
+ GlobalExtendedHeader = (byte)'g',
+ LongName = (byte)'L', // See https://www.gnu.org/software/tar/manual/html_node/Standard.html
+ LongLink = (byte)'K' // See https://www.gnu.org/software/tar/manual/html_node/Standard.html
+ }
+}
diff --git a/DebUOS/Packaging.Targets/IO/XZInputStream.cs b/DebUOS/Packaging.Targets/IO/XZInputStream.cs
new file mode 100644
index 0000000..4a01554
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/XZInputStream.cs
@@ -0,0 +1,333 @@
+/*
+ * The MIT License (MIT)
+
+ * Copyright (c) 2015 Roman Belkov, Kirill Melentyev
+
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.IO
+{
+ ///
+ /// Represents a which can decompress xz-compressed data.
+ ///
+ public class XZInputStream : Stream
+ {
+ ///
+ /// The size of the buffer
+ ///
+ private const int BufSize = 512;
+
+ private readonly List internalBuffer = new List();
+ private readonly Stream innerStream;
+ private readonly IntPtr inbuf;
+ private readonly IntPtr outbuf;
+ private LzmaStream lzmaStream;
+ private long length;
+ private long position;
+ private bool disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The underlying from which to decompress the data.
+ ///
+ public XZInputStream(Stream stream)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ this.innerStream = stream;
+
+ var ret = NativeMethods.lzma_stream_decoder(ref this.lzmaStream, ulong.MaxValue, LzmaDecodeFlags.Concatenated);
+
+ this.inbuf = Marshal.AllocHGlobal(BufSize);
+ this.outbuf = Marshal.AllocHGlobal(BufSize);
+
+ this.lzmaStream.AvailIn = 0;
+ this.lzmaStream.NextOut = this.outbuf;
+ this.lzmaStream.AvailOut = BufSize;
+
+ if (ret == LzmaResult.OK)
+ {
+ return;
+ }
+
+ switch (ret)
+ {
+ case LzmaResult.MemError:
+ throw new Exception("Memory allocation failed");
+
+ case LzmaResult.OptionsError:
+ throw new Exception("Unsupported decompressor flags");
+
+ default:
+ throw new Exception("Unknown error, possibly a bug");
+ }
+ }
+
+ ///
+ public override bool CanRead
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return true;
+ }
+ }
+
+ ///
+ public override bool CanSeek
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return false;
+ }
+ }
+
+ ///
+ public override bool CanWrite
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return false;
+ }
+ }
+
+ ///
+ public override long Length
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+
+ const int streamFooterSize = 12;
+
+ if (this.length == 0)
+ {
+ var lzmaStreamFlags = default(LzmaStreamFlags);
+ var streamFooter = new byte[streamFooterSize];
+
+ this.innerStream.Seek(-streamFooterSize, SeekOrigin.End);
+ this.innerStream.Read(streamFooter, 0, streamFooterSize);
+
+ NativeMethods.lzma_stream_footer_decode(ref lzmaStreamFlags, streamFooter);
+ var indexPointer = new byte[lzmaStreamFlags.BackwardSize];
+
+ this.innerStream.Seek(-streamFooterSize - (long)lzmaStreamFlags.BackwardSize, SeekOrigin.End);
+ this.innerStream.Read(indexPointer, 0, (int)lzmaStreamFlags.BackwardSize);
+ this.innerStream.Seek(0, SeekOrigin.Begin);
+
+ var index = IntPtr.Zero;
+ var memLimit = ulong.MaxValue;
+ uint inPos = 0;
+
+ NativeMethods.lzma_index_buffer_decode(ref index, ref memLimit, IntPtr.Zero, indexPointer, ref inPos, lzmaStreamFlags.BackwardSize);
+
+ if (inPos != lzmaStreamFlags.BackwardSize)
+ {
+ NativeMethods.lzma_index_end(index, IntPtr.Zero);
+ throw new Exception("Index decoding failed!");
+ }
+
+ var uSize = NativeMethods.lzma_index_uncompressed_size(index);
+
+ NativeMethods.lzma_index_end(index, IntPtr.Zero);
+ this.length = (long)uSize;
+ return this.length;
+ }
+ else
+ {
+ return this.length;
+ }
+ }
+ }
+
+ ///
+ public override long Position
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return this.position;
+ }
+
+ set
+ {
+ this.EnsureNotDisposed();
+ throw new NotSupportedException("XZ Stream does not support setting position");
+ }
+ }
+
+ ///
+ public override void Flush()
+ {
+ this.EnsureNotDisposed();
+
+ throw new NotSupportedException("XZ Stream does not support flush");
+ }
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ this.EnsureNotDisposed();
+
+ throw new NotSupportedException("XZ Stream does not support seek");
+ }
+
+ ///
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException("XZ Stream does not support setting length");
+ }
+
+ ///
+ /// Reads bytes from stream
+ ///
+ /// byte read or -1 on end of stream
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ this.EnsureNotDisposed();
+
+ var action = LzmaAction.Run;
+
+ var readBuf = new byte[BufSize];
+ var outManagedBuf = new byte[BufSize];
+
+ while (this.internalBuffer.Count < count)
+ {
+ if (this.lzmaStream.AvailIn == 0)
+ {
+ this.lzmaStream.AvailIn = (uint)this.innerStream.Read(readBuf, 0, readBuf.Length);
+ Marshal.Copy(readBuf, 0, this.inbuf, (int)this.lzmaStream.AvailIn);
+ this.lzmaStream.NextIn = this.inbuf;
+
+ if (this.lzmaStream.AvailIn == 0)
+ {
+ action = LzmaAction.Finish;
+ }
+ }
+
+ var ret = NativeMethods.lzma_code(ref this.lzmaStream, action);
+
+ if (this.lzmaStream.AvailOut == 0 || ret == LzmaResult.StreamEnd)
+ {
+ var writeSize = BufSize - (int)this.lzmaStream.AvailOut;
+ Marshal.Copy(this.outbuf, outManagedBuf, 0, writeSize);
+
+ this.internalBuffer.AddRange(outManagedBuf);
+ var tail = outManagedBuf.Length - writeSize;
+ this.internalBuffer.RemoveRange(this.internalBuffer.Count - tail, tail);
+
+ this.lzmaStream.NextOut = this.outbuf;
+ this.lzmaStream.AvailOut = BufSize;
+ }
+
+ if (ret != LzmaResult.OK)
+ {
+ if (ret == LzmaResult.StreamEnd)
+ {
+ break;
+ }
+
+ NativeMethods.lzma_end(ref this.lzmaStream);
+
+ switch (ret)
+ {
+ case LzmaResult.MemError:
+ throw new Exception("Memory allocation failed");
+
+ case LzmaResult.FormatError:
+ throw new Exception("The input is not in the .xz format");
+
+ case LzmaResult.OptionsError:
+ throw new Exception("Unsupported compression options");
+
+ case LzmaResult.DataError:
+ throw new Exception("Compressed file is corrupt");
+
+ case LzmaResult.BufferError:
+ throw new Exception("Compressed file is truncated or otherwise corrupt");
+
+ default:
+ throw new Exception("Unknown error.Possibly a bug");
+ }
+ }
+ }
+
+ if (this.internalBuffer.Count >= count)
+ {
+ this.internalBuffer.CopyTo(0, buffer, offset, count);
+ this.internalBuffer.RemoveRange(0, count);
+ this.position += count;
+ return count;
+ }
+ else
+ {
+ var intBufLength = this.internalBuffer.Count;
+ this.internalBuffer.CopyTo(0, buffer, offset, intBufLength);
+ this.internalBuffer.Clear();
+ this.position += intBufLength;
+ return intBufLength;
+ }
+ }
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException("XZ Input stream does not support writing");
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (this.disposed)
+ {
+ return;
+ }
+
+ NativeMethods.lzma_end(ref this.lzmaStream);
+
+ Marshal.FreeHGlobal(this.inbuf);
+ Marshal.FreeHGlobal(this.outbuf);
+
+ base.Dispose(disposing);
+
+ this.disposed = true;
+ }
+
+ private void EnsureNotDisposed()
+ {
+ if (this.disposed)
+ {
+ throw new ObjectDisposedException(nameof(XZInputStream));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/IO/XZOutputStream.cs b/DebUOS/Packaging.Targets/IO/XZOutputStream.cs
new file mode 100644
index 0000000..1e76991
--- /dev/null
+++ b/DebUOS/Packaging.Targets/IO/XZOutputStream.cs
@@ -0,0 +1,339 @@
+/*
+ * The MIT License (MIT)
+
+ * Copyright (c) 2015 Roman Belkov, Kirill Melentyev
+
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+*/
+
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace Packaging.Targets.IO
+{
+ public unsafe class XZOutputStream : Stream
+ {
+ ///
+ /// Default compression preset.
+ ///
+ public const uint DefaultPreset = 6;
+ public const uint PresetExtremeFlag = (uint)1 << 31;
+
+ // You can tweak BufSize value to get optimal results
+ // of speed and chunk size
+ private const int BufSize = 4096;
+
+ private readonly Stream innerStream;
+ private readonly bool leaveOpen;
+ private readonly byte[] outbuf;
+ private LzmaStream lzmaStream;
+ private bool disposed;
+
+ public XZOutputStream(Stream s)
+ : this(s, DefaultThreads)
+ {
+ }
+
+ public XZOutputStream(Stream s, int threads)
+ : this(s, threads, DefaultPreset)
+ {
+ }
+
+ public XZOutputStream(Stream s, int threads, uint preset)
+ : this(s, threads, preset, false)
+ {
+ }
+
+ public XZOutputStream(Stream s, int threads, uint preset, bool leaveOpen)
+ {
+ this.innerStream = s;
+ this.leaveOpen = leaveOpen;
+
+ LzmaResult ret;
+ if (threads == 1 || !NativeMethods.SupportsMultiThreading)
+ {
+ ret = NativeMethods.lzma_easy_encoder(ref this.lzmaStream, preset, LzmaCheck.Crc64);
+ }
+ else
+ {
+ if (threads <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(threads));
+ }
+
+ if (threads > Environment.ProcessorCount)
+ {
+ Trace.TraceWarning("{0} threads required, but only {1} processors available", threads, Environment.ProcessorCount);
+ threads = Environment.ProcessorCount;
+ }
+
+ var mt = new LzmaMT()
+ {
+ preset = preset,
+ check = LzmaCheck.Crc64,
+ threads = (uint)threads
+ };
+ ret = NativeMethods.lzma_stream_encoder_mt(ref this.lzmaStream, ref mt);
+ }
+
+ if (ret == LzmaResult.OK)
+ {
+ this.outbuf = new byte[BufSize];
+ this.lzmaStream.AvailOut = BufSize;
+ return;
+ }
+
+ GC.SuppressFinalize(this);
+ throw GetError(ret);
+ }
+
+ ~XZOutputStream()
+ {
+ this.Dispose(false);
+ }
+
+ public static int DefaultThreads => Environment.ProcessorCount;
+
+ public static bool SupportsMultiThreading => NativeMethods.SupportsMultiThreading;
+
+ ///
+ public override bool CanRead
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return false;
+ }
+ }
+
+ ///
+ public override bool CanSeek
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return false;
+ }
+ }
+
+ ///
+ public override bool CanWrite
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return true;
+ }
+ }
+
+ ///
+ public override long Length
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ throw new NotSupportedException();
+ }
+ }
+
+ ///
+ public override long Position
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ throw new NotSupportedException();
+ }
+
+ set
+ {
+ this.EnsureNotDisposed();
+ throw new NotSupportedException();
+ }
+ }
+
+ ///
+ /// Single-call buffer encoding
+ ///
+ public static byte[] Encode(byte[] buffer, uint preset = DefaultPreset)
+ {
+ var res = new byte[(long)NativeMethods.lzma_stream_buffer_bound((UIntPtr)buffer.Length)];
+
+ UIntPtr outPos;
+ var ret = NativeMethods.lzma_easy_buffer_encode(preset, LzmaCheck.Crc64, null, buffer, (UIntPtr)buffer.Length, res, &outPos, (UIntPtr)res.Length);
+ if (ret != LzmaResult.OK)
+ {
+ throw GetError(ret);
+ }
+
+ if ((long)outPos < res.Length)
+ {
+ Array.Resize(ref res, (int)(ulong)outPos);
+ }
+
+ return res;
+ }
+
+ ///
+ public override void Flush()
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ this.EnsureNotDisposed();
+ throw new NotSupportedException();
+ }
+
+ ///
+ public override void SetLength(long value)
+ {
+ this.EnsureNotDisposed();
+ throw new NotSupportedException();
+ }
+
+ ///
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ this.EnsureNotDisposed();
+ throw new NotSupportedException();
+ }
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ this.EnsureNotDisposed();
+
+ if (count == 0)
+ {
+ return;
+ }
+
+ var guard = buffer[checked((uint)offset + (uint)count) - 1];
+
+ if (this.lzmaStream.AvailIn != 0)
+ {
+ throw new InvalidOperationException();
+ }
+
+ this.lzmaStream.AvailIn = (uint)count;
+ do
+ {
+ LzmaResult ret;
+ fixed (byte* inbuf = &buffer[offset])
+ {
+ this.lzmaStream.NextIn = (IntPtr)inbuf;
+ fixed (byte* outbuf = &this.outbuf[BufSize - this.lzmaStream.AvailOut])
+ {
+ this.lzmaStream.NextOut = (IntPtr)outbuf;
+ ret = NativeMethods.lzma_code(ref this.lzmaStream, LzmaAction.Run);
+ }
+
+ offset += (int)((ulong)this.lzmaStream.NextIn - (ulong)(IntPtr)inbuf);
+ }
+
+ if (ret != LzmaResult.OK)
+ {
+ throw this.ThrowError(ret);
+ }
+
+ if (this.lzmaStream.AvailOut == 0)
+ {
+ this.innerStream.Write(this.outbuf, 0, BufSize);
+ this.lzmaStream.AvailOut = BufSize;
+ }
+ }
+ while (this.lzmaStream.AvailIn != 0);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ // finish encoding only if all input has been successfully processed
+ if (this.lzmaStream.InternalState != IntPtr.Zero && this.lzmaStream.AvailIn == 0)
+ {
+ LzmaResult ret;
+ do
+ {
+ fixed (byte* outbuf = &this.outbuf[BufSize - (int)this.lzmaStream.AvailOut])
+ {
+ this.lzmaStream.NextOut = (IntPtr)outbuf;
+ ret = NativeMethods.lzma_code(ref this.lzmaStream, LzmaAction.Finish);
+ }
+
+ if (ret > LzmaResult.StreamEnd)
+ {
+ throw this.ThrowError(ret);
+ }
+
+ var writeSize = BufSize - (int)this.lzmaStream.AvailOut;
+ if (writeSize != 0)
+ {
+ this.innerStream.Write(this.outbuf, 0, writeSize);
+ this.lzmaStream.AvailOut = BufSize;
+ }
+ }
+ while (ret != LzmaResult.StreamEnd);
+ }
+
+ NativeMethods.lzma_end(ref this.lzmaStream);
+
+ if (disposing && !this.leaveOpen)
+ {
+ this.innerStream?.Dispose();
+ }
+
+ base.Dispose(disposing);
+
+ this.disposed = true;
+ }
+
+ private static Exception GetError(LzmaResult ret)
+ {
+ switch (ret)
+ {
+ case LzmaResult.MemError: return new OutOfMemoryException("Memory allocation failed");
+ case LzmaResult.OptionsError: return new ArgumentException("Specified preset is not supported");
+ case LzmaResult.UnsupportedCheck: return new Exception("Specified integrity check is not supported");
+ case LzmaResult.DataError: return new InvalidDataException("File size limits exceeded");
+ default: return new Exception("Unknown error, possibly a bug: " + ret);
+ }
+ }
+
+ ///
+ /// Throws an exception if this stream is disposed of.
+ ///
+ private void EnsureNotDisposed()
+ {
+ if (this.disposed)
+ {
+ throw new ObjectDisposedException(nameof(XZOutputStream));
+ }
+ }
+
+ private Exception ThrowError(LzmaResult ret)
+ {
+ NativeMethods.lzma_end(ref this.lzmaStream);
+ return GetError(ret);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/Native/FunctionLoader.cs b/DebUOS/Packaging.Targets/Native/FunctionLoader.cs
new file mode 100644
index 0000000..1500691
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Native/FunctionLoader.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.Native
+{
+ ///
+ /// Supports loading functions from native libraries. Provides a more flexible alternative to P/Invoke.
+ ///
+ internal static class FunctionLoader
+ {
+ ///
+ /// Attempts to load a native library.
+ ///
+ ///
+ /// Possible names of the library on Windows. This can include full paths.
+ ///
+ ///
+ /// Possible names of the library on Linux.
+ ///
+ ///
+ /// Possible names of the library on macOS.
+ ///
+ ///
+ /// A handle to the library when found; otherwise, .
+ ///
+ public static IntPtr LoadNativeLibrary(IEnumerable windowsNames, IEnumerable linuxNames, IEnumerable osxNames)
+ {
+ IntPtr lib = IntPtr.Zero;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ foreach (var name in linuxNames)
+ {
+ lib = LinuxNativeMethods.dlopen(name, LinuxNativeMethods.RTLD_NOW);
+
+ if (lib != IntPtr.Zero)
+ {
+ break;
+ }
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ foreach (var name in osxNames)
+ {
+ lib = MacNativeMethods.dlopen(name, MacNativeMethods.RTLD_NOW);
+
+ if (lib != IntPtr.Zero)
+ {
+ break;
+ }
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ foreach (var name in windowsNames)
+ {
+ lib = WindowsNativeMethods.LoadLibrary(name);
+
+ if (lib != IntPtr.Zero)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ throw new PlatformNotSupportedException();
+ }
+
+ // This function may return a null handle. If it does, individual functions loaded from it will throw a DllNotFoundException,
+ // but not until an attempt is made to actually use the function (rather than load it). This matches how PInvokes behave.
+ return lib;
+ }
+
+ ///
+ /// Creates a delegate which invokes a native function.
+ ///
+ ///
+ /// The function delegate.
+ ///
+ ///
+ /// The native library which contains the function.
+ ///
+ ///
+ /// The name of the function for which to create the delegate.
+ ///
+ ///
+ /// A new delegate which points to the native function.
+ ///
+ public static T LoadFunctionDelegate(IntPtr nativeLibraryHandle, string functionName, bool throwOnError = true)
+ where T : class
+ {
+ IntPtr ptr = LoadFunctionPointer(nativeLibraryHandle, functionName);
+
+ if (ptr == IntPtr.Zero)
+ {
+ if (throwOnError)
+ {
+#if NETSTANDARD2_0
+ throw new EntryPointNotFoundException($"Could not find the entrypoint for {functionName}");
+#else
+ throw new Exception($"Could not find the entrypoint for {functionName}");
+#endif
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return Marshal.GetDelegateForFunctionPointer(ptr);
+ }
+
+ private static IntPtr LoadFunctionPointer(IntPtr nativeLibraryHandle, string functionName)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return LinuxNativeMethods.dlsym(nativeLibraryHandle, functionName);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return MacNativeMethods.dlsym(nativeLibraryHandle, functionName);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return WindowsNativeMethods.GetProcAddress(nativeLibraryHandle, functionName);
+ }
+ else
+ {
+ throw new PlatformNotSupportedException();
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Native/LinuxNativeMethods.cs b/DebUOS/Packaging.Targets/Native/LinuxNativeMethods.cs
new file mode 100644
index 0000000..2870a41
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Native/LinuxNativeMethods.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.Native
+{
+ internal static class LinuxNativeMethods
+ {
+ public const int RTLD_NOW = 0x002;
+
+ private const string Libdl = "libdl.so.2";
+
+ [DllImport(Libdl)]
+ public static extern IntPtr dlsym(IntPtr handle, string symbol);
+
+ [DllImport(Libdl)]
+ public static extern IntPtr dlopen(string fileName, int flag);
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Native/MacNativeMethods.cs b/DebUOS/Packaging.Targets/Native/MacNativeMethods.cs
new file mode 100644
index 0000000..ea1a706
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Native/MacNativeMethods.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.Native
+{
+ internal static class MacNativeMethods
+ {
+ public const int RTLD_NOW = 0x002;
+
+ private const string Libdl = "libdl";
+
+ [DllImport(Libdl)]
+ public static extern IntPtr dlsym(IntPtr handle, string symbol);
+
+ [DllImport(Libdl)]
+ public static extern IntPtr dlopen(string fileName, int flag);
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Native/WindowsNativeMethods.cs b/DebUOS/Packaging.Targets/Native/WindowsNativeMethods.cs
new file mode 100644
index 0000000..3389a82
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Native/WindowsNativeMethods.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets.Native
+{
+ internal static class WindowsNativeMethods
+ {
+ private const string Kernel32 = "kernel32";
+
+ [DllImport(Kernel32, CharSet = CharSet.Ansi, BestFitMapping = false)]
+ public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
+
+ ///
+ /// Loads the specified module into the address space of the calling process. The specified module may cause other modules to be loaded.
+ ///
+ ///
+ ///
+ /// The name of the module. This can be either a library module (a .dll file) or an executable module (an .exe file).
+ /// The name specified is the file name of the module and is not related to the name stored in the library module itself,
+ /// as specified by the LIBRARY keyword in the module-definition (.def) file.
+ ///
+ ///
+ /// If the string specifies a full path, the function searches only that path for the module.
+ ///
+ ///
+ /// If the string specifies a relative path or a module name without a path, the function uses a standard search strategy
+ /// to find the module; for more information, see the Remarks.
+ ///
+ ///
+ /// If the function cannot find the module, the function fails. When specifying a path, be sure to use backslashes (\),
+ /// not forward slashes (/). For more information about paths, see Naming a File or Directory.
+ ///
+ ///
+ /// If the string specifies a module name without a path and the file name extension is omitted, the function appends the
+ /// default library extension .dll to the module name. To prevent the function from appending .dll to the module name,
+ /// include a trailing point character (.) in the module name string.
+ ///
+ ///
+ ///
+ /// If the function succeeds, the return value is a handle to the module.
+ /// If the function fails, the return value is . To get extended error information, call
+ /// .
+ ///
+ ///
+ [DllImport(Kernel32, SetLastError = true)]
+ public static extern IntPtr LoadLibrary(string dllToLoad);
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Packaging.Targets.csproj b/DebUOS/Packaging.Targets/Packaging.Targets.csproj
new file mode 100644
index 0000000..4800398
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Packaging.Targets.csproj
@@ -0,0 +1,27 @@
+
+
+ netstandard2.0
+ This package supports the dotnet-pack and dotnet-zip packages. Once you've installed this package together with dotnet-zip or dotnet-tarball, you can run commands such as dotnet zip or dotnet tarball to generate .zip or .tar.gz archives which contain the published output of your project.
+ True
+
+ false
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/DebUOS/Packaging.Targets/README.md b/DebUOS/Packaging.Targets/README.md
new file mode 100644
index 0000000..e02e122
--- /dev/null
+++ b/DebUOS/Packaging.Targets/README.md
@@ -0,0 +1 @@
+Copy From https://github.com/quamotion/dotnet-packaging
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/Rpm/ChangelogEntry.cs b/DebUOS/Packaging.Targets/Rpm/ChangelogEntry.cs
new file mode 100644
index 0000000..8799cb7
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/ChangelogEntry.cs
@@ -0,0 +1,69 @@
+using System;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Represents an entry in the package changelog.
+ ///
+ internal class ChangelogEntry
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChangelogEntry()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class, and propopulates the fields.
+ ///
+ ///
+ /// The date at which the entry was created.
+ ///
+ ///
+ /// The name and e-mail address of the author.
+ ///
+ ///
+ /// A description fo the entry.
+ ///
+ public ChangelogEntry(DateTimeOffset date, string name, string text)
+ {
+ this.Date = date;
+ this.Name = name;
+ this.Text = text;
+ }
+
+ ///
+ /// Gets or sets the date at which the entry was created.
+ ///
+ public DateTimeOffset Date
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets the name and e-mail address of the author.
+ ///
+ public string Name
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets a description of the entry.
+ ///
+ public string Text
+ {
+ get;
+ set;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"{this.Date} {this.Name}: {this.Text}";
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/CollectionExtensions.cs b/DebUOS/Packaging.Targets/Rpm/CollectionExtensions.cs
new file mode 100644
index 0000000..a1320ea
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/CollectionExtensions.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Provides extension methods for the class.
+ ///
+ internal static class CollectionExtensions
+ {
+ ///
+ /// Adds multiple values at once.
+ ///
+ ///
+ /// The type of the values.
+ ///
+ ///
+ /// The collection to which to add the values.
+ ///
+ ///
+ /// The values to add.
+ ///
+ public static void AddRange(this Collection collection, IEnumerable values)
+ {
+ foreach (var v in values)
+ {
+ collection.Add(v);
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/DefaultOrder.cs b/DebUOS/Packaging.Targets/Rpm/DefaultOrder.cs
new file mode 100644
index 0000000..11f7d8e
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/DefaultOrder.cs
@@ -0,0 +1,113 @@
+using System.Collections.Generic;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Specifies the default order in which tags are saved in the various sections. Used mainly to maintain binary compatibility
+ /// with some test files.
+ ///
+ internal class DefaultOrder
+ {
+ ///
+ /// Gets the default order in which the tags are saved in the header section. The order depends on the integer value
+ /// of the tag, in ascending order. RPM is very finicky about this order - if you do not respect it, the RPM package
+ /// will almost certainly be rejected which messages such as "headerRead failed: hdr load: BAD".
+ ///
+ public static List Header
+ {
+ get
+ {
+ return new List()
+ {
+ IndexTag.RPMTAG_HEADERIMMUTABLE,
+ IndexTag.RPMTAG_HEADERI18NTABLE,
+ IndexTag.RPMTAG_NAME,
+ IndexTag.RPMTAG_VERSION,
+ IndexTag.RPMTAG_RELEASE,
+ IndexTag.RPMTAG_SUMMARY,
+ IndexTag.RPMTAG_DESCRIPTION,
+ IndexTag.RPMTAG_BUILDTIME,
+ IndexTag.RPMTAG_BUILDHOST,
+ IndexTag.RPMTAG_SIZE,
+ IndexTag.RPMTAG_DISTRIBUTION,
+ IndexTag.RPMTAG_VENDOR,
+ IndexTag.RPMTAG_LICENSE,
+ IndexTag.RPMTAG_GROUP,
+ IndexTag.RPMTAG_URL,
+ IndexTag.RPMTAG_OS,
+ IndexTag.RPMTAG_ARCH,
+ IndexTag.RPMTAG_PREIN,
+ IndexTag.RPMTAG_POSTIN,
+ IndexTag.RPMTAG_PREUN,
+ IndexTag.RPMTAG_POSTUN,
+ IndexTag.RPMTAG_FILESIZES,
+ IndexTag.RPMTAG_FILEMODES,
+ IndexTag.RPMTAG_FILERDEVS,
+ IndexTag.RPMTAG_FILEMTIMES,
+ IndexTag.RPMTAG_FILEDIGESTS,
+ IndexTag.RPMTAG_FILELINKTOS,
+ IndexTag.RPMTAG_FILEFLAGS,
+ IndexTag.RPMTAG_FILEUSERNAME,
+ IndexTag.RPMTAG_FILEGROUPNAME,
+ IndexTag.RPMTAG_SOURCERPM,
+ IndexTag.RPMTAG_FILEVERIFYFLAGS,
+ IndexTag.RPMTAG_PROVIDENAME,
+ IndexTag.RPMTAG_REQUIREFLAGS,
+ IndexTag.RPMTAG_REQUIRENAME,
+ IndexTag.RPMTAG_REQUIREVERSION,
+ IndexTag.RPMTAG_RPMVERSION,
+ IndexTag.RPMTAG_CHANGELOGTIME,
+ IndexTag.RPMTAG_CHANGELOGNAME,
+ IndexTag.RPMTAG_CHANGELOGTEXT,
+ IndexTag.RPMTAG_PREINPROG,
+ IndexTag.RPMTAG_POSTINPROG,
+ IndexTag.RPMTAG_PREUNPROG,
+ IndexTag.RPMTAG_POSTUNPROG,
+ IndexTag.RPMTAG_COOKIE,
+ IndexTag.RPMTAG_FILEDEVICES,
+ IndexTag.RPMTAG_FILEINODES,
+ IndexTag.RPMTAG_FILELANGS,
+ IndexTag.RPMTAG_PROVIDEFLAGS,
+ IndexTag.RPMTAG_PROVIDEVERSION,
+ IndexTag.RPMTAG_DIRINDEXES,
+ IndexTag.RPMTAG_BASENAMES,
+ IndexTag.RPMTAG_DIRNAMES,
+ IndexTag.RPMTAG_OPTFLAGS,
+ IndexTag.RPMTAG_DISTURL,
+ IndexTag.RPMTAG_PAYLOADFORMAT,
+ IndexTag.RPMTAG_PAYLOADCOMPRESSOR,
+ IndexTag.RPMTAG_PAYLOADFLAGS,
+ IndexTag.RPMTAG_PLATFORM,
+ IndexTag.RPMTAG_FILECOLORS,
+ IndexTag.RPMTAG_FILECLASS,
+ IndexTag.RPMTAG_CLASSDICT,
+ IndexTag.RPMTAG_FILEDEPENDSX,
+ IndexTag.RPMTAG_FILEDEPENDSN,
+ IndexTag.RPMTAG_DEPENDSDICT,
+ IndexTag.RPMTAG_SOURCEPKGID,
+ IndexTag.RPMTAG_FILEDIGESTALGO,
+ };
+ }
+ }
+
+ ///
+ /// Gets the default order in which tags are saved in the signature sectin.
+ ///
+ public static List Signature
+ {
+ get
+ {
+ return new List()
+ {
+ SignatureTag.RPMTAG_HEADERSIGNATURES,
+ SignatureTag.RPMSIGTAG_RSA,
+ SignatureTag.RPMSIGTAG_SHA1,
+ SignatureTag.RPMSIGTAG_SIZE,
+ SignatureTag.RPMSIGTAG_PGP,
+ SignatureTag.RPMSIGTAG_MD5,
+ SignatureTag.RPMSIGTAG_PAYLOADSIZE
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DebUOS/Packaging.Targets/Rpm/DependencyAttribute.cs b/DebUOS/Packaging.Targets/Rpm/DependencyAttribute.cs
new file mode 100644
index 0000000..21c8ff5
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/DependencyAttribute.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ internal enum DependencyAttribute
+ {
+ RPMSENSE_LESS = 0x02,
+ RPMSENSE_GREATER = 0x04,
+ RPMSENSE_EQUAL = 0x08,
+ RPMSENSE_PREREQ = 0x40,
+ RPMSENSE_INTERP = 0x100,
+ RPMSENSE_SCRIPT_PRE = 0x200,
+ RPMSENSE_SCRIPT_POST = 0x400,
+ RPMSENSE_SCRIPT_PREUN = 0x800,
+ RPMSENSE_SCRIPT_POSTUN = 0x1000,
+ RPMSENSE_RPMLIB = 0x1000000
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/ElfClass.cs b/DebUOS/Packaging.Targets/Rpm/ElfClass.cs
new file mode 100644
index 0000000..e4ef779
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/ElfClass.cs
@@ -0,0 +1,18 @@
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Specifies whether an ELF file is 32-bit or 64-bit.
+ ///
+ internal enum ElfClass : byte
+ {
+ ///
+ /// The file is 32-bit.
+ ///
+ Elf32 = 1,
+
+ ///
+ /// The file is 64-bit.
+ ///
+ Elf64 = 2
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/ElfFile.cs b/DebUOS/Packaging.Targets/Rpm/ElfFile.cs
new file mode 100644
index 0000000..84207a3
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/ElfFile.cs
@@ -0,0 +1,52 @@
+using System;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Provides methods for working with ELF files.
+ ///
+ internal static class ElfFile
+ {
+ ///
+ /// Determines whether a file is an ELF file or not.
+ ///
+ ///
+ /// An array containing at least the first 4 bytes of the file.
+ ///
+ ///
+ /// if the file is an ELF file; otherwise, .
+ ///
+ internal static bool IsElfFile(byte[] header)
+ {
+ return header.Length > 4 && header[0] == 0x7f && header[1] == 0x45 && header[2] == 0x4c && header[3] == 0x46;
+ }
+
+ ///
+ /// Reads the of an ELF file.
+ ///
+ ///
+ /// An array containing at least the first 0x14 bytes of the file.
+ ///
+ ///
+ /// A object represening the ELF file header.
+ ///
+ internal static ElfHeader ReadHeader(byte[] header)
+ {
+ if (!IsElfFile(header))
+ {
+ throw new InvalidOperationException();
+ }
+
+ ElfHeader value = default(ElfHeader);
+ value.@class = (ElfClass)header[4];
+ value.data = header[5];
+ value.version = header[6];
+ value.osAbi = header[7];
+ value.abiVersion = header[8];
+ value.type = (ElfType)BitConverter.ToInt16(header, 0x10);
+ value.machine = (ElfMachine)BitConverter.ToInt16(header, 0x12);
+
+ return value;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/ElfHeader.cs b/DebUOS/Packaging.Targets/Rpm/ElfHeader.cs
new file mode 100644
index 0000000..a5c9083
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/ElfHeader.cs
@@ -0,0 +1,46 @@
+namespace Packaging.Targets.Rpm
+{
+#pragma warning disable SA1307 // Accessible fields must begin with upper-case letter
+ ///
+ /// Represents the first fields of a header of an ELF file.
+ ///
+ ///
+ internal struct ElfHeader
+ {
+ ///
+ /// The . Indicates whether this is 64 or 32-bit.
+ ///
+ public ElfClass @class;
+
+ ///
+ /// Set to 1 or 2 to indicate the endianness of the file.
+ ///
+ public byte data;
+
+ ///
+ /// Should always be 1 to indicate the original ELF file format.
+ ///
+ public byte version;
+
+ ///
+ /// Identifies the target operating system ABI.
+ ///
+ public byte osAbi;
+
+ ///
+ /// Further specifies the ABI version. Its interpretation depends on the target ABI.
+ ///
+ public byte abiVersion;
+
+ ///
+ /// Indicates whether the object is relocatable, executable, shared, or core.
+ ///
+ public ElfType type;
+
+ ///
+ /// Specifies target instruction set architecture.
+ ///
+ public ElfMachine machine;
+ }
+#pragma warning restore SA1307 // Accessible fields must begin with upper-case letter
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/ElfMachine.cs b/DebUOS/Packaging.Targets/Rpm/ElfMachine.cs
new file mode 100644
index 0000000..7d9fe09
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/ElfMachine.cs
@@ -0,0 +1,65 @@
+namespace Packaging.Targets.Rpm
+{
+#pragma warning disable SA1300 // Element must begin with upper-case letter
+ ///
+ /// Specifies target instruction set architecture.
+ ///
+ internal enum ElfMachine
+ {
+ ///
+ /// The file is generic.
+ ///
+ Generic = 0,
+
+ ///
+ /// The file targets the SPARC architecture.
+ ///
+ SPARC = 2,
+
+ ///
+ /// The file targets the x64 architecture.
+ ///
+ x64 = 3,
+
+ ///
+ /// The file targets the MIPS architecture.
+ ///
+ MIPS = 8,
+
+ ///
+ /// The file targets the PowerPC architecture.
+ ///
+ PowerPC = 0x14,
+
+ ///
+ /// The file targets the ARM architecture.
+ ///
+ ARM = 0x28,
+
+ ///
+ /// The file targets the SuperH architecture.
+ ///
+ SuperH = 0x2A,
+
+ ///
+ /// THe file targets the IA-64 architecture.
+ ///
+ IA64 = 0x32,
+
+ ///
+ /// The file targets the x86-64 architecture.
+ ///
+ x8664 = 0x3E,
+
+ ///
+ /// The file targets the AArch64 (ARM64) architecture.
+ ///
+ AArch64 = 0xb7,
+
+ ///
+ /// The file targets the RISC V architecture.
+ ///
+ RiscV = 0xF3
+ }
+#pragma warning restore SA1300 // Element must begin with upper-case letter
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/ElfType.cs b/DebUOS/Packaging.Targets/Rpm/ElfType.cs
new file mode 100644
index 0000000..bc1fe5b
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/ElfType.cs
@@ -0,0 +1,28 @@
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Represents the different types of ELF files.
+ ///
+ internal enum ElfType : ushort
+ {
+ ///
+ /// The file is relocatable.
+ ///
+ Relocatable = 1,
+
+ ///
+ /// The file is executable.
+ ///
+ Executable = 2,
+
+ ///
+ /// The file is shared.
+ ///
+ Shared = 3,
+
+ ///
+ /// The file is core.
+ ///
+ Core = 4
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/FileAnalyzer.cs b/DebUOS/Packaging.Targets/Rpm/FileAnalyzer.cs
new file mode 100644
index 0000000..70f5f03
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/FileAnalyzer.cs
@@ -0,0 +1,131 @@
+using Packaging.Targets.IO;
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Provides a very basic implementation of the class. To get the details that are consistent
+ /// with those found in real-world RPM packages, more complete parsing of ELF files is required to get the full list of
+ /// dependencies, as well as the class (description).
+ ///
+ internal class FileAnalyzer : IFileAnalyzer
+ {
+ ///
+ public virtual Collection DetermineRequires(ArchiveEntry entry)
+ {
+ // For now, report no dependencies at all. Could be enhanced if ELF parsing is available.
+ var dependencies = new Collection();
+
+ return dependencies;
+ }
+
+ ///
+ public virtual Collection DetermineProvides(ArchiveEntry entry)
+ {
+ // For now, report no provides at all. Could be enhanced if ELF parsing is available.
+ var dependencies = new Collection();
+
+ return dependencies;
+ }
+
+ ///
+ public virtual RpmFileFlags DetermineFlags(ArchiveEntry entry)
+ {
+ // The only custom flags which are supported for now are the RPMFILE_DOC flags for non-executable
+ // files.
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFDIR))
+ {
+ return RpmFileFlags.None;
+ }
+ else if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK))
+ {
+ return RpmFileFlags.None;
+ }
+ else if (entry.TargetPath.StartsWith("/usr/share/doc"))
+ {
+ return RpmFileFlags.RPMFILE_DOC;
+ }
+ else
+ {
+ return RpmFileFlags.None;
+ }
+ }
+
+ ///
+ public virtual RpmFileColor DetermineColor(ArchiveEntry entry)
+ {
+ // Only support ELF32 and ELF64
+ switch (entry.Type)
+ {
+ case ArchiveEntryType.Executable32:
+ return RpmFileColor.RPMFC_ELF32;
+
+ case ArchiveEntryType.Executable64:
+ return RpmFileColor.RPMFC_ELF64;
+
+ default:
+ return RpmFileColor.RPMFC_BLACK;
+ }
+ }
+
+ ///
+ public virtual bool IsExecutable(ArchiveEntry entry)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ public virtual string DetermineClass(ArchiveEntry entry)
+ {
+ // Very simplistic implementation - non-executable files are considered to be tet files.
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFDIR))
+ {
+ return "directory";
+ }
+
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK))
+ {
+ return string.Empty;
+ }
+
+ if (entry.TargetPath.EndsWith(".svg"))
+ {
+ return "SVG Scalable Vector Graphics image";
+ }
+ else if (entry.TargetPath.EndsWith(".ttf"))
+ {
+ return "TrueType font data";
+ }
+ else if (entry.TargetPath.EndsWith(".woff"))
+ {
+ return string.Empty;
+ }
+ else if (entry.TargetPath.EndsWith(".woff2"))
+ {
+ return string.Empty;
+ }
+ else if (entry.TargetPath.EndsWith(".eot"))
+ {
+ return string.Empty;
+ }
+
+ if (!entry.Mode.HasFlag(LinuxFileMode.S_IXGRP)
+ && !entry.Mode.HasFlag(LinuxFileMode.S_IXOTH)
+ && !entry.Mode.HasFlag(LinuxFileMode.S_IXUSR))
+ {
+ if (entry.IsAscii)
+ {
+ return "ASCII text";
+ }
+ else
+ {
+ return "UTF-8 Unicode text";
+ }
+ }
+
+ return string.Empty;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/IFileAnalyzer.cs b/DebUOS/Packaging.Targets/Rpm/IFileAnalyzer.cs
new file mode 100644
index 0000000..170f2e5
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/IFileAnalyzer.cs
@@ -0,0 +1,68 @@
+using Packaging.Targets.IO;
+using System.Collections.ObjectModel;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Provides a common interface for classes which are able to analyze the contents of a file and provide metadata for that file.
+ ///
+ internal interface IFileAnalyzer
+ {
+ ///
+ /// Gets the which apply to this file.
+ ///
+ ///
+ /// The entry to analyze.
+ ///
+ /// The for the file.
+ ///
+ RpmFileFlags DetermineFlags(ArchiveEntry entry);
+
+ ///
+ /// Gets the dependencies for this file.
+ ///
+ ///
+ /// The entry to analyze.
+ ///
+ ///
+ /// The dependencies for the file.
+ ///
+ Collection DetermineRequires(ArchiveEntry entry);
+
+ ///
+ /// Gets the dependencies fulfilled by this file.
+ ///
+ ///
+ /// The entry to analyze.
+ ///
+ ///
+ /// The dependencies fulfilled by the file.
+ ///
+ Collection DetermineProvides(ArchiveEntry entry);
+
+ ///
+ /// Gets the for this file.
+ ///
+ ///
+ /// The entry to analyze.
+ /// tes of the file to analyze.
+ ///
+ ///
+ /// The for the file.
+ ///
+ RpmFileColor DetermineColor(ArchiveEntry entry);
+
+ ///
+ /// Gets the class of this file.
+ ///
+ ///
+ /// The entry to analyze.
+ ///
+ ///
+ /// The class of this file.
+ ///
+ string DetermineClass(ArchiveEntry entry);
+
+ bool IsExecutable(ArchiveEntry entry);
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/IPackageSigner.cs b/DebUOS/Packaging.Targets/Rpm/IPackageSigner.cs
new file mode 100644
index 0000000..08ee9df
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/IPackageSigner.cs
@@ -0,0 +1,13 @@
+using Org.BouncyCastle.Bcpg.OpenPgp;
+using System.IO;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Common interface for any class that can generate PGP signatures of a .
+ ///
+ internal interface IPackageSigner
+ {
+ PgpSignature Sign(Stream payload);
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/IndexHeader.cs b/DebUOS/Packaging.Targets/Rpm/IndexHeader.cs
new file mode 100644
index 0000000..b5d1dfd
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/IndexHeader.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Packaging.Targets.Rpm
+{
+ internal struct IndexHeader
+ {
+ ///
+ /// Value identifying the purpose of the data associated with this Index Record.
+ /// The value of this field is dependent on the context in which the Index Record is used.
+ ///
+ public uint Tag;
+
+ ///
+ /// Value identifying the type of the data associated with this Index Record.
+ ///
+ public IndexType Type;
+
+ ///
+ /// Location in the Store of the data associated with this Index Record. This value should between 0 and the value contained
+ /// in .
+ ///
+ public int Offset;
+
+ ///
+ /// Size of the data associated with this Index Record. The count is the number of elements whose size is defined by the type of this Record.
+ ///
+ public int Count;
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/IndexRecord.cs b/DebUOS/Packaging.Targets/Rpm/IndexRecord.cs
new file mode 100644
index 0000000..13c3f34
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/IndexRecord.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ internal class IndexRecord
+ {
+ public IndexHeader Header
+ {
+ get;
+ set;
+ }
+
+ public object Value
+ {
+ get;
+ set;
+ }
+
+ public override string ToString()
+ {
+ return $"{this.Value} ({this.Header.Type})";
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/IndexTag.cs b/DebUOS/Packaging.Targets/Rpm/IndexTag.cs
new file mode 100644
index 0000000..a4052cb
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/IndexTag.cs
@@ -0,0 +1,665 @@
+namespace Packaging.Targets.Rpm
+{
+ ///
+ internal enum IndexTag
+ {
+ ///
+ /// Unknown tag
+ ///
+ RPMTAG_NOT_FOUND = -1,
+
+ RPMTAG_HEADERIMAGE = 61,
+
+ ///
+ /// The signature tag differentiates a signature header from a metadata header, and identifies the original contents of the signature header.
+ ///
+ RPMTAG_HEADERSIGNATURES = 62,
+
+ ///
+ /// This tag contains an index record which specifies the portion of the Header Record which was used for the calculation of a signature.
+ /// This data shall be preserved or any header-only signature will be invalidated.
+ ///
+ RPMTAG_HEADERIMMUTABLE = 63,
+
+ RPMTAG_HEADERREGIONS = 64,
+
+ ///
+ /// Contains a list of locales for which strings are provided in other parts of the package.
+ ///
+ RPMTAG_HEADERI18NTABLE = 100,
+
+ // Header tags
+ ///
+ /// This tag specifies the name of the package.
+ ///
+ RPMTAG_NAME = 1000,
+
+ ///
+ /// This tag specifies the version of the package.
+ ///
+ RPMTAG_VERSION = 1001,
+
+ ///
+ /// This tag specifies the release of the package.
+ ///
+ RPMTAG_RELEASE = 1002,
+
+ RPMTAG_EPOCH = 1003,
+
+ ///
+ /// This tag specifies the summary description of the package. The summary value pointed to by this index record contains a one line description of the package.
+ ///
+ RPMTAG_SUMMARY = 1004,
+
+ ///
+ /// This tag specifies the description of the package. The description value pointed to by this index record contains a full desription of the package.
+ ///
+ RPMTAG_DESCRIPTION = 1005,
+
+ RPMTAG_BUILDTIME = 1006,
+
+ RPMTAG_BUILDHOST = 1007,
+
+ RPMTAG_INSTALLTIME = 1008,
+
+ ///
+ /// This tag specifies the sum of the sizes of the regular files in the archive.
+ ///
+ RPMTAG_SIZE = 1009,
+
+ ///
+ /// A string containing the name of the distribution on which the package was built.
+ ///
+ RPMTAG_DISTRIBUTION = 1010,
+
+ ///
+ /// A string containing the name of the organization that produced the package.
+ ///
+ RPMTAG_VENDOR = 1011,
+
+ RPMTAG_GIF = 1012,
+
+ RPMTAG_XPM = 1013,
+
+ ///
+ /// This tag specifies the license which applies to this package.
+ ///
+ RPMTAG_LICENSE = 1014,
+
+ ///
+ /// A string identifying the tool used to build the package.
+ ///
+ RPMTAG_PACKAGER = 1015,
+
+ ///
+ /// This tag specifies the administrative group to which this package belongs.
+ ///
+ RPMTAG_GROUP = 1016,
+
+ RPMTAG_CHANGELOG = 1017,
+
+ RPMTAG_SOURCE = 1018,
+
+ RPMTAG_PATCH = 1019,
+
+ ///
+ /// Generic package information URL.
+ ///
+ RPMTAG_URL = 1020,
+
+ ///
+ /// This tag specifies the OS of the package. The OS value pointed to by this index record shall be "linux".
+ ///
+ RPMTAG_OS = 1021,
+
+ ///
+ /// This tag specifies the architecture of the package. The architecture value pointed to by this index record is defined in architecture specific LSB specification.
+ ///
+ RPMTAG_ARCH = 1022,
+
+ ///
+ /// Scripts which run before installing the package.
+ ///
+ RPMTAG_PREIN = 1023,
+
+ ///
+ /// Scripts which run after installing the package.
+ ///
+ RPMTAG_POSTIN = 1024,
+
+ ///
+ /// Scripts which run before uninstalling the package.
+ ///
+ RPMTAG_PREUN = 1025,
+
+ ///
+ /// Scripts which run after uninstalling the package.
+ ///
+ RPMTAG_POSTUN = 1026,
+
+ ///
+ /// This tag specifies the filenames when not in a compressed format as determined by the absence of rpmlib(CompressedFileNames) in the RPMTAG_REQUIRENAME index.
+ ///
+ RPMTAG_OLDFILENAMES = 1027,
+
+ ///
+ /// This tag specifies the size of each file in the archive.
+ ///
+ RPMTAG_FILESIZES = 1028,
+
+ RPMTAG_FILESTATES = 1029,
+
+ ///
+ /// This tag specifies the mode of each file in the archive.
+ ///
+ RPMTAG_FILEMODES = 1030,
+
+ RPMTAG_FILEUIDS = 1031,
+
+ RPMTAG_FILEGIDS = 1032,
+
+ ///
+ /// This tag specifies the device number from which the file was copied.
+ ///
+ RPMTAG_FILERDEVS = 1033,
+
+ ///
+ /// This tag specifies the modification time in seconds since the epoch of each file in the archive.
+ ///
+ RPMTAG_FILEMTIMES = 1034,
+
+ ///
+ /// This tag specifies the ASCII representation of the MD5 sum of the corresponding file contents. This value is empty if the corresponding archive entry is not a regular file.
+ ///
+ ///
+ RPMTAG_FILEDIGESTS = 1035,
+
+ ///
+ /// The target for a symlink, otherwise NULL.
+ ///
+ RPMTAG_FILELINKTOS = 1036,
+
+ ///
+ /// This tag specifies the bit(s) to classify and control how files are to be installed. See below.
+ ///
+ RPMTAG_FILEFLAGS = 1037,
+
+ RPMTAG_ROOT = 1038,
+
+ ///
+ /// This tag specifies the owner of the corresponding file.
+ ///
+ RPMTAG_FILEUSERNAME = 1039,
+
+ ///
+ /// This tag specifies the group of the corresponding file.
+ ///
+ RPMTAG_FILEGROUPNAME = 1040,
+
+ RPMTAG_EXCLUDE = 1041,
+
+ RPMTAG_EXCLUSIVE = 1042,
+
+ RPMTAG_ICON = 1043,
+
+ ///
+ /// This tag specifies the name of the source RPM.
+ ///
+ RPMTAG_SOURCERPM = 1044,
+
+ ///
+ /// This tag specifies the bit(s) to control how files are to be verified after install, specifying which checks should be performed.
+ ///
+ RPMTAG_FILEVERIFYFLAGS = 1045,
+
+ ///
+ /// This tag specifies the uncompressed size of the Payload archive, including the cpio headers.
+ ///
+ RPMTAG_ARCHIVESIZE = 1046,
+
+ ///
+ /// This tag indicates the name of the dependency provided by this package.
+ ///
+ RPMTAG_PROVIDENAME = 1047,
+
+ ///
+ /// Bits(s) to specify the dependency range and context.
+ ///
+ RPMTAG_REQUIREFLAGS = 1048,
+
+ ///
+ /// This tag indicates the dependencies for this package.
+ ///
+ RPMTAG_REQUIRENAME = 1049,
+
+ ///
+ /// This tag indicates the versions associated with the values found in the RPMTAG_REQUIRENAME Index.
+ ///
+ RPMTAG_REQUIREVERSION = 1050,
+
+ RPMTAG_NOSOURCE = 1051,
+
+ RPMTAG_NOPATCH = 1052,
+
+ ///
+ /// Bits(s) to specify the conflict range and context.
+ ///
+ RPMTAG_CONFLICTFLAGS = 1053,
+
+ ///
+ /// This tag indicates the conflicting dependencies for this package.
+ ///
+ RPMTAG_CONFLICTNAME = 1054,
+
+ ///
+ /// This tag indicates the versions associated with the values found in the RPMTAG_CONFLICTNAME Index.
+ ///
+ RPMTAG_CONFLICTVERSION = 1055,
+
+ RPMTAG_DEFAULTPREFIX = 1056,
+
+ RPMTAG_BUILDROOT = 1057,
+
+ RPMTAG_INSTALLPREFIX = 1058,
+
+ RPMTAG_EXCLUDEARCH = 1059,
+
+ RPMTAG_EXCLUDEOS = 1060,
+
+ RPMTAG_EXCLUSIVEARCH = 1061,
+
+ RPMTAG_EXCLUSIVEOS = 1062,
+
+ RPMTAG_AUTOREQPROV = 1063,
+
+ ///
+ /// This tag indicates the version of RPM tool used to build this package. The value is unused.
+ ///
+ RPMTAG_RPMVERSION = 1064,
+
+ RPMTAG_TRIGGERSCRIPTS = 1065,
+
+ RPMTAG_TRIGGERNAME = 1066,
+
+ RPMTAG_TRIGGERVERSION = 1067,
+
+ RPMTAG_TRIGGERFLAGS = 1068,
+
+ RPMTAG_TRIGGERINDEX = 1069,
+
+ RPMTAG_VERIFYSCRIPT = 1079,
+
+ ///
+ /// This tag specifies the Unix time in seconds since the epoch associated with each entry in the Changelog file.
+ ///
+ RPMTAG_CHANGELOGTIME = 1080,
+
+ ///
+ /// This tag specifies the name of who made a change to this package.
+ ///
+ RPMTAG_CHANGELOGNAME = 1081,
+
+ ///
+ /// This tag specifies the changes asssociated with a changelog entry.
+ ///
+ RPMTAG_CHANGELOGTEXT = 1082,
+
+ RPMTAG_BROKENMD5 = 1083,
+
+ RPMTAG_PREREQ = 1084,
+
+ ///
+ /// The program which launches the pre-install script.
+ ///
+ RPMTAG_PREINPROG = 1085,
+
+ ///
+ /// The program which launches the post-install script.
+ ///
+ RPMTAG_POSTINPROG = 1086,
+
+ ///
+ /// The program which launches the pre-removal script.
+ ///
+ RPMTAG_PREUNPROG = 1087,
+
+ ///
+ /// The program which launches the post-removal script.
+ ///
+ RPMTAG_POSTUNPROG = 1088,
+
+ RPMTAG_BUILDARCHS = 1089,
+
+ ///
+ /// This tag indicates the obsoleted dependencies for this package.
+ ///
+ RPMTAG_OBSOLETENAME = 1090,
+
+ RPMTAG_VERIFYSCRIPTPROG = 1091,
+
+ RPMTAG_TRIGGERSCRIPTPROG = 1092,
+
+ RPMTAG_DOCDIR = 1093,
+
+ ///
+ /// This tag contains an opaque string whose contents are undefined.
+ ///
+ RPMTAG_COOKIE = 1094,
+
+ ///
+ /// This tag specifies the 16 bit device number from which the file was copied.
+ ///
+ RPMTAG_FILEDEVICES = 1095,
+
+ ///
+ /// This tag specifies the inode value from the original file system on the the system on which it was built.
+ ///
+ RPMTAG_FILEINODES = 1096,
+
+ ///
+ /// This tag specifies a per-file locale marker used to install only locale specific subsets of files when the package is installed.
+ ///
+ RPMTAG_FILELANGS = 1097,
+ RPMTAG_PREFIXES = 1098,
+ RPMTAG_INSTPREFIXES = 1099,
+ RPMTAG_TRIGGERIN = 1100,
+ RPMTAG_TRIGGERUN = 1101,
+ RPMTAG_TRIGGERPOSTUN = 1102,
+ RPMTAG_AUTOREQ = 1103,
+ RPMTAG_AUTOPROV = 1104,
+ RPMTAG_CAPABILITY = 1105,
+ RPMTAG_SOURCEPACKAGE = 1106,
+ RPMTAG_OLDORIGFILENAMES = 1107,
+ RPMTAG_BUILDPREREQ = 1108,
+ RPMTAG_BUILDREQUIRES = 1109,
+ RPMTAG_BUILDCONFLICTS = 1110,
+ RPMTAG_BUILDMACROS = 1111,
+
+ ///
+ /// Bits(s) to specify the conflict range and context.
+ ///
+ RPMTAG_PROVIDEFLAGS = 1112,
+
+ ///
+ /// This tag indicates the versions associated with the values found in the RPMTAG_PROVIDENAME Index.
+ ///
+ RPMTAG_PROVIDEVERSION = 1113,
+
+ ///
+ /// Bits(s) to specify the conflict range and context.
+ ///
+ RPMTAG_OBSOLETEFLAGS = 1114,
+
+ ///
+ /// This tag indicates the versions associated with the values found in the RPMTAG_OBSOLETENAME Index.
+ ///
+ RPMTAG_OBSOLETEVERSION = 1115,
+
+ ///
+ /// This tag specifies the index into the array provided by the RPMTAG_DIRNAMES Index which contains the directory name for the corresponding filename.
+ ///
+ RPMTAG_DIRINDEXES = 1116,
+
+ ///
+ /// This tag specifies the base portion of the corresponding filename.
+ ///
+ RPMTAG_BASENAMES = 1117,
+
+ ///
+ /// One of RPMTAG_OLDFILENAMES or the tuple RPMTAG_DIRINDEXES,RPMTAG_BASENAMES,RPMTAG_DIRNAMES shall be present, but not both.
+ ///
+ RPMTAG_DIRNAMES = 1118,
+ RPMTAG_ORIGDIRINDEXES = 1119,
+ RPMTAG_ORIGBASENAMES = 1120,
+ RPMTAG_ORIGDIRNAMES = 1121,
+
+ ///
+ /// This tag indicates additional flags which may have been passed to the compiler when building this package.
+ ///
+ RPMTAG_OPTFLAGS = 1122,
+
+ ///
+ /// URL for package.
+ ///
+ RPMTAG_DISTURL = 1123,
+
+ ///
+ /// This tag specifies the format of the Archive section. The format value pointed to by this index record shall be 'cpio'.
+ ///
+ RPMTAG_PAYLOADFORMAT = 1124,
+
+ ///
+ /// This tag specifies the compression used on the Archive section. The compression value pointed to by this index record shall be 'gzip'.
+ ///
+ RPMTAG_PAYLOADCOMPRESSOR = 1125,
+
+ ///
+ /// This tag indicates the compression level used for the Payload. This value shall always be '9'.
+ ///
+ RPMTAG_PAYLOADFLAGS = 1126,
+ RPMTAG_INSTALLCOLOR = 1127,
+ RPMTAG_INSTALLTID = 1128,
+ RPMTAG_REMOVETID = 1129,
+ RPMTAG_SHA1RHN = 1130,
+
+ ///
+ /// This tag contains an opaque string whose contents are undefined.
+ ///
+ RPMTAG_RHNPLATFORM = 1131,
+
+ ///
+ /// This tag contains an opaque string whose contents are undefined.
+ ///
+ RPMTAG_PLATFORM = 1132,
+
+ RPMTAG_PATCHESNAME = 1133,
+ RPMTAG_PATCHESFLAGS = 1134,
+ RPMTAG_PATCHESVERSION = 1135,
+ RPMTAG_CACHECTIME = 1136,
+ RPMTAG_CACHEPKGPATH = 1137,
+ RPMTAG_CACHEPKGSIZE = 1138,
+ RPMTAG_CACHEPKGMTIME = 1139,
+ RPMTAG_FILECOLORS = 1140,
+ RPMTAG_FILECLASS = 1141,
+ RPMTAG_CLASSDICT = 1142,
+ RPMTAG_FILEDEPENDSX = 1143,
+ RPMTAG_FILEDEPENDSN = 1144,
+ RPMTAG_DEPENDSDICT = 1145,
+ RPMTAG_SOURCEPKGID = 1146,
+ RPMTAG_FILECONTEXTS = 1147,
+ RPMTAG_FSCONTEXTS = 1148,
+ RPMTAG_RECONTEXTS = 1149,
+ RPMTAG_POLICIES = 1150,
+ RPMTAG_PRETRANS = 1151,
+ RPMTAG_POSTTRANS = 1152,
+ RPMTAG_PRETRANSPROG = 1153,
+ RPMTAG_POSTTRANSPROG = 1154,
+ RPMTAG_DISTTAG = 1155,
+ RPMTAG_OLDSUGGESTSNAME = 1156,
+ RPMTAG_OLDSUGGESTSVERSION = 1157,
+ RPMTAG_OLDSUGGESTSFLAGS = 1158,
+ RPMTAG_OLDENHANCESNAME = 1159,
+ RPMTAG_OLDENHANCESVERSION = 1160,
+ RPMTAG_OLDENHANCESFLAGS = 1161,
+ RPMTAG_PRIORITY = 1162,
+ RPMTAG_CVSID = 1163,
+ RPMTAG_BLINKPKGID = 1164,
+ RPMTAG_BLINKHDRID = 1165,
+ RPMTAG_BLINKNEVRA = 1166,
+ RPMTAG_FLINKPKGID = 1167,
+ RPMTAG_FLINKHDRID = 1168,
+ RPMTAG_FLINKNEVRA = 1169,
+ RPMTAG_PACKAGEORIGIN = 1170,
+ RPMTAG_TRIGGERPREIN = 1171,
+ RPMTAG_BUILDSUGGESTS = 1172,
+ RPMTAG_BUILDENHANCES = 1173,
+ RPMTAG_SCRIPTSTATES = 1174,
+ RPMTAG_SCRIPTMETRICS = 1175,
+ RPMTAG_BUILDCPUCLOCK = 1176,
+ RPMTAG_FILEDIGESTALGOS = 1177,
+ RPMTAG_VARIANTS = 1178,
+ RPMTAG_XMAJOR = 1179,
+ RPMTAG_XMINOR = 1180,
+ RPMTAG_REPOTAG = 1181,
+ RPMTAG_KEYWORDS = 1182,
+ RPMTAG_BUILDPLATFORMS = 1183,
+ RPMTAG_PACKAGECOLOR = 1184,
+ RPMTAG_PACKAGEPREFCOLOR = 1185,
+ RPMTAG_XATTRSDICT = 1186,
+ RPMTAG_FILEXATTRSX = 1187,
+ RPMTAG_DEPATTRSDICT = 1188,
+ RPMTAG_CONFLICTATTRSX = 1189,
+ RPMTAG_OBSOLETEATTRSX = 1190,
+ RPMTAG_PROVIDEATTRSX = 1191,
+ RPMTAG_REQUIREATTRSX = 1192,
+ RPMTAG_BUILDPROVIDES = 1193,
+ RPMTAG_BUILDOBSOLETES = 1194,
+ RPMTAG_DBINSTANCE = 1195,
+ RPMTAG_NVRA = 1196,
+
+ /* tags 1997-4999 reserved */
+ RPMTAG_FILENAMES = 5000,
+ RPMTAG_FILEPROVIDE = 5001,
+ RPMTAG_FILEREQUIRE = 5002,
+ RPMTAG_FSNAMES = 5003,
+ RPMTAG_FSSIZES = 5004,
+ RPMTAG_TRIGGERCONDS = 5005,
+ RPMTAG_TRIGGERTYPE = 5006,
+ RPMTAG_ORIGFILENAMES = 5007,
+ RPMTAG_LONGFILESIZES = 5008,
+ RPMTAG_LONGSIZE = 5009,
+ RPMTAG_FILECAPS = 5010,
+ RPMTAG_FILEDIGESTALGO = 5011,
+ RPMTAG_BUGURL = 5012,
+ RPMTAG_EVR = 5013,
+ RPMTAG_NVR = 5014,
+ RPMTAG_NEVR = 5015,
+ RPMTAG_NEVRA = 5016,
+ RPMTAG_HEADERCOLOR = 5017,
+ RPMTAG_VERBOSE = 5018,
+ RPMTAG_EPOCHNUM = 5019,
+ RPMTAG_PREINFLAGS = 5020,
+ RPMTAG_POSTINFLAGS = 5021,
+ RPMTAG_PREUNFLAGS = 5022,
+ RPMTAG_POSTUNFLAGS = 5023,
+ RPMTAG_PRETRANSFLAGS = 5024,
+ RPMTAG_POSTTRANSFLAGS = 5025,
+ RPMTAG_VERIFYSCRIPTFLAGS = 5026,
+ RPMTAG_TRIGGERSCRIPTFLAGS = 5027,
+ RPMTAG_COLLECTIONS = 5029,
+ RPMTAG_POLICYNAMES = 5030,
+ RPMTAG_POLICYTYPES = 5031,
+ RPMTAG_POLICYTYPESINDEXES = 5032,
+ RPMTAG_POLICYFLAGS = 5033,
+ RPMTAG_VCS = 5034,
+ RPMTAG_ORDERNAME = 5035,
+ RPMTAG_ORDERVERSION = 5036,
+ RPMTAG_ORDERFLAGS = 5037,
+ RPMTAG_MSSFMANIFEST = 5038,
+ RPMTAG_MSSFDOMAIN = 5039,
+ RPMTAG_INSTFILENAMES = 5040,
+ RPMTAG_REQUIRENEVRS = 5041,
+ RPMTAG_PROVIDENEVRS = 5042,
+ RPMTAG_OBSOLETENEVRS = 5043,
+ RPMTAG_CONFLICTNEVRS = 5044,
+ RPMTAG_FILENLINKS = 5045,
+ RPMTAG_RECOMMENDNAME = 5046,
+ RPMTAG_RECOMMENDVERSION = 5047,
+ RPMTAG_RECOMMENDFLAGS = 5048,
+ RPMTAG_SUGGESTNAME = 5049,
+ RPMTAG_SUGGESTVERSION = 5050,
+ RPMTAG_SUGGESTFLAGS = 5051,
+ RPMTAG_SUPPLEMENTNAME = 5052,
+ RPMTAG_SUPPLEMENTVERSION = 5053,
+ RPMTAG_SUPPLEMENTFLAGS = 5054,
+ RPMTAG_ENHANCENAME = 5055,
+ RPMTAG_ENHANCEVERSION = 5056,
+ RPMTAG_ENHANCEFLAGS = 5057,
+ RPMTAG_RECOMMENDNEVRS = 5058,
+ RPMTAG_SUGGESTNEVRS = 5059,
+ RPMTAG_SUPPLEMENTNEVRS = 5060,
+ RPMTAG_ENHANCENEVRS = 5061,
+ RPMTAG_ENCODING = 5062,
+ RPMTAG_FILETRIGGERIN = 5063,
+ RPMTAG_FILETRIGGERUN = 5064,
+ RPMTAG_FILETRIGGERPOSTUN = 5065,
+ RPMTAG_FILETRIGGERSCRIPTS = 5066,
+ RPMTAG_FILETRIGGERSCRIPTPROG = 5067,
+ RPMTAG_FILETRIGGERSCRIPTFLAGS = 5068,
+ RPMTAG_FILETRIGGERNAME = 5069,
+ RPMTAG_FILETRIGGERINDEX = 5070,
+ RPMTAG_FILETRIGGERVERSION = 5071,
+ RPMTAG_FILETRIGGERFLAGS = 5072,
+ RPMTAG_TRANSFILETRIGGERIN = 5073,
+ RPMTAG_TRANSFILETRIGGERUN = 5074,
+ RPMTAG_TRANSFILETRIGGERPOSTUN = 5075,
+ RPMTAG_TRANSFILETRIGGERSCRIPTS = 5076,
+ RPMTAG_TRANSFILETRIGGERSCRIPTPROG = 5077,
+ RPMTAG_TRANSFILETRIGGERSCRIPTFLAGS = 5078,
+ RPMTAG_TRANSFILETRIGGERNAME = 5079,
+ RPMTAG_TRANSFILETRIGGERINDEX = 5080,
+ RPMTAG_TRANSFILETRIGGERVERSION = 5081,
+ RPMTAG_TRANSFILETRIGGERFLAGS = 5082,
+ RPMTAG_REMOVEPATHPOSTFIXES = 5083,
+ RPMTAG_FILETRIGGERPRIORITIES = 5084,
+ RPMTAG_TRANSFILETRIGGERPRIORITIES = 5085,
+ RPMTAG_FILETRIGGERCONDS = 5086,
+ RPMTAG_FILETRIGGERTYPE = 5087,
+ RPMTAG_TRANSFILETRIGGERCONDS = 5088,
+ RPMTAG_TRANSFILETRIGGERTYPE = 5089,
+ RPMTAG_FILESIGNATURES = 5090,
+ RPMTAG_FILESIGNATURELENGTH = 5091,
+ RPMTAG_PAYLOADDIGEST = 5092,
+ RPMTAG_PAYLOADDIGESTALGO = 5093,
+
+ ///
+ /// The file is a configuration file, and an existing file should be saved during a package upgrade operation and not removed during a pakage removal operation.
+ ///
+ RPMFILE_CONFIG = 1 << 0,
+
+ ///
+ /// The file contains documentation.
+ ///
+ RPMFILE_DOC = 1 << 1,
+
+ ///
+ /// This value is reserved for future use; conforming packages may not use this flag.
+ ///
+ RPMFILE_DONOTUSE = 1 << 2,
+
+ ///
+ /// The file need not exist on the installed system.
+ ///
+ RPMFILE_MISSINGOK = 1 << 3,
+
+ ///
+ /// Similar to the RPMFILE_CONFIG, this flag indicates that during an upgrade operation the original file on the system should not be altered.
+ ///
+ RPMFILE_NOREPLACE = 1 << 4,
+
+ ///
+ /// The file is a package specification.
+ ///
+ RPMFILE_SPECFILE = 1 << 5,
+
+ ///
+ /// The file is not actually included in the payload, but should still be considered as a part of the package. For example, a log file generated by the application at run time.
+ ///
+ RPMFILE_GHOST = 1 << 6,
+
+ ///
+ /// The file contains the license conditions.
+ ///
+ RPMFILE_LICENSE = 1 << 7,
+
+ ///
+ /// The file contains high level notes about the package.
+ ///
+ RPMFILE_README = 1 << 8,
+
+ ///
+ /// The corresponding file is not a part of the package, and should not be installed.
+ ///
+ RPMFILE_EXCLUDE = 1 << 9,
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/IndexType.cs b/DebUOS/Packaging.Targets/Rpm/IndexType.cs
new file mode 100644
index 0000000..0280214
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/IndexType.cs
@@ -0,0 +1,16 @@
+namespace Packaging.Targets.Rpm
+{
+ internal enum IndexType : uint
+ {
+ RPM_NULL_TYPE = 0,
+ RPM_CHAR_TYPE = 1,
+ RPM_INT8_TYPE = 2,
+ RPM_INT16_TYPE = 3,
+ RPM_INT32_TYPE = 4,
+ RPM_INT64_TYPE = 5,
+ RPM_STRING_TYPE = 6,
+ RPM_BIN_TYPE = 7,
+ RPM_STRING_ARRAY_TYPE = 8,
+ RPM_I18NSTRING_TYPE = 9
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/PackageDependency.cs b/DebUOS/Packaging.Targets/Rpm/PackageDependency.cs
new file mode 100644
index 0000000..a05d90d
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/PackageDependency.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Represents a dependency on another package, or, a dependency on a specific RPM feature.
+ ///
+ internal class PackageDependency
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PackageDependency()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The name of the dependency.
+ ///
+ ///
+ /// The dependency flags.
+ ///
+ ///
+ /// The dependency version.
+ ///
+ public PackageDependency(string name, RpmSense flags, string version)
+ {
+ this.Name = name;
+ this.Flags = flags;
+ this.Version = version;
+ }
+
+ ///
+ /// Gets or sets a value indicating the dependency constraints.
+ ///
+ public RpmSense Flags
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the name of the package which is a dependency.
+ ///
+ public string Name
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the version of the package on which a dependency is taken.
+ ///
+ public string Version
+ {
+ get;
+ set;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ var other = obj as PackageDependency;
+
+ if (other == null)
+ {
+ return false;
+ }
+
+ return string.Equals(this.Name, other.Name, StringComparison.Ordinal)
+ && this.Flags == other.Flags
+ && string.Equals(this.Version, other.Version, StringComparison.Ordinal);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return 13 * this.Name.GetHashCode() + 7 * (int)this.Flags + 7 * this.Version.GetHashCode();
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"{this.Name} {this.Flags} {this.Version}";
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/PackageSigner.cs b/DebUOS/Packaging.Targets/Rpm/PackageSigner.cs
new file mode 100644
index 0000000..21d7dde
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/PackageSigner.cs
@@ -0,0 +1,30 @@
+using Org.BouncyCastle.Bcpg.OpenPgp;
+using System;
+using System.IO;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Generates values using a and a
+ /// .
+ ///
+ internal class PackageSigner : IPackageSigner
+ {
+ private readonly PgpPrivateKey privateKey;
+
+ public PackageSigner(PgpPrivateKey privateKey)
+ {
+ if (privateKey == null)
+ {
+ throw new ArgumentNullException(nameof(privateKey));
+ }
+
+ this.privateKey = privateKey;
+ }
+
+ public PgpSignature Sign(Stream payload)
+ {
+ return PgpSigner.Sign(this.privateKey, payload);
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/PgpHashAlgo.cs b/DebUOS/Packaging.Targets/Rpm/PgpHashAlgo.cs
new file mode 100644
index 0000000..27d8ee6
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/PgpHashAlgo.cs
@@ -0,0 +1,37 @@
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Represents the various hashing algorithms.
+ ///
+ internal enum PgpHashAlgo
+ {
+ ///
+ /// The MD5 hashing algorithm.
+ ///
+ PGPHASHALGO_MD5 = 1,
+
+ ///
+ /// The SHA1 hashing algorithm.
+ ///
+ PGPHASHALGO_SHA1 = 2,
+ PGPHASHALGO_RIPEMD160 = 3,
+ PGPHASHALGO_MD2 = 5,
+ PGPHASHALGO_TIGER192 = 6,
+ PGPHASHALGO_HAVAL_5_160 = 7,
+
+ ///
+ /// The SHA 256 hashing algorithm.
+ ///
+ PGPHASHALGO_SHA256 = 8,
+
+ ///
+ /// The SHA 384 hashing algorithm.
+ ///
+ PGPHASHALGO_SHA384 = 9,
+
+ ///
+ /// The SHA 512 hashing algorithm.
+ ///
+ PGPHASHALGO_SHA512 = 10,
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/PgpSigner.cs b/DebUOS/Packaging.Targets/Rpm/PgpSigner.cs
new file mode 100644
index 0000000..a29fa4d
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/PgpSigner.cs
@@ -0,0 +1,177 @@
+using Org.BouncyCastle.Bcpg;
+using Org.BouncyCastle.Bcpg.OpenPgp;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Math;
+using Org.BouncyCastle.Security;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Provides helper methods for working with PGP signatures.
+ ///
+ internal class PgpSigner
+ {
+ ///
+ /// Verifies a PGP signature.
+ ///
+ ///
+ /// The PGP signature to verify.
+ ///
+ ///
+ /// A which contains the PGP public key.
+ ///
+ ///
+ /// The payload for which the signature was generated.
+ ///
+ ///
+ /// if the signature is valid; otherwise, .
+ ///
+ public static bool VerifySignature(PgpSignature signature, Stream publicKey, Stream payload)
+ {
+ PgpPublicKeyRingBundle keyRing = new PgpPublicKeyRingBundle(PgpUtilities.GetDecoderStream(publicKey));
+ PgpPublicKey key = keyRing.GetPublicKey(signature.KeyId);
+
+ return VerifySignature(signature, key, payload);
+ }
+
+ ///
+ /// Verifies a PGP signature.
+ ///
+ ///
+ /// The PGP signature to verify.
+ ///
+ ///
+ /// The public key of the signer.
+ ///
+ ///
+ /// The payload for which the signature was generated.
+ ///
+ ///
+ /// if the signature is valid; otherwise, .
+ ///
+ public static bool VerifySignature(PgpSignature signature, PgpPublicKey key, Stream payload)
+ {
+ signature.InitVerify(key);
+
+ byte[] buffer = new byte[1024];
+
+ int read;
+ while ((read = payload.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ signature.Update(buffer, 0, read);
+ }
+
+ return signature.Verify();
+ }
+
+ ///
+ /// Generates a as a digital signature for a certain payload.
+ ///
+ ///
+ /// The key with which to sign the data.
+ ///
+ ///
+ /// The payload to sign.
+ ///
+ ///
+ /// A which represents the signature.
+ ///
+ public static PgpSignature Sign(PgpPrivateKey key, Stream payload)
+ {
+ PgpV3SignatureGenerator signer = new PgpV3SignatureGenerator(PublicKeyAlgorithmTag.RsaGeneral, HashAlgorithmTag.Sha256);
+ signer.InitSign(0, key);
+
+ byte[] buffer = new byte[1024];
+
+ int read;
+ while ((read = payload.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ signer.Update(buffer, 0, read);
+ }
+
+ return signer.Generate();
+ }
+
+ ///
+ /// Generates a
+ ///
+ ///
+ /// The name of the identity.
+ ///
+ ///
+ /// The passphrase used to protect the keyring.
+ ///
+ /// A .
+ ///
+ public static PgpKeyRingGenerator GenerateKeyRingGenerator(string identity, string password)
+ {
+ var rsaParams = new RsaKeyGenerationParameters(BigInteger.ValueOf(0x10001), new SecureRandom(), 2048, 12);
+ var symmetricAlgorithms = new SymmetricKeyAlgorithmTag[]
+ {
+ SymmetricKeyAlgorithmTag.Aes256,
+ SymmetricKeyAlgorithmTag.Aes192,
+ SymmetricKeyAlgorithmTag.Aes128
+ }.Select(a => (int)a).ToArray();
+
+ var hashAlgorithms = new HashAlgorithmTag[]
+ {
+ HashAlgorithmTag.Sha256,
+ HashAlgorithmTag.Sha1,
+ HashAlgorithmTag.Sha384,
+ HashAlgorithmTag.Sha512,
+ HashAlgorithmTag.Sha224,
+ }.Select(a => (int)a).ToArray();
+
+ IAsymmetricCipherKeyPairGenerator generator = GeneratorUtilities.GetKeyPairGenerator("RSA");
+ generator.Init(rsaParams);
+
+ // Create the master (signing-only) key.
+ PgpKeyPair masterKeyPair = new PgpKeyPair(
+ PublicKeyAlgorithmTag.RsaSign,
+ generator.GenerateKeyPair(),
+ DateTime.UtcNow);
+
+ PgpSignatureSubpacketGenerator masterSubpckGen
+ = new PgpSignatureSubpacketGenerator();
+ masterSubpckGen.SetKeyFlags(false, PgpKeyFlags.CanSign
+ | PgpKeyFlags.CanCertify);
+ masterSubpckGen.SetPreferredSymmetricAlgorithms(false, symmetricAlgorithms);
+ masterSubpckGen.SetPreferredHashAlgorithms(false, hashAlgorithms);
+
+ // Create a signing and encryption key for daily use.
+ PgpKeyPair encKeyPair = new PgpKeyPair(
+ PublicKeyAlgorithmTag.RsaGeneral,
+ generator.GenerateKeyPair(),
+ DateTime.UtcNow);
+
+ PgpSignatureSubpacketGenerator encSubpckGen = new PgpSignatureSubpacketGenerator();
+ encSubpckGen.SetKeyFlags(false, PgpKeyFlags.CanEncryptCommunications | PgpKeyFlags.CanEncryptStorage);
+
+ masterSubpckGen.SetPreferredSymmetricAlgorithms(false, symmetricAlgorithms);
+ masterSubpckGen.SetPreferredHashAlgorithms(false, hashAlgorithms);
+
+ // Create the key ring.
+ PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator(
+ PgpSignature.DefaultCertification,
+ masterKeyPair,
+ identity,
+ SymmetricKeyAlgorithmTag.Aes128,
+ password.ToCharArray(),
+ true,
+ masterSubpckGen.Generate(),
+ null,
+ new SecureRandom());
+
+ // Add encryption subkey.
+ keyRingGen.AddSubKey(encKeyPair, encSubpckGen.Generate(), null);
+
+ return keyRingGen;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmDumper.cs b/DebUOS/Packaging.Targets/Rpm/RpmDumper.cs
new file mode 100644
index 0000000..7189a0f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmDumper.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Dumps the contents of the headers of a RPM package to a text file. Useful when comparing two
+ /// RPM packages.
+ ///
+ internal static class RpmDumper
+ {
+ public static void Dump(RpmPackage package, string path)
+ {
+ using (var file = File.Open(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ using (StreamWriter writer = new StreamWriter(file))
+ {
+ Dump(package, writer);
+ }
+ }
+
+ public static void Dump(RpmPackage package, StreamWriter writer)
+ {
+ writer.WriteLine("Lead:");
+ writer.WriteLine(" ArchNum {0}", package.Lead.ArchNum);
+ writer.WriteLine(" Magic {0}", package.Lead.Magic);
+ writer.WriteLine(" Major {0}", package.Lead.Major);
+ writer.WriteLine(" Minor {0}", package.Lead.Minor);
+ writer.WriteLine(" Name {0}", package.Lead.Name);
+ writer.WriteLine(" OsNum {0}", package.Lead.OsNum);
+ writer.WriteLine(" SignatureType {0}", package.Lead.SignatureType);
+ writer.WriteLine(" Type {0}", package.Lead.Type);
+
+ writer.WriteLine();
+
+ writer.WriteLine("Signature:");
+ Dump(package.Signature, writer);
+
+ writer.WriteLine("Header:");
+ Dump(package.Header, writer);
+ }
+
+ public static void Dump(Section section, StreamWriter writer)
+ {
+ foreach (var record in section.Records)
+ {
+ writer.WriteLine(" {0}:", record.Key);
+ writer.WriteLine(" Count: {0}", record.Value.Header.Count);
+ writer.WriteLine(" Offset: {0}", record.Value.Header.Offset);
+ writer.WriteLine(" Tag: {0}", record.Value.Header.Tag);
+ writer.WriteLine(" Type: {0}", record.Value.Header.Type);
+
+ if (record.Value.Value is IEnumerable)
+ {
+ writer.WriteLine(" Value: {0}", BitConverter.ToString((byte[])record.Value.Value).Replace("-", string.Empty));
+ }
+ else if (record.Value.Value is IEnumerable)
+ {
+ writer.Write(" Value: ");
+ bool isFirst = true;
+
+ foreach (var value in (IEnumerable)record.Value.Value)
+ {
+ if (isFirst)
+ {
+ isFirst = false;
+ }
+ else
+ {
+ writer.Write(", ");
+ }
+
+ writer.Write(value);
+ }
+
+ writer.WriteLine();
+ }
+ else
+ {
+ writer.WriteLine(" Value: {0}", record.Value.Value);
+ }
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmFile.cs b/DebUOS/Packaging.Targets/Rpm/RpmFile.cs
new file mode 100644
index 0000000..0a7704b
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmFile.cs
@@ -0,0 +1,182 @@
+using Packaging.Targets.IO;
+using System;
+using System.Collections.ObjectModel;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Represents a file in an RPM package.
+ ///
+ internal class RpmFile
+ {
+ ///
+ /// Gets or sets the size of the file.
+ ///
+ public int Size
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the file mode.
+ ///
+ public LinuxFileMode Mode
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the device ID of the file, if the file is a special file.
+ ///
+ public short Rdev
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the time at which the file was last modified.
+ ///
+ public DateTimeOffset ModifiedTime
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the MD5 hash of the file.
+ ///
+ public byte[] MD5Hash
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the name of the file to which this file links.
+ ///
+ public string LinkTo
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets information which describes this file.
+ ///
+ public RpmFileFlags Flags
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the user name of the owner of the file.
+ ///
+ public string UserName
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the group name of the owner of the file.
+ ///
+ public string GroupName
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets a value indicating how the file should be validated.
+ ///
+ public RpmVerifyFlags VerifyFlags
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the ID of device containing file.
+ ///
+ public int Device
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the inode number of the file.
+ ///
+ public int Inode
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets a per-file locale marker used to install only locale specific subsets of files when the package is installed.
+ ///
+ public string Lang
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or set sthe file color, a classification of file types.
+ ///
+ public RpmFileColor Color
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the class of the file. This is usually the output of the file command for this file.
+ ///
+ public string Class
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets a list of all dependencies of this file. If the file is an ELF file, the dependencies
+ /// are the ELF dependencies.
+ ///
+ public Collection Requires
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets a list of all dependencies fulfilled by this file. If the file is an ELF file,
+ /// these are the ELF provides.
+ ///
+ public Collection Provides
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the name of the file.
+ ///
+ public string Name
+ {
+ get;
+ set;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return this.Name;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmFileColor.cs b/DebUOS/Packaging.Targets/Rpm/RpmFileColor.cs
new file mode 100644
index 0000000..f81dbdf
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmFileColor.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Determines the type of the file.
+ ///
+ [Flags]
+ internal enum RpmFileColor
+ {
+ RPMFC_BLACK = 0,
+ RPMFC_ELF32 = 1 << 0,
+ RPMFC_ELF64 = 1 << 1,
+ RPMFC_ELFMIPSN32 = 1 << 2,
+ RPMFC_PKGCONFIG = 1 << 4,
+ RPMFC_LIBTOOL = 1 << 5,
+ RPMFC_BOURNE = 1 << 6,
+ RPMFC_MODULE = 1 << 7,
+ RPMFC_EXECUTABLE = 1 << 8,
+ RPMFC_SCRIPT = 1 << 9,
+ RPMFC_TEXT = 1 << 10,
+ RPMFC_DATA = 1 << 11,
+ RPMFC_DOCUMENT = 1 << 12,
+ RPMFC_STATIC = 1 << 13,
+ RPMFC_NOTSTRIPPED = 1 << 14,
+ RPMFC_COMPRESSED = 1 << 15,
+ RPMFC_DIRECTORY = 1 << 16,
+ RPMFC_SYMLINK = 1 << 17,
+ RPMFC_DEVICE = 1 << 18,
+ RPMFC_LIBRARY = 1 << 19,
+ RPMFC_ARCHIVE = 1 << 20,
+ RPMFC_FONT = 1 << 21,
+ RPMFC_IMAGE = 1 << 22,
+ RPMFC_MANPAGE = 1 << 23,
+ RPMFC_PERL = 1 << 24,
+ RPMFC_JAVA = 1 << 25,
+ RPMFC_PYTHON = 1 << 26,
+ RPMFC_PHP = 1 << 27,
+ RPMFC_TCL = 1 << 28,
+ RPMFC_WHITE = 1 << 29,
+ RPMFC_INCLUDE = 1 << 30,
+ RPMFC_ERROR = 1 << 31
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmFileFlags.cs b/DebUOS/Packaging.Targets/Rpm/RpmFileFlags.cs
new file mode 100644
index 0000000..71f8a3b
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmFileFlags.cs
@@ -0,0 +1,20 @@
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Determines the type of a file.
+ ///
+ internal enum RpmFileFlags
+ {
+ None = 0,
+ RPMFILE_CONFIG = 1 << 0,
+ RPMFILE_DOC = 1 << 1,
+ RPMFILE_DONOTUSE = 1 << 2,
+ RPMFILE_MISSINGOK = 1 << 3,
+ RPMFILE_NOREPLACE = 1 << 4,
+ RPMFILE_SPECFILE = 1 << 5,
+ RPMFILE_GHOST = 1 << 6,
+ RPMFILE_LICENSE = 1 << 7,
+ RPMFILE_README = 1 << 8,
+ RPMFILE_EXCLUDE = 1 << 9
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmHeader.cs b/DebUOS/Packaging.Targets/Rpm/RpmHeader.cs
new file mode 100644
index 0000000..3ef3cd2
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmHeader.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ internal struct RpmHeader
+ {
+ ///
+ /// Value identifying this record as an RPM header record. This value shall be "\216\255\350\001".
+ ///
+ public uint Magic;
+
+ ///
+ /// Reserved space. This value shall be "\000\000\000\000".
+ ///
+ public uint Reserved;
+
+ ///
+ /// The number of Index Records that follow this Header Record. There should be at least 1 Index Record.
+ ///
+ public uint IndexCount;
+
+ ///
+ /// The size in bytes of the storage area for the data pointed to by the Index Records.
+ ///
+ public uint HeaderSize;
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmLead.cs b/DebUOS/Packaging.Targets/Rpm/RpmLead.cs
new file mode 100644
index 0000000..4b204aa
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmLead.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// The lead section is used to identify the package file.
+ ///
+ ///
+ internal struct RpmLead
+ {
+ ///
+ /// Value identifying this file as an RPM format file. This value shall be "\355\253\356\333".
+ ///
+ public uint Magic;
+
+ ///
+ /// Value indicating the major version number of the file format version. This value shall be 3.
+ ///
+ public byte Major;
+
+ ///
+ /// Value indicating the minor revision number of file format version. This value shall be 0.
+ ///
+ public byte Minor;
+
+ ///
+ /// Value indicating whether this is a source or binary package. This value shall be 0 to indicate a binary package.
+ ///
+ public ushort Type;
+
+ ///
+ /// Value indicating the architecture for which this package is valid. This value is specified in the relevant architecture
+ /// specific part of ISO/IEC 23360.
+ ///
+ public ushort ArchNum;
+
+ ///
+ /// A NUL terminated string that provides the package name. This name shall conform with the Package Naming section of this specification.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 66)]
+ public byte[] NameBytes;
+
+ ///
+ /// Value indicating the Operating System for which this package is valid. This value shall be 1.
+ ///
+ public ushort OsNum;
+
+ ///
+ /// Value indicating the type of the signature used in the Signature part of the file. This value shall be 5.
+ ///
+ public ushort SignatureType;
+
+ ///
+ /// Reserved space. The value is undefined.
+ ///
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
+ public byte[] Reserved;
+
+ public string Name
+ {
+ get
+ {
+ var length = Array.FindIndex(this.NameBytes, 0, (x) => x == 0);
+ return Encoding.ASCII.GetString(this.NameBytes, 0, length);
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmMetadata.cs b/DebUOS/Packaging.Targets/Rpm/RpmMetadata.cs
new file mode 100644
index 0000000..52ebf98
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmMetadata.cs
@@ -0,0 +1,1019 @@
+using Packaging.Targets.IO;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Provides access to the metadata stored in the RPM package.
+ ///
+ internal class RpmMetadata
+ {
+ public RpmMetadata(RpmPackage package)
+ {
+ if (package == null)
+ {
+ throw new ArgumentNullException(nameof(package));
+ }
+
+ this.Package = package;
+ }
+
+ public RpmPackage Package
+ {
+ get;
+ private set;
+ }
+
+ ///
+ /// Gets or sets the length of the immutable region, starting from the position of the
+ /// record. This record should be the last record in the signature block, and the value is negative, indicating the previous
+ /// blocks are considered immutable.
+ ///
+ public int ImmutableRegionSize
+ {
+ get
+ {
+ // For more information about immutable regions, see:
+ // http://ftp.rpm.org/api/4.4.2.2/hregions.html
+ // https://dentrassi.de/2016/04/15/writing-rpm-files-in-plain-java/
+ // https://blog.bethselamin.de/posts/argh-pm.html
+ // For now, we're always assuming the entire header and the entire signature is immutable
+ var immutableSignatureRegion = this.GetByteArray(IndexTag.RPMTAG_HEADERIMMUTABLE);
+
+ using (MemoryStream s = new MemoryStream(immutableSignatureRegion))
+ {
+ var h = s.ReadStruct();
+ return h.Offset;
+ }
+ }
+
+ set
+ {
+ IndexHeader header = default(IndexHeader);
+ header.Offset = value;
+ header.Count = 0x10;
+ header.Type = IndexType.RPM_BIN_TYPE;
+ header.Tag = (uint)IndexTag.RPMTAG_HEADERIMMUTABLE;
+
+ byte[] data;
+
+ using (MemoryStream s = new MemoryStream())
+ {
+ s.WriteStruct(header);
+ data = s.ToArray();
+ }
+
+ this.SetByteArray(IndexTag.RPMTAG_HEADERIMMUTABLE, data);
+ }
+ }
+
+ ///
+ /// Gets or sets a list of all locales supported by this package. The default, invariant locale is marked as the C locale.
+ ///
+ public Collection Locales
+ {
+ get { return this.GetStringArray(IndexTag.RPMTAG_HEADERI18NTABLE); }
+ set { this.SetStringArray(IndexTag.RPMTAG_HEADERI18NTABLE, value.ToArray()); }
+ }
+
+ ///
+ /// Gets or sets the name of the package.
+ ///
+ public string Name
+ {
+ get { return this.GetString(IndexTag.RPMTAG_NAME); }
+ set { this.SetString(IndexTag.RPMTAG_NAME, value); }
+ }
+
+ ///
+ /// Gets or sets the package version.
+ ///
+ public string Version
+ {
+ get { return this.GetString(IndexTag.RPMTAG_VERSION); }
+ set { this.SetString(IndexTag.RPMTAG_VERSION, value); }
+ }
+
+ ///
+ /// Gets or sets the release number of the package.
+ ///
+ public string Release
+ {
+ get { return this.GetString(IndexTag.RPMTAG_RELEASE); }
+ set { this.SetString(IndexTag.RPMTAG_RELEASE, value); }
+ }
+
+ ///
+ /// Gets or sets the a summary (one line) description of the package.
+ ///
+ public string Summary
+ {
+ get { return this.GetLocalizedString(IndexTag.RPMTAG_SUMMARY); }
+ set { this.SetLocalizedString(IndexTag.RPMTAG_SUMMARY, value); }
+ }
+
+ ///
+ /// Gets or sets the full description of the package.
+ ///
+ public string Description
+ {
+ get { return this.GetLocalizedString(IndexTag.RPMTAG_DESCRIPTION); }
+ set { this.SetLocalizedString(IndexTag.RPMTAG_DESCRIPTION, value); }
+ }
+
+ ///
+ /// Gets or sets the date and time at which the package was built.
+ ///
+ public DateTimeOffset BuildTime
+ {
+ get { return DateTimeOffset.FromUnixTimeSeconds(this.GetInt(IndexTag.RPMTAG_BUILDTIME)); }
+ set { this.SetInt(IndexTag.RPMTAG_BUILDTIME, (int)value.ToUnixTimeSeconds()); }
+ }
+
+ ///
+ /// Gets or sets the name of the host on which the package was built.
+ ///
+ public string BuildHost
+ {
+ get { return this.GetString(IndexTag.RPMTAG_BUILDHOST); }
+ set { this.SetString(IndexTag.RPMTAG_BUILDHOST, value); }
+ }
+
+ ///
+ /// Gets or sets the sum of the sizes of the regular files in the package.
+ ///
+ public int Size
+ {
+ get { return this.GetInt(IndexTag.RPMTAG_SIZE); }
+ set { this.SetInt(IndexTag.RPMTAG_SIZE, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the distribution for which the package was built.
+ ///
+ public string Distribution
+ {
+ get { return this.GetString(IndexTag.RPMTAG_DISTRIBUTION); }
+ set { this.SetString(IndexTag.RPMTAG_DISTRIBUTION, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the organization which produced the package.
+ ///
+ public string Vendor
+ {
+ get { return this.GetString(IndexTag.RPMTAG_VENDOR); }
+ set { this.SetString(IndexTag.RPMTAG_VENDOR, value); }
+ }
+
+ ///
+ /// Gets or sets the license which applies to this package.
+ ///
+ public string License
+ {
+ get { return this.GetString(IndexTag.RPMTAG_LICENSE); }
+ set { this.SetString(IndexTag.RPMTAG_LICENSE, value); }
+ }
+
+ ///
+ /// Gets or sets the administrative group to which this package belongs.
+ ///
+ public string Group
+ {
+ get { return this.GetLocalizedString(IndexTag.RPMTAG_GROUP); }
+ set { this.SetLocalizedString(IndexTag.RPMTAG_GROUP, value); }
+ }
+
+ ///
+ /// Gets or sets the generic package information URL.
+ ///
+ public string Url
+ {
+ get { return this.GetString(IndexTag.RPMTAG_URL); }
+ set { this.SetString(IndexTag.RPMTAG_URL, value); }
+ }
+
+ ///
+ /// Gets or sets the generic name of the OS for which this package was built. Should be linux.
+ ///
+ public string Os
+ {
+ get { return this.GetString(IndexTag.RPMTAG_OS); }
+ set { this.SetString(IndexTag.RPMTAG_OS, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the architecture for which the package was built, as defined in the architecture-specific
+ /// LSB specifications.
+ ///
+ public string Arch
+ {
+ get { return this.GetString(IndexTag.RPMTAG_ARCH); }
+ set { this.SetString(IndexTag.RPMTAG_ARCH, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the source RPM.
+ ///
+ public string SourceRpm
+ {
+ get { return this.GetString(IndexTag.RPMTAG_SOURCERPM); }
+ set { this.SetString(IndexTag.RPMTAG_SOURCERPM, value); }
+ }
+
+ ///
+ /// Gets or sets the version of the RPM tool used to build this package.
+ ///
+ public string RpmVersion
+ {
+ get { return this.GetString(IndexTag.RPMTAG_RPMVERSION); }
+ set { this.SetString(IndexTag.RPMTAG_RPMVERSION, value); }
+ }
+
+ ///
+ /// Gets or sets the scipt to run before installation of this package.
+ ///
+ public string PreIn
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PREIN); }
+ set { this.SetString(IndexTag.RPMTAG_PREIN, value); }
+ }
+
+ ///
+ /// Gets or sets the scipt to run after installation of this package.
+ ///
+ public string PostIn
+ {
+ get { return this.GetString(IndexTag.RPMTAG_POSTIN); }
+ set { this.SetString(IndexTag.RPMTAG_POSTIN, value); }
+ }
+
+ ///
+ /// Gets or sets the scipt to run before removal of this package.
+ ///
+ public string PreUn
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PREUN); }
+ set { this.SetString(IndexTag.RPMTAG_PREUN, value); }
+ }
+
+ ///
+ /// Gets or sets the scipt to run after removal of this package.
+ ///
+ public string PostUn
+ {
+ get { return this.GetString(IndexTag.RPMTAG_POSTUN); }
+ set { this.SetString(IndexTag.RPMTAG_POSTUN, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the program to run before installation of this package.
+ ///
+ public string PreInProg
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PREINPROG); }
+ set { this.SetString(IndexTag.RPMTAG_PREINPROG, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the program to run after installation of this package.
+ ///
+ public string PostInProg
+ {
+ get { return this.GetString(IndexTag.RPMTAG_POSTINPROG); }
+ set { this.SetString(IndexTag.RPMTAG_POSTINPROG, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the program to run before removal of this package.
+ ///
+ public string PreUnProg
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PREUNPROG); }
+ set { this.SetString(IndexTag.RPMTAG_PREUNPROG, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the program to run after removal of this package.
+ ///
+ public string PostUnProg
+ {
+ get { return this.GetString(IndexTag.RPMTAG_POSTUNPROG); }
+ set { this.SetString(IndexTag.RPMTAG_POSTUNPROG, value); }
+ }
+
+ ///
+ /// Gets or sets an opaque string whose contents are undefined.
+ ///
+ public string Cookie
+ {
+ get { return this.GetString(IndexTag.RPMTAG_COOKIE); }
+ set { this.SetString(IndexTag.RPMTAG_COOKIE, value); }
+ }
+
+ ///
+ /// Gets or sets additional flags which may have been passed to the compiler when building this package.
+ ///
+ public string OptFlags
+ {
+ get { return this.GetString(IndexTag.RPMTAG_OPTFLAGS); }
+ set { this.SetString(IndexTag.RPMTAG_OPTFLAGS, value); }
+ }
+
+ ///
+ /// Gets or sets the URL for the package.
+ ///
+ public string DistUrl
+ {
+ get { return this.GetString(IndexTag.RPMTAG_DISTURL); }
+ set { this.SetString(IndexTag.RPMTAG_DISTURL, value); }
+ }
+
+ ///
+ /// Gets or sets the format of the payload. Should be cpio.
+ ///
+ public string PayloadFormat
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PAYLOADFORMAT); }
+ set { this.SetString(IndexTag.RPMTAG_PAYLOADFORMAT, value); }
+ }
+
+ ///
+ /// Gets or sets the name of the compressor used to compress the payload.
+ ///
+ public string PayloadCompressor
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PAYLOADCOMPRESSOR); }
+ set { this.SetString(IndexTag.RPMTAG_PAYLOADCOMPRESSOR, value); }
+ }
+
+ ///
+ /// Gets or sets the compression level used for the payload.
+ ///
+ public string PayloadFlags
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PAYLOADFLAGS); }
+ set { this.SetString(IndexTag.RPMTAG_PAYLOADFLAGS, value); }
+ }
+
+ ///
+ /// Gets or sets an opaque string whose value is undefined.
+ ///
+ public string Platform
+ {
+ get { return this.GetString(IndexTag.RPMTAG_PLATFORM); }
+ set { this.SetString(IndexTag.RPMTAG_PLATFORM, value); }
+ }
+
+ public byte[] SourcePkgId
+ {
+ get { return this.GetByteArray(IndexTag.RPMTAG_SOURCEPKGID); }
+ set { this.SetByteArray(IndexTag.RPMTAG_SOURCEPKGID, value); }
+ }
+
+ ///
+ /// Gets or sets the hashing algorithm used to calculate the hash of files embedded
+ /// in this RPM archive.
+ ///
+ public PgpHashAlgo FileDigetsAlgo
+ {
+ get { return (PgpHashAlgo)this.GetInt(IndexTag.RPMTAG_FILEDIGESTALGO); }
+ set { this.SetInt(IndexTag.RPMTAG_FILEDIGESTALGO, (int)value); }
+ }
+
+ ///
+ /// Gets or sets all change log entries.
+ ///
+ public IEnumerable ChangelogEntries
+ {
+ get
+ {
+ var times = this.GetIntArray(IndexTag.RPMTAG_CHANGELOGTIME);
+ var names = this.GetStringArray(IndexTag.RPMTAG_CHANGELOGNAME);
+ var text = this.GetStringArray(IndexTag.RPMTAG_CHANGELOGTEXT);
+
+ int count = Math.Min(times.Count, Math.Min(names.Count, text.Count));
+
+ for (int i = 0; i < count; i++)
+ {
+ yield return new ChangelogEntry()
+ {
+ Date = DateTimeOffset.FromUnixTimeSeconds(times[i]),
+ Name = names[i],
+ Text = text[i]
+ };
+ }
+ }
+
+ set
+ {
+ var entries = value.ToArray();
+
+ var times = new int[entries.Length];
+ var names = new string[entries.Length];
+ var text = new string[entries.Length];
+
+ for (int i = 0; i < entries.Length; i++)
+ {
+ times[i] = (int)entries[i].Date.ToUnixTimeSeconds();
+ names[i] = entries[i].Name;
+ text[i] = entries[i].Text;
+ }
+
+ this.SetIntArray(IndexTag.RPMTAG_CHANGELOGTIME, times);
+ this.SetStringArray(IndexTag.RPMTAG_CHANGELOGNAME, names);
+ this.SetStringArray(IndexTag.RPMTAG_CHANGELOGTEXT, text);
+ }
+ }
+
+ ///
+ /// Gets or sets a list of all files embedded in this package.
+ ///
+ public IEnumerable Files
+ {
+ get
+ {
+ var sizes = this.GetIntArray(IndexTag.RPMTAG_FILESIZES);
+ var modes = this.GetShortArray(IndexTag.RPMTAG_FILEMODES);
+ var rdevs = this.GetShortArray(IndexTag.RPMTAG_FILERDEVS);
+ var mtimes = this.GetIntArray(IndexTag.RPMTAG_FILEMTIMES);
+ var md5s = this.GetStringArray(IndexTag.RPMTAG_FILEDIGESTS);
+ var linkTos = this.GetStringArray(IndexTag.RPMTAG_FILELINKTOS);
+ var flags = this.GetIntArray(IndexTag.RPMTAG_FILEFLAGS);
+ var usernames = this.GetStringArray(IndexTag.RPMTAG_FILEUSERNAME);
+ var groupnames = this.GetStringArray(IndexTag.RPMTAG_FILEGROUPNAME);
+ var verifyFlags = this.GetIntArray(IndexTag.RPMTAG_FILEVERIFYFLAGS);
+ var devices = this.GetIntArray(IndexTag.RPMTAG_FILEDEVICES);
+ var inodes = this.GetIntArray(IndexTag.RPMTAG_FILEINODES);
+ var langs = this.GetStringArray(IndexTag.RPMTAG_FILELANGS);
+ var colors = this.GetIntArray(IndexTag.RPMTAG_FILECOLORS);
+ var classes = this.GetIntArray(IndexTag.RPMTAG_FILECLASS);
+ var dependsX = this.GetIntArray(IndexTag.RPMTAG_FILEDEPENDSX);
+ var dependsN = this.GetIntArray(IndexTag.RPMTAG_FILEDEPENDSN);
+
+ var baseNames = this.GetStringArray(IndexTag.RPMTAG_BASENAMES);
+ var dirIndexes = this.GetIntArray(IndexTag.RPMTAG_DIRINDEXES);
+ var dirNames = this.GetStringArray(IndexTag.RPMTAG_DIRNAMES);
+
+ var classDict = this.GetStringArray(IndexTag.RPMTAG_CLASSDICT);
+ var dependsDict = this.GetIntArray(IndexTag.RPMTAG_DEPENDSDICT);
+
+ for (int i = 0; i < sizes.Count; i++)
+ {
+ Collection requires = new Collection();
+ Collection provides = new Collection();
+
+ for (int j = dependsX[i]; j < dependsX[i] + dependsN[i]; j++)
+ {
+ // https://github.com/rpm-software-management/rpm/blob/8f509d669b9ae79c86dd510c5a4bc5109f60d733/build/rpmfc.c#L734
+ var value = dependsDict[j];
+
+ var dependencyType = this.GetDependencyTag((char)((value >> 24) & 0xFF));
+ var index = value & 0x00ffffff;
+
+ var values = this.GetStringArray(dependencyType);
+ Collection versions;
+ RpmSense[] types;
+
+ switch (dependencyType)
+ {
+ case IndexTag.RPMTAG_REQUIRENAME:
+ versions = this.GetStringArray(IndexTag.RPMTAG_REQUIREVERSION);
+ types = this.GetIntArray(IndexTag.RPMTAG_REQUIREFLAGS).Select(e => (RpmSense)e).ToArray();
+ break;
+
+ case IndexTag.RPMTAG_PROVIDENAME:
+ versions = this.GetStringArray(IndexTag.RPMTAG_PROVIDEVERSION);
+ types = this.GetIntArray(IndexTag.RPMTAG_PROVIDEFLAGS).Select(e => (RpmSense)e).ToArray();
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(dependencyType));
+ }
+
+ var dependencyName = values[index];
+ var dependencyVersion = versions[index];
+ var dependencyFlags = types[index];
+
+ var dependency = new PackageDependency()
+ {
+ Flags = dependencyFlags,
+ Name = dependencyName,
+ Version = dependencyVersion
+ };
+
+ switch (dependencyType)
+ {
+ case IndexTag.RPMTAG_REQUIRENAME:
+ requires.Add(dependency);
+ break;
+
+ case IndexTag.RPMTAG_PROVIDENAME:
+ provides.Add(dependency);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(dependencyType));
+ }
+ }
+
+ var directoryName = dirNames[dirIndexes[i]];
+ var name = $"{directoryName}{baseNames[i]}";
+
+ yield return new RpmFile()
+ {
+ Size = sizes[i],
+ Mode = (LinuxFileMode)modes[i],
+ Rdev = rdevs[i],
+ ModifiedTime = DateTimeOffset.FromUnixTimeSeconds(mtimes[i]),
+ MD5Hash = RpmSignature.StringToByteArray(md5s[i]),
+ LinkTo = linkTos[i],
+ Flags = (RpmFileFlags)flags[i],
+ UserName = usernames[i],
+ GroupName = groupnames[i],
+ VerifyFlags = (RpmVerifyFlags)verifyFlags[i],
+ Device = devices[i],
+ Inode = inodes[i],
+ Lang = langs[i],
+ Color = (RpmFileColor)colors[i],
+ Class = classDict[classes[i]],
+ Requires = requires,
+ Provides = provides,
+ Name = name
+ };
+ }
+ }
+
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ var files = value.ToArray();
+
+ var sizes = new int[files.Length];
+ var modes = new short[files.Length];
+ var rdevs = new short[files.Length];
+ var mtimes = new int[files.Length];
+ var md5s = new string[files.Length];
+ var linkTos = new string[files.Length];
+ var flags = new int[files.Length];
+ var usernames = new string[files.Length];
+ var groupnames = new string[files.Length];
+ var verifyFlags = new int[files.Length];
+ var devices = new int[files.Length];
+ var inodes = new int[files.Length];
+ var langs = new string[files.Length];
+ var colors = new int[files.Length];
+ var classes = new int[files.Length];
+
+ var baseNames = new string[files.Length];
+ var dirIndexes = new int[files.Length];
+ var dirNames = new Collection();
+
+ var dependsX = new int[files.Length];
+ var dependsN = new int[files.Length];
+ var classDict = new Collection();
+ var dependsDict = new Collection();
+
+ var currentDependencies = this.Dependencies.ToList();
+ var currentProvides = this.Provides.ToList();
+
+ // Prepopulate the new dependencies in sorted order
+ var allDependencies = files
+ .SelectMany(f => f.Requires)
+ .Distinct()
+ .Where(d => !currentDependencies.Contains(d))
+ .OrderBy(f => f.Name)
+ .ToList();
+
+ var allProvides = files
+ .SelectMany(f => f.Provides)
+ .Distinct()
+ .Where(d => !currentProvides.Contains(d))
+ .ToList();
+
+ currentDependencies.AddRange(allDependencies);
+ this.Dependencies = currentDependencies;
+
+ currentProvides.AddRange(allProvides);
+ this.Provides = currentProvides;
+
+ var requireNames = this.GetStringArray(IndexTag.RPMTAG_REQUIRENAME);
+ var provideNames = this.GetStringArray(IndexTag.RPMTAG_PROVIDENAME);
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ var file = files[i];
+ sizes[i] = file.Size;
+ modes[i] = (short)file.Mode;
+ rdevs[i] = file.Rdev;
+ mtimes[i] = (int)file.ModifiedTime.ToUnixTimeSeconds();
+ md5s[i] = file.MD5Hash == null ? string.Empty : BitConverter.ToString(file.MD5Hash).Replace("-", string.Empty).ToLowerInvariant();
+ linkTos[i] = file.LinkTo;
+ usernames[i] = file.UserName;
+ groupnames[i] = file.GroupName;
+ verifyFlags[i] = (int)file.VerifyFlags;
+ devices[i] = file.Device;
+ inodes[i] = file.Inode;
+ langs[i] = file.Lang;
+ colors[i] = (int)file.Color;
+ flags[i] = (int)file.Flags;
+
+ if (!classDict.Contains(file.Class))
+ {
+ classDict.Add(file.Class);
+ }
+
+ classes[i] = classDict.IndexOf(file.Class);
+
+ var dirName = Path.GetDirectoryName(file.Name).Replace('\\', '/') + '/';
+ var fileName = Path.GetFileName(file.Name);
+
+ if (!dirNames.Contains(dirName))
+ {
+ dirNames.Add(dirName);
+ }
+
+ dirIndexes[i] = dirNames.IndexOf(dirName);
+ baseNames[i] = fileName;
+
+ int dependX = dependsDict.Count;
+ dependsN[i] = file.Requires.Count + file.Provides.Count;
+ dependsX[i] = dependsN[i] == 0 ? 0 : dependX;
+
+ foreach (var provide in file.Provides)
+ {
+ byte type = (byte)this.GetDependencyType(IndexTag.RPMTAG_PROVIDENAME);
+
+ // Incomplete - we should check for not only name but also flags & version
+ var index = provideNames.IndexOf(provide.Name);
+ index |= type << 24;
+
+ dependsDict.Add(index);
+ }
+
+ foreach (var dependency in file.Requires)
+ {
+ byte type = (byte)this.GetDependencyType(IndexTag.RPMTAG_REQUIRENAME);
+
+ // Incomplete - we should check for not only name but also flags & version
+ var index = requireNames.IndexOf(dependency.Name);
+ index |= type << 24;
+
+ dependsDict.Add(index);
+ }
+ }
+
+ this.SetIntArray(IndexTag.RPMTAG_FILESIZES, sizes);
+ this.SetShortArray(IndexTag.RPMTAG_FILEMODES, modes);
+ this.SetShortArray(IndexTag.RPMTAG_FILERDEVS, rdevs);
+ this.SetIntArray(IndexTag.RPMTAG_FILEMTIMES, mtimes);
+ this.SetStringArray(IndexTag.RPMTAG_FILEDIGESTS, md5s);
+ this.SetStringArray(IndexTag.RPMTAG_FILELINKTOS, linkTos);
+ this.SetIntArray(IndexTag.RPMTAG_FILEFLAGS, flags);
+ this.SetStringArray(IndexTag.RPMTAG_FILEUSERNAME, usernames);
+ this.SetStringArray(IndexTag.RPMTAG_FILEGROUPNAME, groupnames);
+ this.SetIntArray(IndexTag.RPMTAG_FILEVERIFYFLAGS, verifyFlags);
+ this.SetIntArray(IndexTag.RPMTAG_FILEDEVICES, devices);
+ this.SetIntArray(IndexTag.RPMTAG_FILEINODES, inodes);
+ this.SetStringArray(IndexTag.RPMTAG_FILELANGS, langs);
+ this.SetIntArray(IndexTag.RPMTAG_FILECOLORS, colors);
+ this.SetIntArray(IndexTag.RPMTAG_FILECLASS, classes);
+ this.SetIntArray(IndexTag.RPMTAG_FILEDEPENDSX, dependsX);
+ this.SetIntArray(IndexTag.RPMTAG_FILEDEPENDSN, dependsN);
+
+ this.SetStringArray(IndexTag.RPMTAG_BASENAMES, baseNames);
+ this.SetIntArray(IndexTag.RPMTAG_DIRINDEXES, dirIndexes);
+ this.SetStringArray(IndexTag.RPMTAG_DIRNAMES, dirNames.ToArray());
+
+ this.SetStringArray(IndexTag.RPMTAG_CLASSDICT, classDict.ToArray());
+ this.SetIntArray(IndexTag.RPMTAG_DEPENDSDICT, dependsDict.ToArray());
+
+ this.Size = files.Where(f => f.Mode.HasFlag(LinuxFileMode.S_IFREG)).Sum(f => f.Size) - 0x24;
+ }
+ }
+
+ public IEnumerable Dependencies
+ {
+ get
+ {
+ var flags = this.GetIntArray(IndexTag.RPMTAG_REQUIREFLAGS);
+ var names = this.GetStringArray(IndexTag.RPMTAG_REQUIRENAME);
+ var vers = this.GetStringArray(IndexTag.RPMTAG_REQUIREVERSION);
+
+ for (int i = 0; i < flags.Count; i++)
+ {
+ yield return new PackageDependency()
+ {
+ Flags = (RpmSense)flags[i],
+ Name = names[i],
+ Version = vers[i]
+ };
+ }
+ }
+
+ set
+ {
+ var dependencies = value.ToArray();
+
+ var flags = new int[dependencies.Length];
+ var names = new string[dependencies.Length];
+ var vers = new string[dependencies.Length];
+
+ for (int i = 0; i < dependencies.Length; i++)
+ {
+ flags[i] = (int)dependencies[i].Flags;
+ names[i] = dependencies[i].Name;
+ vers[i] = dependencies[i].Version;
+ }
+
+ this.SetIntArray(IndexTag.RPMTAG_REQUIREFLAGS, flags);
+ this.SetStringArray(IndexTag.RPMTAG_REQUIRENAME, names);
+ this.SetStringArray(IndexTag.RPMTAG_REQUIREVERSION, vers);
+ }
+ }
+
+ public IEnumerable Provides
+ {
+ get
+ {
+ var flags = this.GetIntArray(IndexTag.RPMTAG_PROVIDEFLAGS);
+ var names = this.GetStringArray(IndexTag.RPMTAG_PROVIDENAME);
+ var vers = this.GetStringArray(IndexTag.RPMTAG_PROVIDEVERSION);
+
+ for (int i = 0; i < flags.Count; i++)
+ {
+ yield return new PackageDependency()
+ {
+ Flags = (RpmSense)flags[i],
+ Name = names[i],
+ Version = vers[i]
+ };
+ }
+ }
+
+ set
+ {
+ var provides = value.ToArray();
+
+ var flags = new int[provides.Length];
+ var names = new string[provides.Length];
+ var vers = new string[provides.Length];
+
+ for (int i = 0; i < provides.Length; i++)
+ {
+ flags[i] = (int)provides[i].Flags;
+ names[i] = provides[i].Name;
+ vers[i] = provides[i].Version;
+ }
+
+ this.SetIntArray(IndexTag.RPMTAG_PROVIDEFLAGS, flags);
+ this.SetStringArray(IndexTag.RPMTAG_PROVIDENAME, names);
+ this.SetStringArray(IndexTag.RPMTAG_PROVIDEVERSION, vers);
+ }
+ }
+
+ protected char GetDependencyType(IndexTag tag)
+ {
+ switch (tag)
+ {
+ case IndexTag.RPMTAG_PROVIDENAME:
+ return 'P';
+ case IndexTag.RPMTAG_REQUIRENAME:
+ return 'R';
+ case IndexTag.RPMTAG_RECOMMENDNAME:
+ return 'r';
+ case IndexTag.RPMTAG_SUGGESTNAME:
+ return 'S';
+ case IndexTag.RPMTAG_SUPPLEMENTNAME:
+ return 'S';
+ case IndexTag.RPMTAG_ENHANCENAME:
+ return 'e';
+ case IndexTag.RPMTAG_CONFLICTNAME:
+ return 'C';
+ case IndexTag.RPMTAG_OBSOLETENAME:
+ return 'O';
+ default:
+ throw new ArgumentOutOfRangeException(nameof(tag));
+ }
+ }
+
+ protected IndexTag GetDependencyTag(char deptype)
+ {
+ switch (deptype)
+ {
+ case 'P':
+ return IndexTag.RPMTAG_PROVIDENAME;
+ case 'R':
+ return IndexTag.RPMTAG_REQUIRENAME;
+ case 'r':
+ return IndexTag.RPMTAG_RECOMMENDNAME;
+ case 's':
+ return IndexTag.RPMTAG_SUGGESTNAME;
+ case 'S':
+ return IndexTag.RPMTAG_SUPPLEMENTNAME;
+ case 'e':
+ return IndexTag.RPMTAG_ENHANCENAME;
+ case 'C':
+ return IndexTag.RPMTAG_CONFLICTNAME;
+ case 'O':
+ return IndexTag.RPMTAG_OBSOLETENAME;
+ default:
+ return IndexTag.RPMTAG_NOT_FOUND;
+ }
+ }
+
+ protected Collection GetStringArray(IndexTag tag)
+ {
+ var value = this.GetValue>(tag, IndexType.RPM_STRING_ARRAY_TYPE);
+
+ if (value == null)
+ {
+ return new Collection();
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ protected void SetStringArray(IndexTag tag, string[] value)
+ {
+ this.SetValue(tag, IndexType.RPM_STRING_ARRAY_TYPE, value);
+ }
+
+ protected string GetLocalizedString(IndexTag tag)
+ {
+ var localizedValues = this.GetValue>(tag, IndexType.RPM_I18NSTRING_TYPE);
+
+ return localizedValues.First();
+ }
+
+ protected void SetLocalizedString(IndexTag tag, string value)
+ {
+ if (this.Locales.Count == 0)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var values = new string[this.Locales.Count];
+
+ for (int i = 0; i < values.Length; i++)
+ {
+ values[i] = value;
+ }
+
+ this.SetValue(tag, IndexType.RPM_I18NSTRING_TYPE, values);
+ }
+
+ protected string GetString(IndexTag tag)
+ {
+ return this.GetValue(tag, IndexType.RPM_STRING_TYPE);
+ }
+
+ protected void SetString(IndexTag tag, string value)
+ {
+ this.SetSingleValue(tag, IndexType.RPM_STRING_TYPE, value);
+ }
+
+ protected int GetInt(IndexTag tag)
+ {
+ return this.GetValue(tag, IndexType.RPM_INT32_TYPE);
+ }
+
+ protected void SetInt(IndexTag tag, int value)
+ {
+ this.SetSingleValue(tag, IndexType.RPM_INT32_TYPE, value);
+ }
+
+ protected Collection GetIntArray(IndexTag tag)
+ {
+ var value = this.GetValue>(tag, IndexType.RPM_INT32_TYPE);
+
+ if (value == null)
+ {
+ return new Collection();
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ protected void SetIntArray(IndexTag tag, int[] value)
+ {
+ this.SetValue(tag, IndexType.RPM_INT32_TYPE, value);
+ }
+
+ protected Collection GetShortArray(IndexTag tag)
+ {
+ return this.GetValue>(tag, IndexType.RPM_INT16_TYPE);
+ }
+
+ protected void SetShortArray(IndexTag tag, short[] value)
+ {
+ this.SetValue(tag, IndexType.RPM_INT16_TYPE, value);
+ }
+
+ protected byte[] GetByteArray(IndexTag tag)
+ {
+ return this.GetValue(tag, IndexType.RPM_BIN_TYPE);
+ }
+
+ protected void SetByteArray(IndexTag tag, byte[] value)
+ {
+ this.SetValue(tag, IndexType.RPM_BIN_TYPE, value);
+ }
+
+ protected T GetValue(IndexTag tag, IndexType type)
+ {
+ if (!this.Package.Header.Records.ContainsKey(tag))
+ {
+ return default(T);
+ }
+
+ var record = this.Package.Header.Records[tag];
+
+ if (record.Header.Type != type)
+ {
+ throw new ArgumentOutOfRangeException(nameof(tag));
+ }
+
+ return (T)record.Value;
+ }
+
+ protected void SetValue(IndexTag tag, IndexType type, T[] value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ // We won't accept empty arrays; rather, we remove the key alltogether.
+ // This brings compatibility with newer versions of RPM
+ if (value.Length > 0)
+ {
+ var collection = new Collection(value);
+
+ IndexRecord record = new IndexRecord()
+ {
+ Header = new IndexHeader()
+ {
+ Count = collection.Count,
+ Offset = 0,
+ Tag = (uint)tag,
+ Type = type
+ },
+ Value = collection
+ };
+
+ if (!this.Package.Header.Records.ContainsKey(tag))
+ {
+ this.Package.Header.Records.Add(tag, record);
+ }
+ else
+ {
+ this.Package.Header.Records[tag] = record;
+ }
+ }
+ else
+ {
+ if (this.Package.Header.Records.ContainsKey(tag))
+ {
+ this.Package.Header.Records.Remove(tag);
+ }
+ }
+ }
+
+ protected void SetSingleValue(IndexTag tag, IndexType type, T value)
+ {
+ IndexRecord record = new IndexRecord()
+ {
+ Header = new IndexHeader()
+ {
+ Count = 1,
+ Offset = 0,
+ Tag = (uint)tag,
+ Type = type
+ },
+ Value = value
+ };
+
+ if (!this.Package.Header.Records.ContainsKey(tag))
+ {
+ this.Package.Header.Records.Add(tag, record);
+ }
+ else
+ {
+ this.Package.Header.Records[tag] = record;
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmPackage.cs b/DebUOS/Packaging.Targets/Rpm/RpmPackage.cs
new file mode 100644
index 0000000..017351e
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmPackage.cs
@@ -0,0 +1,49 @@
+using System.IO;
+
+namespace Packaging.Targets.Rpm
+{
+ internal class RpmPackage
+ {
+ public RpmPackage()
+ {
+ this.Header = new Section();
+ this.Signature = new Section();
+ }
+
+ public RpmLead Lead
+ {
+ get;
+ set;
+ }
+
+ public Section Header
+ {
+ get;
+ set;
+ }
+
+ public Section Signature
+ {
+ get;
+ set;
+ }
+
+ public long HeaderOffset
+ {
+ get;
+ set;
+ }
+
+ public long PayloadOffset
+ {
+ get;
+ set;
+ }
+
+ public Stream Stream
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmPackageCreator.cs b/DebUOS/Packaging.Targets/Rpm/RpmPackageCreator.cs
new file mode 100644
index 0000000..9184fa8
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmPackageCreator.cs
@@ -0,0 +1,949 @@
+using DiscUtils.Internal;
+using Org.BouncyCastle.Bcpg.OpenPgp;
+using Packaging.Targets.IO;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Supports creating objects based on a .
+ ///
+ internal class RpmPackageCreator
+ {
+ ///
+ /// The which analyzes the files in this package and provides required
+ /// metadata for the files.
+ ///
+ private readonly IFileAnalyzer analyzer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RpmPackageCreator()
+ : this(new FileAnalyzer())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// An used to analyze the files in this package and provides required
+ /// meatata for the files.
+ ///
+ public RpmPackageCreator(IFileAnalyzer analyzer)
+ {
+ if (analyzer == null)
+ {
+ throw new ArgumentNullException(nameof(analyzer));
+ }
+
+ this.analyzer = analyzer;
+ }
+
+ ///
+ /// Gets or sets the number of threads to use when compressing .xz files.
+ /// The default is to use the number of CPUs. You may want to set this to 1 for
+ /// compatibility purposes.
+ ///
+ public int CompressionThreads { get; set; } = XZOutputStream.DefaultThreads;
+
+ ///
+ /// Creates a RPM Package.
+ ///
+ ///
+ /// The archive entries which make up the RPM package.
+ ///
+ ///
+ /// A which contains the CPIO archive for the RPM package.
+ ///
+ ///
+ /// The name of the package.
+ ///
+ ///
+ /// The version of the software.
+ ///
+ ///
+ /// The architecture targetted by the package.
+ ///
+ ///
+ /// The release version.
+ ///
+ ///
+ /// to create a user account; otherwise, .
+ ///
+ ///
+ /// The name of the user account to create.
+ ///
+ ///
+ /// to install a system service, otherwise, .
+ ///
+ ///
+ /// The name of the system service to create.
+ ///
+ ///
+ /// The package vendor.
+ ///
+ ///
+ /// The package description.
+ ///
+ ///
+ /// The package URL.
+ ///
+ ///
+ /// A prefix to use.
+ ///
+ ///
+ /// Pre-Install script
+ ///
+ ///
+ /// Post-Install script
+ ///
+ ///
+ /// Pre-Remove script
+ ///
+ ///
+ /// Post-Remove script
+ ///
+ ///
+ /// Additional dependencies to add to the RPM package.
+ ///
+ ///
+ /// Any additional metadata.
+ ///
+ ///
+ /// The private key to use when signing the package.
+ ///
+ ///
+ /// The to which to write the package.
+ ///
+ public void CreatePackage(
+ List archiveEntries,
+ Stream payloadStream,
+ string name,
+ string version,
+ string arch,
+ string release,
+ bool createUser,
+ string userName,
+ bool installService,
+ string serviceName,
+ string vendor,
+ string description,
+ string url,
+ string prefix,
+ string preInstallScript,
+ string postInstallScript,
+ string preRemoveScript,
+ string postRemoveScript,
+ IEnumerable additionalDependencies,
+ Action additionalMetadata,
+ PgpPrivateKey privateKey,
+ Stream targetStream)
+ {
+ this.CreatePackage(
+ archiveEntries,
+ payloadStream,
+ name,
+ version,
+ arch,
+ release,
+ createUser,
+ userName,
+ installService,
+ serviceName,
+ vendor,
+ description,
+ url,
+ prefix,
+ preInstallScript,
+ postInstallScript,
+ preRemoveScript,
+ postRemoveScript,
+ additionalDependencies,
+ additionalMetadata,
+ new PackageSigner(privateKey),
+ targetStream);
+ }
+
+ ///
+ /// Creates a RPM Package.
+ ///
+ ///
+ /// The archive entries which make up the RPM package.
+ ///
+ ///
+ /// A which contains the CPIO archive for the RPM package.
+ ///
+ ///
+ /// The name of the package.
+ ///
+ ///
+ /// The version of the software.
+ ///
+ ///
+ /// The architecture targetted by the package.
+ ///
+ ///
+ /// The release version.
+ ///
+ ///
+ /// to create a user account; otherwise, .
+ ///
+ ///
+ /// The name of the user account to create.
+ ///
+ ///
+ /// to install a system service, otherwise, .
+ ///
+ ///
+ /// The name of the system service to create.
+ ///
+ ///
+ /// The package vendor.
+ ///
+ ///
+ /// The package description.
+ ///
+ ///
+ /// The package URL.
+ ///
+ ///
+ /// A prefix to use.
+ ///
+ ///
+ /// Pre-Install script
+ ///
+ ///
+ /// Post-Install script
+ ///
+ ///
+ /// Pre-Remove script
+ ///
+ ///
+ /// Post-Remove script
+ ///
+ ///
+ /// Additional dependencies to add to the RPM package.
+ ///
+ ///
+ /// Any additional metadata.
+ ///
+ ///
+ /// The signer to use when signing the package.
+ ///
+ ///
+ /// The to which to write the package.
+ ///
+ ///
+ /// to include the version number and release number
+ /// in the ; to only
+ /// use the package name.
+ ///
+ ///
+ /// if is already
+ /// compressed. In this case, the will be
+ /// copied "as is" to the resulting RPM package.
+ ///
+ public void CreatePackage(
+ List archiveEntries,
+ Stream payloadStream,
+ string name,
+ string version,
+ string arch,
+ string release,
+ bool createUser,
+ string userName,
+ bool installService,
+ string serviceName,
+ string vendor,
+ string description,
+ string url,
+ string prefix,
+ string preInstallScript,
+ string postInstallScript,
+ string preRemoveScript,
+ string postRemoveScript,
+ IEnumerable additionalDependencies,
+ Action additionalMetadata,
+ IPackageSigner signer,
+ Stream targetStream,
+ bool includeVersionInName = false,
+ bool payloadIsCompressed = false)
+ {
+ // This routine goes roughly like:
+ // 1. Calculate all the metadata, including a signature,
+ // but use an empty compressed payload to calculate
+ // the signature
+ // 2. Write out the rpm file, and compress the payload
+ // 3. Update the signature
+ //
+ // This way, we avoid having to compress the payload into a temporary
+ // file.
+
+ // Core routine to populate files and dependencies (part of the metadata
+ // in the header)
+ RpmPackage package = new RpmPackage();
+ var metadata = new RpmMetadata(package)
+ {
+ Name = name,
+ Version = version,
+ Arch = arch,
+ Release = release,
+ };
+
+ this.AddPackageProvides(metadata);
+ this.AddLdDependencies(metadata);
+
+ var files = this.CreateFiles(archiveEntries);
+ metadata.Files = files;
+
+ this.AddRpmDependencies(metadata, additionalDependencies);
+
+ // Try to define valid defaults for most metadata
+ metadata.Locales = new Collection { "C" }; // Should come before any localizable data.
+ metadata.BuildHost = "dotnet-rpm";
+ metadata.BuildTime = DateTimeOffset.Now;
+ metadata.Cookie = "dotnet-rpm";
+ metadata.FileDigetsAlgo = PgpHashAlgo.PGPHASHALGO_SHA256;
+ metadata.Group = "System Environment/Libraries";
+ metadata.OptFlags = string.Empty;
+ metadata.Os = "linux";
+ metadata.PayloadCompressor = "xz";
+ metadata.PayloadFlags = "2";
+ metadata.PayloadFormat = "cpio";
+ metadata.Platform = "x86_64-redhat-linux-gnu";
+ metadata.RpmVersion = "4.11.3";
+ metadata.SourcePkgId = new byte[0x10];
+ metadata.SourceRpm = $"{name}-{version}-{release}.src.rpm";
+
+ // Scripts which run before & after installation and removal.
+ var preIn = preInstallScript ?? string.Empty;
+ var postIn = postInstallScript ?? string.Empty;
+ var preUn = preRemoveScript ?? string.Empty;
+ var postUn = postRemoveScript ?? string.Empty;
+
+ if (createUser)
+ {
+ // Add the user and group, under which the service runs.
+ // These users are never removed because UIDs are re-used on Linux.
+ preIn += $"/usr/sbin/groupadd -r {userName} 2>/dev/null || :\n" +
+ $"/usr/sbin/useradd -g {userName} -s /sbin/nologin -r {userName} 2>/dev/null || :\n";
+ }
+
+ if (installService)
+ {
+ // Install and activate the service.
+ postIn +=
+ $"if [ $1 -eq 1 ] ; then \n" +
+ $" systemctl enable --now {serviceName}.service >/dev/null 2>&1 || : \n" +
+ $"fi\n";
+
+ preUn +=
+ $"if [ $1 -eq 0 ] ; then \n" +
+ $" # Package removal, not upgrade \n" +
+ $" systemctl --no-reload disable --now {serviceName}.service > /dev/null 2>&1 || : \n" +
+ $"fi\n";
+
+ postUn +=
+ $"if [ $1 -ge 1 ] ; then \n" +
+ $" # Package upgrade, not uninstall \n" +
+ $" systemctl try-restart {serviceName}.service >/dev/null 2>&1 || : \n" +
+ $"fi\n";
+ }
+
+ // Remove all directories marked as such (these are usually directories which contain temporary files)
+ foreach (var entryToRemove in archiveEntries.Where(e => e.RemoveOnUninstall))
+ {
+ preUn +=
+ $"if [ $1 -eq 0 ] ; then \n" +
+ $" # Package removal, not upgrade \n" +
+ $" /usr/bin/rm -rf {entryToRemove.TargetPath}\n" +
+ $"fi\n";
+ }
+
+ if (!string.IsNullOrEmpty(preIn))
+ {
+ metadata.PreInProg = "/bin/sh";
+ metadata.PreIn = preIn;
+ }
+
+ if (!string.IsNullOrEmpty(postIn))
+ {
+ metadata.PostInProg = "/bin/sh";
+ metadata.PostIn = postIn;
+ }
+
+ if (!string.IsNullOrEmpty(preUn))
+ {
+ metadata.PreUnProg = "/bin/sh";
+ metadata.PreUn = preUn;
+ }
+
+ if (!string.IsNullOrEmpty(postUn))
+ {
+ metadata.PostUnProg = "/bin/sh";
+ metadata.PostUn = postUn;
+ }
+
+ // Not providing these (or setting empty values) would cause rpmlint errors
+ metadata.Description = string.IsNullOrEmpty(description)
+ ? $"{name} version {version}-{release}"
+ : description;
+ metadata.Summary = $"{name} version {version}-{release}";
+ metadata.License = $"{name} License";
+
+ metadata.Distribution = string.Empty;
+ metadata.DistUrl = string.Empty;
+ metadata.Url = url ?? string.Empty;
+ metadata.Vendor = vendor ?? string.Empty;
+
+ metadata.ChangelogEntries = new Collection()
+ {
+ new ChangelogEntry(DateTimeOffset.Now, "dotnet-rpm", "Created a RPM package using dotnet-rpm")
+ };
+
+ // User-set metadata
+ if (additionalMetadata != null)
+ {
+ additionalMetadata(metadata);
+ }
+
+ this.CalculateHeaderOffsets(package);
+
+ using (MemoryStream dummyCompressedPayload = new MemoryStream())
+ {
+ using (XZOutputStream dummyPayloadCompressor =
+ new XZOutputStream(
+ dummyCompressedPayload,
+ this.CompressionThreads,
+ XZOutputStream.DefaultPreset,
+ leaveOpen: true))
+ {
+ dummyPayloadCompressor.Write(new byte[] { 0 }, 0, 1);
+ }
+
+ this.CalculateSignature(package, signer, dummyCompressedPayload);
+ }
+
+ this.CalculateSignatureOffsets(package);
+
+ // Write out all the data - includes the lead
+ byte[] nameBytes = new byte[66];
+ var nameInLead = includeVersionInName ? $"{name}-{version}-{release}" : name;
+
+ Encoding.UTF8.GetBytes(nameInLead, 0, nameInLead.Length, nameBytes, 0);
+
+ var lead = new RpmLead()
+ {
+ ArchNum = 1,
+ Magic = 0xedabeedb,
+ Major = 0x03,
+ Minor = 0x00,
+ NameBytes = nameBytes,
+ OsNum = 0x0001,
+ Reserved = new byte[16],
+ SignatureType = 0x0005,
+ Type = 0x0000,
+ };
+
+ // Write out the lead, signature and header
+ targetStream.Position = 0;
+ targetStream.SetLength(0);
+
+ targetStream.WriteStruct(lead);
+ this.WriteSignature(package, targetStream);
+ this.WriteHeader(package, targetStream);
+
+ // Write out the compressed payload
+ int compressedPayloadOffset = (int)targetStream.Position;
+
+ // The user can choose to pass an already-comrpessed
+ // payload. In this case, no need to re-compress.
+ if (payloadIsCompressed)
+ {
+ payloadStream.CopyTo(targetStream);
+ }
+ else
+ {
+ using (XZOutputStream compressor = new XZOutputStream(
+ targetStream,
+ XZOutputStream.DefaultThreads,
+ XZOutputStream.DefaultPreset,
+ leaveOpen: true))
+ {
+ payloadStream.Position = 0;
+ payloadStream.CopyTo(compressor);
+ }
+ }
+
+ using (SubStream compressedPayloadStream = new SubStream(
+ targetStream,
+ compressedPayloadOffset,
+ targetStream.Length - compressedPayloadOffset,
+ leaveParentOpen: true,
+ readOnly: true))
+ {
+ this.CalculateSignature(package, signer, compressedPayloadStream);
+ this.CalculateSignatureOffsets(package);
+ }
+
+ // Update the lead and signature
+ targetStream.Position = 0;
+
+ targetStream.WriteStruct(lead);
+ this.WriteSignature(package, targetStream);
+ }
+
+ ///
+ /// Creates the metadata for all files in the .
+ ///
+ ///
+ /// The archive entries for which to generate the metadata.
+ ///
+ ///
+ /// A which contains all the metadata.
+ ///
+ public Collection CreateFiles(List archiveEntries)
+ {
+ Collection files = new Collection();
+
+ foreach (var entry in archiveEntries)
+ {
+ var size = entry.FileSize;
+
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFDIR))
+ {
+ size = 0x1000;
+ }
+
+ if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK))
+ {
+ // RPM uses cpio archives. cpio archives store the target of a symlink as
+ // the file content (as opposed to tar, which has a dedicated field for this).
+ // As a result, the file size is equal to the length of the path of the link
+ // target.
+ // https://bugzilla.redhat.com/show_bug.cgi?id=1224555 has some background info.
+ size = (uint)Encoding.UTF8.GetByteCount(entry.LinkTo);
+ }
+
+ RpmFile file = new RpmFile()
+ {
+ Size = (int)size,
+ Mode = entry.Mode,
+ Rdev = 0,
+ ModifiedTime = entry.Modified,
+ MD5Hash = entry.Sha256, // Yes, the MD5 hash does not actually contain a MD5 hash
+ LinkTo = entry.LinkTo,
+ Flags = this.analyzer.DetermineFlags(entry),
+ UserName = entry.Owner,
+ GroupName = entry.Group,
+ VerifyFlags = RpmVerifyFlags.RPMVERIFY_ALL,
+ Device = 1,
+ Inode = (int)entry.Inode,
+ Lang = string.Empty,
+ Color = this.analyzer.DetermineColor(entry),
+ Class = this.analyzer.DetermineClass(entry),
+ Requires = this.analyzer.DetermineRequires(entry),
+ Provides = this.analyzer.DetermineProvides(entry),
+ Name = entry.TargetPath
+ };
+
+ files.Add(file);
+ }
+
+ return files;
+ }
+
+ ///
+ /// Adds the package-level provides to the metadata. These are basically statements
+ /// indicating that the package provides, well, itself.
+ ///
+ ///
+ /// The package to which to add the provides.
+ ///
+ public void AddPackageProvides(RpmMetadata metadata)
+ {
+ var provides = metadata.Provides.ToList();
+
+ var packageProvides = new PackageDependency(metadata.Name, RpmSense.RPMSENSE_EQUAL, $"{metadata.Version}-{metadata.Release}");
+
+ var normalizedArch = metadata.Arch;
+ if (normalizedArch == "x86_64")
+ {
+ normalizedArch = "x86-64";
+ }
+
+ var packageArchProvides = new PackageDependency($"{metadata.Name}({normalizedArch})", RpmSense.RPMSENSE_EQUAL, $"{metadata.Version}-{metadata.Release}");
+
+ if (!provides.Contains(packageProvides))
+ {
+ provides.Add(packageProvides);
+ }
+
+ if (!provides.Contains(packageArchProvides))
+ {
+ provides.Add(packageArchProvides);
+ }
+
+ metadata.Provides = provides;
+ }
+
+ ///
+ /// Adds the dependency on ld to the RPM package. These dependencies cause ldconfig to run post installation
+ /// and uninstallation of the RPM package.
+ ///
+ ///
+ /// The to which to add the dependencies.
+ ///
+ public void AddLdDependencies(RpmMetadata metadata)
+ {
+ Collection ldDependencies = new Collection()
+ {
+ new PackageDependency("/sbin/ldconfig", RpmSense.RPMSENSE_INTERP | RpmSense.RPMSENSE_SCRIPT_POST, string.Empty),
+ new PackageDependency("/sbin/ldconfig", RpmSense.RPMSENSE_INTERP | RpmSense.RPMSENSE_SCRIPT_POSTUN, string.Empty)
+ };
+
+ var dependencies = metadata.Dependencies.ToList();
+ dependencies.AddRange(ldDependencies);
+ metadata.Dependencies = dependencies;
+ }
+
+ ///
+ /// Adds the RPM dependencies to the package. These dependencies express dependencies on specific RPM features, such as compressed file names,
+ /// file digets, and xz-compressed payloads.
+ ///
+ ///
+ /// The to which to add the dependencies.
+ ///
+ public void AddRpmDependencies(RpmMetadata metadata, IEnumerable additionalDependencies)
+ {
+ // Somehow, three rpmlib dependencies come before the rtld(GNU_HASH) dependency and one after.
+ // The rtld(GNU_HASH) indicates that hashes are stored in the .gnu_hash instead of the .hash section
+ // in the ELF file, so it is a file-level dependency that bubbles up
+ // http://lists.rpm.org/pipermail/rpm-maint/2014-September/003764.html
+ // 9:34 PM 10/6/2017
+ // To work around it, we remove the rtld(GNU_HASH) dependency on the files, remove it as a dependency,
+ // and add it back once we're done.
+ //
+ // The sole purpose of this is to ensure binary compatibility, which is probably not required at runtime,
+ // but makes certain regression tests more stable.
+ //
+ // Here we go:
+ var files = metadata.Files.ToArray();
+
+ var gnuHashFiles =
+ files
+ .Where(f => f.Requires.Any(r => string.Equals(r.Name, "rtld(GNU_HASH)", StringComparison.Ordinal)))
+ .ToArray();
+
+ foreach (var file in gnuHashFiles)
+ {
+ var rtldDependency = file.Requires.Where(r => string.Equals(r.Name, "rtld(GNU_HASH)", StringComparison.Ordinal)).Single();
+ file.Requires.Remove(rtldDependency);
+ }
+
+ // Refresh
+ metadata.Files = files;
+
+ Collection rpmDependencies = new Collection()
+ {
+ new PackageDependency("rpmlib(CompressedFileNames)", RpmSense.RPMSENSE_LESS | RpmSense.RPMSENSE_EQUAL | RpmSense.RPMSENSE_RPMLIB, "3.0.4-1"),
+ new PackageDependency("rpmlib(FileDigests)", RpmSense.RPMSENSE_LESS | RpmSense.RPMSENSE_EQUAL | RpmSense.RPMSENSE_RPMLIB, "4.6.0-1"),
+ new PackageDependency("rpmlib(PayloadFilesHavePrefix)", RpmSense.RPMSENSE_LESS | RpmSense.RPMSENSE_EQUAL | RpmSense.RPMSENSE_RPMLIB, "4.0-1"),
+ new PackageDependency("rtld(GNU_HASH)", RpmSense.RPMSENSE_FIND_REQUIRES, string.Empty),
+ };
+
+ // Inject any additional dependencies the user may have specified.
+ if (additionalDependencies != null)
+ {
+ rpmDependencies.AddRange(additionalDependencies);
+ }
+
+ rpmDependencies.Add(new PackageDependency("rpmlib(PayloadIsXz)", RpmSense.RPMSENSE_LESS | RpmSense.RPMSENSE_EQUAL | RpmSense.RPMSENSE_RPMLIB, "5.2-1"));
+
+ var dependencies = metadata.Dependencies.ToList();
+ var last = dependencies.Last();
+
+ if (last.Name == "rtld(GNU_HASH)")
+ {
+ dependencies.Remove(last);
+ }
+
+ dependencies.AddRange(rpmDependencies);
+ metadata.Dependencies = dependencies;
+
+ // Add the rtld(GNU_HASH) dependency back to the files
+ foreach (var file in gnuHashFiles)
+ {
+ file.Requires.Add(new PackageDependency("rtld(GNU_HASH)", RpmSense.RPMSENSE_FIND_REQUIRES, string.Empty));
+ }
+
+ // Refresh
+ metadata.Files = files;
+ }
+
+ ///
+ /// Determines the offsets for all records in the header of a package.
+ ///
+ ///
+ /// The package for which to generate the offsets.
+ ///
+ public void CalculateHeaderOffsets(RpmPackage package)
+ {
+ var metadata = new RpmMetadata(package);
+ metadata.ImmutableRegionSize = -1 * Marshal.SizeOf() * (package.Header.Records.Count + 1);
+
+ this.CalculateSectionOffsets(package.Header, k => (int)k);
+ }
+
+ ///
+ /// Determines the offsets for all records in the signature of a package.
+ ///
+ ///
+ /// The package for which to generate the offsets.
+ ///
+ public void CalculateSignatureOffsets(RpmPackage package)
+ {
+ var signature = new RpmSignature(package);
+ signature.ImmutableRegionSize = -1 * Marshal.SizeOf() * package.Signature.Records.Count;
+
+ this.CalculateSectionOffsets(package.Signature, k => (int)k);
+ }
+
+ ///
+ /// Gets a wich represents the entire header.
+ ///
+ ///
+ /// The package for which to get the header stream.
+ ///
+ ///
+ /// A wich represents the entire header.
+ ///
+ public MemoryStream GetHeaderStream(RpmPackage package)
+ {
+ MemoryStream stream = new MemoryStream();
+ this.WriteHeader(package, stream);
+ stream.Position = 0;
+ return stream;
+ }
+
+ ///
+ /// Writes the header to a .
+ ///
+ ///
+ /// The package for which to write the header.
+ ///
+ ///
+ /// The to which to write the header.
+ ///
+ public void WriteHeader(RpmPackage package, Stream targetStream)
+ {
+ RpmPackageWriter.WriteSection(targetStream, package.Header, DefaultOrder.Header);
+ }
+
+ ///
+ /// Gets a wich represents the entire signature.
+ ///
+ ///
+ /// The package for which to get the header stream.
+ ///
+ ///
+ /// A wich represents the entire signature.
+ ///
+ public MemoryStream GetSignatureStream(RpmPackage package)
+ {
+ MemoryStream stream = new MemoryStream();
+ this.WriteSignature(package, stream);
+ stream.Position = 0;
+ return stream;
+ }
+
+ ///
+ /// Writes the signature to a .
+ ///
+ ///
+ /// The package for which to write the signature.
+ ///
+ ///
+ /// The tow chih to write the package.
+ ///
+ public void WriteSignature(RpmPackage package, Stream targetStream)
+ {
+ RpmPackageWriter.WriteSection(targetStream, package.Signature, DefaultOrder.Signature);
+ }
+
+ ///
+ /// Calculates the signature for this package.
+ ///
+ ///
+ /// The package for whcih to calculate the signature.
+ ///
+ ///
+ /// The private key to use when signing packages.
+ ///
+ ///
+ /// The compressed payload.
+ ///
+ public void CalculateSignature(RpmPackage package, PgpPrivateKey privateKey, Stream compressedPayloadStream)
+ {
+ this.CalculateSignature(package, new PackageSigner(privateKey), compressedPayloadStream);
+ }
+
+ ///
+ /// Calculates the signature for this package.
+ ///
+ ///
+ /// The package for whcih to calculate the signature.
+ ///
+ ///
+ /// The signer to use.
+ ///
+ ///
+ /// The compressed payload.
+ ///
+ public void CalculateSignature(RpmPackage package, IPackageSigner signer, Stream compressedPayloadStream)
+ {
+ RpmSignature signature = new RpmSignature(package);
+
+ using (MemoryStream headerStream = this.GetHeaderStream(package))
+ using (ConcatStream headerAndPayloadStream = new ConcatStream(leaveOpen: true, streams: new Stream[] { headerStream, compressedPayloadStream }))
+ {
+ SHA1 sha = SHA1.Create();
+ signature.Sha1Hash = sha.ComputeHash(headerStream);
+
+ MD5 md5 = MD5.Create();
+ signature.MD5Hash = md5.ComputeHash(headerAndPayloadStream);
+
+ // Verify the PGP signatures
+ // 3 for the header
+ headerStream.Position = 0;
+ signature.HeaderPgpSignature = signer.Sign(headerStream);
+
+ headerAndPayloadStream.Position = 0;
+ signature.HeaderAndPayloadPgpSignature = signer.Sign(headerAndPayloadStream);
+
+ // Verify the signature size (header + compressed payload)
+ signature.HeaderAndPayloadSize = (int)headerAndPayloadStream.Length;
+ }
+
+ // Verify the payload size (header + uncompressed payload)
+ using (Stream payloadStream = RpmPayloadReader.GetDecompressedPayloadStream(package, compressedPayloadStream))
+ {
+ signature.UncompressedPayloadSize = (int)payloadStream.Length;
+ }
+ }
+
+ protected void CalculateSectionOffsets(Section section, Func getSortOrder)
+ {
+ var records = section.Records;
+ int offset = 0;
+
+ T immutableKey = default(T);
+
+ var sortedRecords = records.OrderBy(r => getSortOrder(r.Key)).ToArray();
+
+ foreach (var record in sortedRecords)
+ {
+ var indexTag = record.Key as IndexTag?;
+ var signatureTag = record.Key as SignatureTag?;
+
+ if (indexTag == IndexTag.RPMTAG_HEADERIMMUTABLE
+ || signatureTag == SignatureTag.RPMTAG_HEADERSIGNATURES)
+ {
+ immutableKey = record.Key;
+ continue;
+ }
+
+ var header = record.Value.Header;
+
+ // Determine the size
+ int size = 0;
+ int align = 0;
+
+ switch (header.Type)
+ {
+ case IndexType.RPM_BIN_TYPE:
+ case IndexType.RPM_CHAR_TYPE:
+ case IndexType.RPM_INT8_TYPE:
+ // These are all single-byte types
+ size = header.Count;
+ break;
+
+ case IndexType.RPM_I18NSTRING_TYPE:
+ case IndexType.RPM_STRING_ARRAY_TYPE:
+ foreach (var value in (IEnumerable)record.Value.Value)
+ {
+ if (value != null)
+ {
+ size += Encoding.UTF8.GetByteCount(value) + 1;
+ }
+ else
+ {
+ size += 1;
+ }
+ }
+
+ break;
+
+ case IndexType.RPM_STRING_TYPE:
+ size = Encoding.UTF8.GetByteCount((string)record.Value.Value) + 1;
+ break;
+
+ case IndexType.RPM_INT16_TYPE:
+ size = 2 * header.Count;
+ break;
+
+ case IndexType.RPM_INT32_TYPE:
+ size = 4 * header.Count;
+ align = 4;
+ break;
+
+ case IndexType.RPM_INT64_TYPE:
+ size = 8 * header.Count;
+ break;
+
+ case IndexType.RPM_NULL_TYPE:
+ size = 0;
+ break;
+ }
+
+ if (align != 0 && offset % align != 0)
+ {
+ offset += align - (offset % align);
+ }
+
+ header.Offset = offset;
+ offset += size;
+
+ record.Value.Header = header;
+ }
+
+ if (!object.Equals(immutableKey, default(T)))
+ {
+ var record = records[immutableKey];
+ var header = record.Header;
+ header.Offset = offset;
+ record.Header = header;
+
+ offset += Marshal.SizeOf();
+ }
+
+ // This is also a good time to refresh the header
+ section.Header = new RpmHeader()
+ {
+ HeaderSize = (uint)offset,
+ IndexCount = (uint)section.Records.Count,
+ Magic = 0x8eade801,
+ Reserved = 0
+ };
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmPackageReader.cs b/DebUOS/Packaging.Targets/Rpm/RpmPackageReader.cs
new file mode 100644
index 0000000..24d519f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmPackageReader.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ internal class RpmPackageReader
+ {
+ public static RpmPackage Read(Stream stream)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (!stream.CanSeek)
+ {
+ throw new ArgumentOutOfRangeException(nameof(stream), "A stream which backs a RPM package must be seekable");
+ }
+
+ RpmPackage package = new RpmPackage();
+ package.Stream = stream;
+
+ package.Lead = stream.ReadStruct();
+
+ package.Signature = ReadSection(stream, h => (SignatureTag)h.Tag);
+
+ if (stream.Position % 8 != 0)
+ {
+ stream.Position += 8 - (stream.Position % 8);
+ }
+
+ package.HeaderOffset = stream.Position;
+ package.Header = ReadSection(stream, h => (IndexTag)h.Tag);
+
+ package.PayloadOffset = stream.Position;
+
+ return package;
+ }
+
+ public static Section ReadSection(Stream stream, Func getTag)
+ {
+ Section section = new Section();
+
+ section.Header = stream.ReadStruct();
+
+ for (int i = 0; i < section.Header.IndexCount; i++)
+ {
+ var header = stream.ReadStruct();
+ section.Records.Add(
+ getTag(header),
+ new IndexRecord()
+ {
+ Header = header
+ });
+ }
+
+ var data = new byte[section.Header.HeaderSize];
+ stream.Read(data, 0, (int)section.Header.HeaderSize);
+
+ var headerSize = Marshal.SizeOf() * section.Records.Count;
+
+ byte[] int16Buffer = new byte[2];
+ byte[] int32Buffer = new byte[4];
+ byte[] int64Buffer = new byte[8];
+
+ // Read the data for all records
+ foreach (var record in section.Records.Values)
+ {
+ var offset = (int)record.Header.Offset;
+
+ switch (record.Header.Type)
+ {
+ case IndexType.RPM_CHAR_TYPE:
+ record.Value = (char)data[offset];
+ break;
+
+ case IndexType.RPM_INT8_TYPE:
+ record.Value = (byte)data[offset];
+ break;
+
+ case IndexType.RPM_INT16_TYPE:
+ Collection shortValues = new Collection();
+
+ for (int i = 0; i < record.Header.Count; i++)
+ {
+ Array.Copy(data, offset + i * sizeof(short), int16Buffer, 0, sizeof(short));
+ Array.Reverse(int16Buffer);
+ shortValues.Add(BitConverter.ToInt16(int16Buffer, 0));
+ }
+
+ if (shortValues.Count == 1)
+ {
+ record.Value = shortValues[0];
+ }
+ else
+ {
+ record.Value = shortValues;
+ }
+
+ break;
+
+ case IndexType.RPM_INT32_TYPE:
+ Collection intValues = new Collection();
+
+ for (int i = 0; i < record.Header.Count; i++)
+ {
+ Array.Copy(data, offset + i * sizeof(int), int32Buffer, 0, sizeof(int));
+ Array.Reverse(int32Buffer);
+ intValues.Add(BitConverter.ToInt32(int32Buffer, 0));
+ }
+
+ if (intValues.Count == 1)
+ {
+ record.Value = intValues[0];
+ }
+ else
+ {
+ record.Value = intValues;
+ }
+
+ break;
+
+ case IndexType.RPM_INT64_TYPE:
+ Collection longValues = new Collection();
+
+ for (int i = 0; i < record.Header.Count; i++)
+ {
+ Array.Copy(data, offset + i * sizeof(long), int64Buffer, 0, sizeof(long));
+ Array.Reverse(int64Buffer);
+ longValues.Add(BitConverter.ToInt64(int64Buffer, 0));
+ }
+
+ if (longValues.Count == 1)
+ {
+ record.Value = longValues[0];
+ }
+ else
+ {
+ record.Value = longValues;
+ }
+
+ break;
+
+ case IndexType.RPM_STRING_TYPE:
+ record.Value = ReadNullTerminatedString(ref offset, data);
+ break;
+
+ case IndexType.RPM_I18NSTRING_TYPE:
+ case IndexType.RPM_STRING_ARRAY_TYPE:
+ Collection strings = new Collection();
+
+ for (int i = 0; i < record.Header.Count; i++)
+ {
+ strings.Add(ReadNullTerminatedString(ref offset, data));
+ }
+
+ record.Value = strings;
+ break;
+
+ case IndexType.RPM_BIN_TYPE:
+ byte[] value = new byte[record.Header.Count];
+ Array.Copy(data, offset, value, 0, (int)record.Header.Count);
+ record.Value = value;
+ break;
+
+ case IndexType.RPM_NULL_TYPE:
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ return section;
+ }
+
+ private static string ReadNullTerminatedString(ref int offset, byte[] data)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+
+ int stringLength = 0;
+
+ while (offset + stringLength < data.Length && data[offset + stringLength] != 0)
+ {
+ stringLength++;
+ }
+
+ string value;
+
+ if (stringLength > 0)
+ {
+ value = Encoding.UTF8.GetString(data, offset, stringLength);
+ }
+ else
+ {
+ value = string.Empty;
+ }
+
+ offset += stringLength;
+ offset += 1;
+ return value;
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmPackageWriter.cs b/DebUOS/Packaging.Targets/Rpm/RpmPackageWriter.cs
new file mode 100644
index 0000000..f966462
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmPackageWriter.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Provides methods which support writing a to a .
+ ///
+ internal class RpmPackageWriter
+ {
+ ///
+ /// Writes a to a .
+ ///
+ ///
+ /// The to which to write the .
+ ///
+ ///
+ /// The to write to the stream.
+ ///
+ public static void Write(Stream stream, RpmPackage package)
+ {
+ if (package == null)
+ {
+ throw new ArgumentNullException(nameof(package));
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ stream.WriteStruct(package.Lead);
+
+ WriteSection(stream, package.Signature, DefaultOrder.Signature);
+ WriteSection(stream, package.Header, DefaultOrder.Header);
+ }
+
+ public static void WriteSection(Stream stream, Section section, List order)
+ {
+ while (stream.Position % 8 != 0)
+ {
+ stream.WriteByte(0);
+ }
+
+ stream.WriteStruct(section.Header);
+
+ // First write out all records in the pre-defined order
+ foreach (var record in order)
+ {
+ if (section.Records.ContainsKey(record))
+ {
+ stream.WriteStruct(section.Records[record].Header);
+ }
+ }
+
+ // Then write out any record which may be new.
+ foreach (var record in section.Records)
+ {
+ if (!order.Contains(record.Key))
+ {
+ stream.WriteStruct(record.Value.Header);
+ }
+ }
+
+ // Write the data for all records
+ long start = stream.Position;
+
+ var records = section.Records.Values.OrderBy(v => v.Header.Offset).ToArray();
+ foreach (var record in records)
+ {
+ long actualOffset = stream.Position - start;
+
+ if (actualOffset > record.Header.Offset)
+ {
+ throw new InvalidOperationException();
+ }
+
+ for (int i = 0; i < record.Header.Offset - actualOffset; i++)
+ {
+ stream.WriteByte(0);
+ }
+
+ switch (record.Header.Type)
+ {
+ case IndexType.RPM_CHAR_TYPE:
+ stream.WriteByte((byte)record.Value);
+ break;
+
+ case IndexType.RPM_INT8_TYPE:
+ stream.WriteByte((byte)record.Value);
+ break;
+
+ case IndexType.RPM_INT16_TYPE:
+ if (record.Value is short)
+ {
+ stream.WriteBE((short)record.Value);
+ }
+ else
+ {
+ var shortValues = (IEnumerable)record.Value;
+
+ foreach (var shortValue in shortValues)
+ {
+ stream.WriteBE(shortValue);
+ }
+ }
+
+ break;
+
+ case IndexType.RPM_INT32_TYPE:
+ if (record.Value is int)
+ {
+ stream.WriteBE((int)record.Value);
+ }
+ else
+ {
+ var shortValues = (IEnumerable)record.Value;
+
+ foreach (var shortValue in shortValues)
+ {
+ stream.WriteBE(shortValue);
+ }
+ }
+
+ break;
+
+ case IndexType.RPM_INT64_TYPE:
+ if (record.Value is long)
+ {
+ stream.WriteBE((long)record.Value);
+ }
+ else
+ {
+ var longValues = (IEnumerable)record.Value;
+
+ foreach (var shortValue in longValues)
+ {
+ stream.WriteBE(shortValue);
+ }
+ }
+
+ break;
+
+ case IndexType.RPM_STRING_TYPE:
+ WriteNullTerminatedString(stream, (string)record.Value);
+ break;
+
+ case IndexType.RPM_I18NSTRING_TYPE:
+ case IndexType.RPM_STRING_ARRAY_TYPE:
+ Collection strings = (Collection)record.Value;
+
+ foreach (var s in strings)
+ {
+ WriteNullTerminatedString(stream, s);
+ }
+
+ break;
+
+ case IndexType.RPM_BIN_TYPE:
+ var value = (IEnumerable)record.Value;
+ var array = value.ToArray();
+ stream.Write(array, 0, array.Length);
+ break;
+
+ case IndexType.RPM_NULL_TYPE:
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+
+ private static void WriteNullTerminatedString(Stream stream, string value)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (value == null)
+ {
+ stream.WriteByte(0x00);
+ }
+ else
+ {
+ var length = Encoding.UTF8.GetByteCount(value) + 1;
+ byte[] data = new byte[length];
+ Encoding.UTF8.GetBytes(value, 0, value.Length, data, 0);
+
+ stream.Write(data, 0, data.Length);
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmPayloadReader.cs b/DebUOS/Packaging.Targets/Rpm/RpmPayloadReader.cs
new file mode 100644
index 0000000..a2ad637
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmPayloadReader.cs
@@ -0,0 +1,125 @@
+using Packaging.Targets.IO;
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Allows interacting with the payload of a RPM package.
+ ///
+ internal class RpmPayloadReader
+ {
+ public static Collection Read(RpmPackage package)
+ {
+ if (package == null)
+ {
+ throw new ArgumentNullException(nameof(package));
+ }
+
+ Collection names = new Collection();
+
+ using (var payloadDecompressedStream = GetDecompressedPayloadStream(package))
+ using (CpioFile file = new CpioFile(payloadDecompressedStream, leaveOpen: true))
+ {
+ while (file.Read())
+ {
+ names.Add(file.FileName);
+
+ using (Stream stream = file.Open())
+ {
+ byte[] data = new byte[stream.Length];
+ stream.Read(data, 0, data.Length);
+ }
+ }
+ }
+
+ return names;
+ }
+
+ ///
+ /// Gets the compressed payload for a package.
+ ///
+ ///
+ /// The package for which to get the compressed payload.
+ ///
+ ///
+ /// A which contains the compressed payload.
+ ///
+ public static Stream GetCompressedPayloadStream(RpmPackage package)
+ {
+ if (package == null)
+ {
+ throw new ArgumentNullException(nameof(package));
+ }
+
+ SubStream compressedPayloadStream = new SubStream(
+ stream: package.Stream,
+ offset: package.PayloadOffset,
+ length: package.Stream.Length - package.PayloadOffset,
+ leaveParentOpen: true,
+ readOnly: true);
+
+ return compressedPayloadStream;
+ }
+
+ ///
+ /// Gets a which allows reading the decompressed payload data.
+ ///
+ ///
+ /// The package for which to read the decompressed payload data.
+ ///
+ ///
+ /// A which allows reading the decompressed payload data.
+ ///
+ public static Stream GetDecompressedPayloadStream(RpmPackage package)
+ {
+ if (package == null)
+ {
+ throw new ArgumentNullException(nameof(package));
+ }
+
+ var compressedPayloadStream = GetCompressedPayloadStream(package);
+
+ return GetDecompressedPayloadStream(package, compressedPayloadStream);
+ }
+
+ ///
+ /// Gets a which allows reading the decompressed payload data.
+ ///
+ ///
+ /// The package for which to read the decompressed payload data.
+ ///
+ ///
+ /// The compressed payload stream.
+ ///
+ ///
+ /// A which allows reading the decompressed payload data.
+ ///
+ public static Stream GetDecompressedPayloadStream(RpmPackage package, Stream compressedPayloadStream)
+ {
+ var compressor = (string)package.Header.Records[IndexTag.RPMTAG_PAYLOADCOMPRESSOR].Value;
+ var payloadDecompressedStream = GetPayloadDecompressor(compressedPayloadStream, compressor);
+
+ return payloadDecompressedStream;
+ }
+
+ private static Stream GetPayloadDecompressor(Stream payloadStream, string compressor)
+ {
+ if (string.Equals(compressor, "gzip"))
+ {
+ return new GZipStream(payloadStream, CompressionMode.Decompress, leaveOpen: false);
+ }
+ else if (string.Equals(compressor, "xz"))
+ {
+ return new XZInputStream(payloadStream);
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(compressor));
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmSense.cs b/DebUOS/Packaging.Targets/Rpm/RpmSense.cs
new file mode 100644
index 0000000..3eed735
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmSense.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ [Flags]
+ internal enum RpmSense
+ {
+ RPMSENSE_ANY = 0,
+ RPMSENSE_LESS = 1 << 1,
+ RPMSENSE_GREATER = 1 << 2,
+ RPMSENSE_EQUAL = 1 << 3,
+ RPMSENSE_POSTTRANS = 1 << 5, /*!< %posttrans dependency */
+ RPMSENSE_PREREQ = 1 << 6, /* legacy prereq dependency */
+ RPMSENSE_PRETRANS = 1 << 7, /*!< Pre-transaction dependency. */
+ RPMSENSE_INTERP = 1 << 8, /*!< Interpreter used by scriptlet. */
+ RPMSENSE_SCRIPT_PRE = 1 << 9, /*!< %pre dependency. */
+ RPMSENSE_SCRIPT_POST = 1 << 10, /*!< %post dependency. */
+ RPMSENSE_SCRIPT_PREUN = 1 << 11, /*!< %preun dependency. */
+ RPMSENSE_SCRIPT_POSTUN = 1 << 12, /*!< %postun dependency. */
+ RPMSENSE_SCRIPT_VERIFY = 1 << 13, /*!< %verify dependency. */
+ RPMSENSE_FIND_REQUIRES = 1 << 14, /*!< find-requires generated dependency. */
+ RPMSENSE_FIND_PROVIDES = 1 << 15, /*!< find-provides generated dependency. */
+
+ RPMSENSE_TRIGGERIN = 1 << 16, /*!< %triggerin dependency. */
+ RPMSENSE_TRIGGERUN = 1 << 17, /*!< %triggerun dependency. */
+ RPMSENSE_TRIGGERPOSTUN = 1 << 18, /*!< %triggerpostun dependency. */
+ RPMSENSE_MISSINGOK = 1 << 19, /*!< suggests/enhances hint. */
+ RPMSENSE_RPMLIB = 1 << 24, /*!< rpmlib(feature) dependency. */
+ RPMSENSE_TRIGGERPREIN = 1 << 25, /*!< %triggerprein dependency. */
+ RPMSENSE_KEYRING = 1 << 26,
+ RPMSENSE_CONFIG = 1 << 28
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmSignature.cs b/DebUOS/Packaging.Targets/Rpm/RpmSignature.cs
new file mode 100644
index 0000000..b291999
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmSignature.cs
@@ -0,0 +1,372 @@
+using Org.BouncyCastle.Bcpg.OpenPgp;
+using Packaging.Targets.IO;
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Enables interacting with the signature of a .
+ ///
+ internal class RpmSignature
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The package for which to access the signature.
+ ///
+ public RpmSignature(RpmPackage package)
+ {
+ if (package == null)
+ {
+ throw new ArgumentNullException(nameof(package));
+ }
+
+ this.Package = package;
+ }
+
+ ///
+ /// Gets the package for which the signature is being inspected.
+ ///
+ public RpmPackage Package
+ {
+ get;
+ private set;
+ }
+
+ ///
+ /// Gets or sets the length of the immutable region, starting from the position of the
+ /// record. This record should be the last record in the signature block, and the value is negative, indicating the previous
+ /// blocks are considered immutable.
+ ///
+ public int ImmutableRegionSize
+ {
+ get
+ {
+ // For more information about immutable regions, see:
+ // http://ftp.rpm.org/api/4.4.2.2/hregions.html
+ // https://dentrassi.de/2016/04/15/writing-rpm-files-in-plain-java/
+ // https://blog.bethselamin.de/posts/argh-pm.html
+ // For now, we're always assuming the entire header and the entire signature is immutable
+ var immutableSignatureRegion = (byte[])this.Package.Signature.Records[SignatureTag.RPMTAG_HEADERSIGNATURES].Value;
+
+ using (MemoryStream s = new MemoryStream(immutableSignatureRegion))
+ {
+ var h = s.ReadStruct();
+ return h.Offset;
+ }
+ }
+
+ set
+ {
+ IndexHeader header = default(IndexHeader);
+ header.Offset = value;
+ header.Count = 0x10;
+ header.Type = IndexType.RPM_BIN_TYPE;
+ header.Tag = (uint)SignatureTag.RPMTAG_HEADERSIGNATURES;
+
+ byte[] data;
+
+ using (MemoryStream s = new MemoryStream())
+ {
+ s.WriteStruct(header);
+ data = s.ToArray();
+ }
+
+ this.SetByteArray(SignatureTag.RPMTAG_HEADERSIGNATURES, data);
+ }
+ }
+
+ ///
+ /// Gets or sets the SHA1 hash of the header section.
+ ///
+ public byte[] Sha1Hash
+ {
+ get { return StringToByteArray((string)this.Package.Signature.Records[SignatureTag.RPMSIGTAG_SHA1].Value); }
+ set { this.SetString(SignatureTag.RPMSIGTAG_SHA1, BitConverter.ToString(value).Replace("-", string.Empty).ToLower()); }
+ }
+
+ ///
+ /// Gets or sets the MD5 hash of the header section and the compressed payload.
+ ///
+ public byte[] MD5Hash
+ {
+ get { return (byte[])this.Package.Signature.Records[SignatureTag.RPMSIGTAG_MD5].Value; }
+ set { this.SetByteArray(SignatureTag.RPMSIGTAG_MD5, value); }
+ }
+
+ ///
+ /// Gets or sets the of the header section.
+ ///
+ public PgpSignature HeaderPgpSignature
+ {
+ get { return this.GetPgpSignature(SignatureTag.RPMSIGTAG_RSA); }
+ set { this.SetPgpSignature(SignatureTag.RPMSIGTAG_RSA, value); }
+ }
+
+ ///
+ /// Gets or sets the of the header section and compressed payload.
+ ///
+ public PgpSignature HeaderAndPayloadPgpSignature
+ {
+ get { return this.GetPgpSignature(SignatureTag.RPMSIGTAG_PGP); }
+ set { this.SetPgpSignature(SignatureTag.RPMSIGTAG_PGP, value); }
+ }
+
+ ///
+ /// Gets or sets the combined size of the header section and the compressed payload.
+ ///
+ public int HeaderAndPayloadSize
+ {
+ get { return (int)this.Package.Signature.Records[SignatureTag.RPMSIGTAG_SIZE].Value; }
+ set { this.SetInt(SignatureTag.RPMSIGTAG_SIZE, value); }
+ }
+
+ ///
+ /// Gets or sets the size of the uncompressed payload.
+ ///
+ public int UncompressedPayloadSize
+ {
+ get { return (int)this.Package.Signature.Records[SignatureTag.RPMSIGTAG_PAYLOADSIZE].Value; }
+ set { this.SetInt(SignatureTag.RPMSIGTAG_PAYLOADSIZE, value); }
+ }
+
+ ///
+ /// Verifys the signature of a RPM package.
+ ///
+ ///
+ /// A which contains the public key used to verify the data.
+ ///
+ ///
+ /// if the verification was successful; otherwise, .
+ ///
+ public bool Verify(Stream pgpPublicKeyStream)
+ {
+ var bundle = new PgpPublicKeyRingBundle(PgpUtilities.GetDecoderStream(pgpPublicKeyStream));
+ var publicKeyRings = bundle.GetKeyRings().OfType();
+ var publicKeys = publicKeyRings.SelectMany(x => x.GetPublicKeys().OfType());
+ var publicKey = publicKeys.FirstOrDefault();
+
+ return this.Verify(publicKey);
+ }
+
+ public bool Verify(PgpPublicKey pgpPublicKey)
+ {
+ // 1 Verify the header signature immutable block: make sure all records are marked as immutable
+ var immutableRegionSize = this.ImmutableRegionSize;
+ var immutableEntryCount = -immutableRegionSize / Marshal.SizeOf();
+
+ if (this.Package.Signature.Records.Count != immutableEntryCount)
+ {
+ return false;
+ }
+
+ // Store the header data and the header + payload data in substreams. This enables us to calculate hashes and
+ // verify signatures using these data ranges.
+ using (Stream headerStream = new SubStream(
+ this.Package.Stream,
+ this.Package.HeaderOffset,
+ this.Package.PayloadOffset - this.Package.HeaderOffset,
+ leaveParentOpen: true,
+ readOnly: true))
+ using (Stream headerAndPayloadStream = new SubStream(
+ this.Package.Stream,
+ this.Package.HeaderOffset,
+ this.Package.Stream.Length - this.Package.HeaderOffset,
+ leaveParentOpen: true,
+ readOnly: true))
+ {
+ // Verify the SHA1 hash. This one covers the header block
+ SHA1 sha = SHA1.Create();
+ var calculatedShaValue = sha.ComputeHash(headerStream);
+ var actualShaValue = this.Sha1Hash;
+
+ if (!calculatedShaValue.SequenceEqual(actualShaValue))
+ {
+ return false;
+ }
+
+ // Verify the MD5 hash. This one covers the header and the payload block
+ MD5 md5 = MD5.Create();
+ var calculatedMd5Value = md5.ComputeHash(headerAndPayloadStream);
+ var actualMd5Value = this.MD5Hash;
+
+ if (!calculatedMd5Value.SequenceEqual(actualMd5Value))
+ {
+ return false;
+ }
+
+ // Verify the PGP signatures
+ // 3 for the header
+ var headerPgpSignature = this.HeaderPgpSignature;
+ headerStream.Position = 0;
+
+ if (!PgpSigner.VerifySignature(headerPgpSignature, pgpPublicKey, headerStream))
+ {
+ return false;
+ }
+
+ var headerAndPayloadPgpSignature = this.HeaderAndPayloadPgpSignature;
+ headerAndPayloadStream.Position = 0;
+
+ if (!PgpSigner.VerifySignature(headerAndPayloadPgpSignature, pgpPublicKey, headerAndPayloadStream))
+ {
+ return false;
+ }
+ }
+
+ // Verify the signature size (header + compressed payload)
+ var headerSize = this.HeaderAndPayloadSize;
+
+ if (headerSize != this.Package.Stream.Length - this.Package.HeaderOffset)
+ {
+ return false;
+ }
+
+ // Verify the payload size (header + uncompressed payload)
+ var expectedDecompressedPayloadSize = this.UncompressedPayloadSize;
+
+ long actualDecompressedPayloadLength = 0;
+
+ using (Stream payloadStream = RpmPayloadReader.GetDecompressedPayloadStream(this.Package))
+ {
+ actualDecompressedPayloadLength = payloadStream.Length;
+ }
+
+ if (expectedDecompressedPayloadSize != actualDecompressedPayloadLength)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Converst a hexadecimal string to a array.
+ ///
+ ///
+ /// A containing the hexadecimal representation of the value.
+ ///
+ ///
+ /// A byte array representing the same value.
+ ///
+ internal static byte[] StringToByteArray(string hex)
+ {
+ int numberChars = hex.Length;
+ byte[] bytes = new byte[numberChars / 2];
+
+ for (int i = 0; i < numberChars; i += 2)
+ {
+ bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
+ }
+
+ return bytes;
+ }
+
+ protected void SetInt(SignatureTag tag, int value)
+ {
+ this.SetSingleValue(tag, IndexType.RPM_INT32_TYPE, value);
+ }
+
+ protected void SetString(SignatureTag tag, string value)
+ {
+ this.SetSingleValue(tag, IndexType.RPM_STRING_TYPE, value);
+ }
+
+ protected void SetByteArray(SignatureTag tag, byte[] value)
+ {
+ this.SetValue(tag, IndexType.RPM_BIN_TYPE, value);
+ }
+
+ protected void SetValue(SignatureTag tag, IndexType type, T[] value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ var collection = new Collection(value);
+
+ IndexRecord record = new IndexRecord()
+ {
+ Header = new IndexHeader()
+ {
+ Count = collection.Count,
+ Offset = 0,
+ Tag = (uint)tag,
+ Type = type
+ },
+ Value = collection
+ };
+
+ if (!this.Package.Signature.Records.ContainsKey(tag))
+ {
+ this.Package.Signature.Records.Add(tag, record);
+ }
+ else
+ {
+ this.Package.Signature.Records[tag] = record;
+ }
+ }
+
+ protected void SetSingleValue(SignatureTag tag, IndexType type, T value)
+ {
+ IndexRecord record = new IndexRecord()
+ {
+ Header = new IndexHeader()
+ {
+ Count = 1,
+ Offset = 0,
+ Tag = (uint)tag,
+ Type = type
+ },
+ Value = value
+ };
+
+ if (!this.Package.Signature.Records.ContainsKey(tag))
+ {
+ this.Package.Signature.Records.Add(tag, record);
+ }
+ else
+ {
+ this.Package.Signature.Records[tag] = record;
+ }
+ }
+
+ ///
+ /// Gets a PGP signature.
+ ///
+ ///
+ /// The tag which contains the PGP signature.
+ ///
+ ///
+ /// A object which represents the PGP signature.
+ ///
+ private PgpSignature GetPgpSignature(SignatureTag tag)
+ {
+ byte[] value = (byte[])this.Package.Signature.Records[tag].Value;
+
+ using (MemoryStream stream = new MemoryStream(value))
+ using (var signatureStream = PgpUtilities.GetDecoderStream(stream))
+ {
+ PgpObjectFactory pgpFactory = new PgpObjectFactory(signatureStream);
+ PgpSignatureList signatureList = (PgpSignatureList)pgpFactory.NextPgpObject();
+ PgpSignature signature = signatureList[0];
+ return signature;
+ }
+ }
+
+ private void SetPgpSignature(SignatureTag tag, PgpSignature signature)
+ {
+ PgpSignatureList signatureList = new PgpSignatureList(signature);
+ byte[] value = signature.GetEncoded();
+
+ this.SetByteArray(tag, value);
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/RpmVerifyFlags.cs b/DebUOS/Packaging.Targets/Rpm/RpmVerifyFlags.cs
new file mode 100644
index 0000000..01a12f0
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/RpmVerifyFlags.cs
@@ -0,0 +1,26 @@
+namespace Packaging.Targets.Rpm
+{
+ ///
+ /// Specifies how a file should be verified.
+ ///
+ internal enum RpmVerifyFlags
+ {
+ RPMVERIFY_NONE = 0,
+ RPMVERIFY_MD5 = 1 << 0,
+ RPMVERIFY_FILEDIGEST = 1 << 0,
+ RPMVERIFY_FILESIZE = 1 << 1,
+ RPMVERIFY_LINKTO = 1 << 2,
+ RPMVERIFY_USER = 1 << 3,
+ RPMVERIFY_GROUP = 1 << 4,
+ RPMVERIFY_MTIME = 1 << 5,
+ RPMVERIFY_MODE = 1 << 6,
+ RPMVERIFY_RDEV = 1 << 7,
+ RPMVERIFY_CAPS = 1 << 8,
+ RPMVERIFY_CONTEXTS = 1 << 15,
+ RPMVERIFY_READLINKFAIL = 1 << 28,
+ RPMVERIFY_READFAIL = 1 << 29,
+ RPMVERIFY_LSTATFAIL = 1 << 30,
+ RPMVERIFY_LGETFILECONFAIL = 1 << 31,
+ RPMVERIFY_ALL = ~RPMVERIFY_NONE
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/Section.cs b/DebUOS/Packaging.Targets/Rpm/Section.cs
new file mode 100644
index 0000000..108a93f
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/Section.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Packaging.Targets.Rpm
+{
+ internal class Section
+ {
+ public RpmHeader Header
+ {
+ get;
+ set;
+ }
+
+ public Dictionary Records
+ { get; set; } = new Dictionary();
+ }
+}
diff --git a/DebUOS/Packaging.Targets/Rpm/SignatureTag.cs b/DebUOS/Packaging.Targets/Rpm/SignatureTag.cs
new file mode 100644
index 0000000..b791bec
--- /dev/null
+++ b/DebUOS/Packaging.Targets/Rpm/SignatureTag.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Packaging.Targets.Rpm
+{
+ internal enum SignatureTag
+ {
+ ///
+ /// The signature tag differentiates a signature header from a metadata header, and identifies the original contents of the signature header.
+ ///
+ RPMTAG_HEADERSIGNATURES = 62,
+
+ ///
+ /// This tag specifies the combined size of the Header and Payload sections.
+ ///
+ RPMSIGTAG_SIZE = 1000,
+
+ ///
+ /// This tag specifies the uncompressed size of the Payload archive, including the cpio headers.
+ ///
+ RPMSIGTAG_PAYLOADSIZE = 1007,
+
+ ///
+ /// This index contains the SHA1 checksum of the entire Header Section, including the Header Record, Index Records and Header store.
+ ///
+ RPMSIGTAG_SHA1 = 269,
+
+ ///
+ /// This tag specifies the 128-bit MD5 checksum of the combined Header and Archive sections.
+ ///
+ RPMSIGTAG_MD5 = 1004,
+
+ ///
+ /// The tag contains the DSA signature of the Header section. The data is formatted as a Version 3 Signature Packet as specified in
+ /// RFC 2440: OpenPGP Message Format. If this tag is present, then the SIGTAG_GPG tag shall also be present.
+ ///
+ RPMSIGTAG_DSA = 267,
+
+ ///
+ /// The tag contains the RSA signature of the Header section.The data is formatted as a Version 3 Signature Packet as specified in
+ /// RFC 2440: OpenPGP Message Format. If this tag is present, then the SIGTAG_PGP shall also be present.
+ ///
+ RPMSIGTAG_RSA = 268,
+
+ ///
+ /// This tag specifies the RSA signature of the combined Header and Payload sections. The data is formatted as a Version 3 Signature Packet
+ /// as specified in RFC 2440: OpenPGP Message Format.
+ ///
+ RPMSIGTAG_PGP = 1002,
+
+ ///
+ /// The tag contains the DSA signature of the combined Header and Payload sections. The data is formatted as a Version 3 Signature Packet
+ /// as specified in RFC 2440: OpenPGP Message Format.
+ ///
+ RPMSIGTAG_GPG = 1005,
+ }
+}
diff --git a/DebUOS/Packaging.Targets/RuntimeIdentifiers.cs b/DebUOS/Packaging.Targets/RuntimeIdentifiers.cs
new file mode 100644
index 0000000..ff64a77
--- /dev/null
+++ b/DebUOS/Packaging.Targets/RuntimeIdentifiers.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets
+{
+ ///
+ /// Provides methods for working with .NET Runtime Identifiers.
+ ///
+ public static class RuntimeIdentifiers
+ {
+ ///
+ /// Parses a runtime identifier.
+ ///
+ ///
+ /// The runtime identifier to parse.
+ ///
+ ///
+ /// The operating system name embedded in the runtime identifier.
+ ///
+ ///
+ /// The operating system version embedded in the runtime identifier.
+ ///
+ ///
+ /// The processor architecture embedded in the runtime identifier.
+ ///
+ ///
+ /// Any additional qualifiers (such as aot) embedded in the runtime identifier.
+ ///
+ public static void ParseRuntimeId(string runtimeId, out string osName, out string version, out Architecture? architecture, out string qualifiers)
+ {
+ osName = null;
+ version = null;
+ architecture = null;
+ qualifiers = null;
+
+ if (string.IsNullOrEmpty(runtimeId))
+ {
+ return;
+ }
+
+ // We use the following convention in all newly-defined RIDs. Some RIDs (win7-x64, win8-x64) predate this convention and don't follow it, but all new RIDs should follow it.
+ // [os name].[version]-[architecture]-[additional qualifiers]
+ // See https://github.com/dotnet/corefx/blob/master/pkg/Microsoft.NETCore.Platforms/readme.md#naming-convention
+ int versionSeparator = runtimeId.IndexOf('.');
+ if (versionSeparator >= 0)
+ {
+ osName = runtimeId.Substring(0, versionSeparator);
+ }
+ else
+ {
+ osName = null;
+ }
+
+ // As a special case, for "linux-musl", we consider "-musl" to be part of the osName
+ int muslSeparator = runtimeId.IndexOf("-musl", versionSeparator + 1);
+ int architectureSeparator = runtimeId.IndexOf('-', muslSeparator + 1);
+ if (architectureSeparator >= 0)
+ {
+ if (versionSeparator >= 0)
+ {
+ version = runtimeId.Substring(versionSeparator + 1, architectureSeparator - versionSeparator - 1);
+ }
+ else
+ {
+ osName = runtimeId.Substring(0, architectureSeparator);
+ version = null;
+ }
+
+ qualifiers = runtimeId.Substring(architectureSeparator + 1);
+ }
+ else
+ {
+ if (versionSeparator >= 0)
+ {
+ version = runtimeId.Substring(versionSeparator + 1);
+ }
+ else
+ {
+ osName = runtimeId;
+ version = null;
+ }
+
+ qualifiers = null;
+ }
+
+ // As a special case, os names like win7, win81 and win10 are processed separately
+ if (osName.StartsWith("win") && osName.Length > 3)
+ {
+ version = osName.Substring(3);
+ osName = "win";
+ }
+
+ architecture = null;
+
+ if (!string.IsNullOrEmpty(qualifiers))
+ {
+ string architectureString = qualifiers;
+ qualifiers = null;
+
+ architectureSeparator = architectureString.IndexOf('-');
+ if (architectureSeparator > 0)
+ {
+ qualifiers = architectureString.Substring(architectureSeparator + 1);
+ architectureString = architectureString.Substring(0, architectureSeparator);
+ }
+
+ // As a special case, "armel" is mapped to "arm" for now
+ if (architectureString == "armel")
+ {
+ architectureString = "arm";
+ }
+
+ if (Enum.TryParse(architectureString, ignoreCase: true, out Architecture parsedArchitecture))
+ {
+ architecture = parsedArchitecture;
+ }
+ else
+ {
+ // E.g. in the case of "win-aot"
+ qualifiers = architectureString;
+ }
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/StreamExtensions.cs b/DebUOS/Packaging.Targets/StreamExtensions.cs
new file mode 100644
index 0000000..5fa2e4d
--- /dev/null
+++ b/DebUOS/Packaging.Targets/StreamExtensions.cs
@@ -0,0 +1,189 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+namespace Packaging.Targets
+{
+ ///
+ /// Provides extension methods for the class.
+ ///
+ internal static class StreamExtensions
+ {
+ ///
+ /// Reads a struct from the stream.
+ ///
+ ///
+ /// The type of the struct to read.
+ ///
+ ///
+ /// The to read the struct from.
+ ///
+ ///
+ /// A new struct, with the data read
+ /// from the stream.
+ ///
+ public static T ReadStruct(this Stream stream)
+ where T : struct
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ var size = Marshal.SizeOf();
+
+ var data = new byte[size];
+ var totalRead = 0;
+
+ while (totalRead < size)
+ {
+ var read = stream.Read(data, totalRead, size);
+
+ if (read == 0)
+ {
+ break;
+ }
+
+ totalRead += read;
+ }
+
+ if (totalRead < size)
+ {
+ throw new InvalidOperationException("Not enough data");
+ }
+
+ // Convert from network byte order (big endian) to little endian.
+ RespectEndianness(data);
+
+ var pinnedData = GCHandle.Alloc(data, GCHandleType.Pinned);
+
+ try
+ {
+ var ptr = pinnedData.AddrOfPinnedObject();
+ return Marshal.PtrToStructure(ptr);
+ }
+ finally
+ {
+ pinnedData.Free();
+ }
+ }
+
+ ///
+ /// Writes a struct to a stream.
+ ///
+ ///
+ /// The type of the struct to write.
+ ///
+ ///
+ /// The stream to which to write the struct.
+ ///
+ ///
+ /// The struct to write to the stram.
+ ///
+ public static int WriteStruct(this Stream stream, T data)
+ where T : struct
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ byte[] bytes = new byte[Marshal.SizeOf()];
+
+ GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+
+ try
+ {
+ Marshal.StructureToPtr(data, handle.AddrOfPinnedObject(), true);
+ }
+ finally
+ {
+ handle.Free();
+ }
+
+ RespectEndianness(bytes);
+
+ stream.Write(bytes, 0, bytes.Length);
+ return bytes.Length;
+ }
+
+ ///
+ /// Writes a to a using big-endian encoding.
+ ///
+ ///
+ /// The to which to write the value.
+ ///
+ ///
+ /// The value to write to the stream.
+ ///
+ public static void WriteBE(this Stream stream, short value)
+ {
+ var data = BitConverter.GetBytes(value);
+ Array.Reverse(data);
+ stream.Write(data, 0, data.Length);
+ }
+
+ ///
+ /// Writes a to a using big-endian encoding.
+ ///
+ ///
+ /// The to which to write the value.
+ ///
+ ///
+ /// The value to write to the stream.
+ ///
+ public static void WriteBE(this Stream stream, int value)
+ {
+ var data = BitConverter.GetBytes(value);
+ Array.Reverse(data);
+ stream.Write(data, 0, data.Length);
+ }
+
+ ///
+ /// Writes a to a using big-endian encoding.
+ ///
+ ///
+ /// The to which to write the value.
+ ///
+ ///
+ /// The value to write to the stream.
+ ///
+ public static void WriteBE(this Stream stream, long value)
+ {
+ var data = BitConverter.GetBytes(value);
+ Array.Reverse(data);
+ stream.Write(data, 0, data.Length);
+ }
+
+ private static void RespectEndianness(byte[] data)
+ {
+ foreach (var field in typeof(T).GetTypeInfo().DeclaredFields)
+ {
+ int length = 0;
+
+ var type = field.FieldType;
+
+ if (type.GetTypeInfo().IsEnum)
+ {
+ type = Enum.GetUnderlyingType(type);
+ }
+
+ if (type == typeof(short) || type == typeof(ushort))
+ {
+ length = 2;
+ }
+ else if (type == typeof(int) || type == typeof(uint))
+ {
+ length = 4;
+ }
+
+ if (length > 0)
+ {
+ var offset = Marshal.OffsetOf(field.Name).ToInt32();
+ Array.Reverse(data, offset, length);
+ }
+ }
+ }
+ }
+}
diff --git a/DebUOS/Packaging.Targets/runtimes/win7-x64/native/lzma.dll b/DebUOS/Packaging.Targets/runtimes/win7-x64/native/lzma.dll
new file mode 100644
index 0000000..2a2e4c7
Binary files /dev/null and b/DebUOS/Packaging.Targets/runtimes/win7-x64/native/lzma.dll differ
diff --git a/DebUOS/README.md b/DebUOS/README.md
new file mode 100644
index 0000000..bbbb66b
--- /dev/null
+++ b/DebUOS/README.md
@@ -0,0 +1,137 @@
+# 制作符合 UOS 规范的 deb 安装包的工具
+
+## 使用方法
+
+有以下两个使用方式,任选其一即可
+
+### 命令行工具方式
+
+使用以下命令进行更新或安装工具:
+
+```
+dotnet tool update -g Packaging.DebUOS.Tool
+```
+
+将已经准备好的符合 UOS 安装包文件组织规范的文件夹打包为 deb 安装包:
+
+```
+dotnet dpkg-debuos -b C:\lindexi\DebPacking -o C:\lindexi\UOS\Foo.deb
+```
+
+以上的 `C:\lindexi\DebPacking` 为已准备好的符合 UOS 安装包文件组织规范的文件夹,以上的 `C:\lindexi\UOS\Foo.deb` 为打包输出的文件。其中 `-o` 指定打包输出文件参数可以忽略,如忽略此参数,则将会在打包文件夹输出 deb 安装包
+
+使用命令行工具比较适合创建构建更为复杂的 deb 安装包,可以有更强的定制化,适合对 UOS 安装包规范较熟悉的开发者使用。或者是作为 `dpkg-deb` 工具在 Windows 上的替代品
+
+### 配合 csproj 的 NuGet 包方式
+
+请通过 NuGet 管理器或采用如下代码编辑 csproj 文件安装 `Packaging.DebUOS` 库
+
+```xml
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+```
+
+接着添加一些必要的配置参数,比如用来配置 UOS 的 AppId 的 `UOSAppId` 属性,如以下代码示例。更多的属性配置请参阅下文
+
+```xml
+
+ com.xx.xxx
+
+```
+
+通过如下命令行发布时,即可打出符合 UOS 规范的 deb 包
+
+```
+dotnet publish -t:CreateDebUOS -c release -r linux-x64 --self-contained
+```
+
+以上命令行与传统的发布命令最大的不同在于添加了 `-t:CreateDebUOS` 参数,通过此参数即可触发名为 `CreateDebUOS` 的 Target 进行创建 deb 包
+
+如期望自动在发布之后输出符合 UOS 规范的 deb 包,期望不添加 `-t:CreateDebUOS` 参数,则可以通过配置 `true` 属性到 csproj 从而实现在发布之后,自动执行打包,如以下代码
+
+```xml
+
+ true
+
+```
+
+通过 NuGet 包配置的方法,可以很方便进行接入,自带大量的默认配置,从零开始接入的成本低,且不需要有许多额外的知识。可以完全复用原有的构建工具链,可以配合其他工具实现一次打包创建多个平台的安装包,可以将各项配置写入到 csproj 里面方便客制化定制以及接入更多自动化参数和加入代码管理
+
+更多可配置属性请参阅 DebUOSConfiguration.cs 文件
+
+调试方法:
+
+- 可通过命令行参数输出了解打包参数的 `DebUOSPackingArgs.coin` 路径,从而了解打包所输入的参数。可以使用将此参数文件重新输入进行调试
+- 可通过命令行参数输出了解文件组织的 `DebUOSPacking\Packing` 文件夹路径,通过此文件夹即可了解打出的 deb 的文件夹内容,也可以切换到 Linux 服务器上使用更稳定的 `dpkg-deb` 对此文件夹进行打包
+
+一个简单的 csproj 配置示例
+
+```xml
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+ 1.0.9
+
+
+
+ com.xxx.foo
+
+
+
+ C:\lindexi\Code\foo.deb
+
+
+
+ true
+
+
+ icon.png
+ icon32.png
+
+ icon.svg
+
+
+ Test demo 也可以写中文
+
+ 测试中文名
+ foo;icon
+ 中文测试;foo
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+```
+
+## 开发
+
+### 各项目作用
+
+- Packaging.Targets : 来源于 [https://github.com/quamotion/dotnet-packaging](https://github.com/quamotion/dotnet-packaging) 项目,用来提供基础的打包功能
+- Packaging.DebUOS : 打出符合 UOS 规范的 deb 包的核心项目,包括组织文件夹以及使用 Packaging.Targets 创建 deb 包两个功能
+- Packaging.DebUOS.Tool : 对 Packaging.DebUOS 封装 dotnet tool 命令行工具
+- Packaging.DebUOS.NuGet : 对 Packaging.DebUOS.Tool 工具进行封装为配合 csproj 的 NuGet 包,且添加大量的和 csproj 属性对接的中间属性
+
+## 参考文档
+
+- [一步步教你在 Windows 上构建 dotnet 系应用的 UOS 软件安装包](https://blog.lindexi.com/post/%E4%B8%80%E6%AD%A5%E6%AD%A5%E6%95%99%E4%BD%A0%E5%9C%A8-Windows-%E4%B8%8A%E6%9E%84%E5%BB%BA-dotnet-%E7%B3%BB%E5%BA%94%E7%94%A8%E7%9A%84-UOS-%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85%E5%8C%85.html )
+
+- [应用打包规范 文档中心-统信UOS生态社区](https://doc.chinauos.com/content/M7kCi3QB_uwzIp6HyF5J )
+
+## 感谢
+
+- [https://github.com/quamotion/dotnet-packaging](https://github.com/quamotion/dotnet-packaging)
diff --git a/DotNETBuild.sln b/DotNETBuild.sln
index 5dcc683..3e4fd9e 100644
--- a/DotNETBuild.sln
+++ b/DotNETBuild.sln
@@ -89,6 +89,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SyncTool", "SyncTool", "{3C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SyncTool", "SyncTool\SyncTool.csproj", "{4360E6F1-680C-45D7-A4E0-1663A237709A}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebUOS", "DebUOS", "{AC990428-ACB7-46A9-B66A-AF0557A7D0C6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Packaging.Targets", "DebUOS\Packaging.Targets\Packaging.Targets.csproj", "{C69F9A99-8110-4379-A22C-176B3E05E481}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Packaging.DebUOS", "DebUOS\Packaging.DebUOS\Packaging.DebUOS.csproj", "{125615F2-0873-4C66-8FB3-E334D2F5BE9C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Packaging.DebUOS.Tool", "DebUOS\Packaging.DebUOS.Tool\Packaging.DebUOS.Tool.csproj", "{A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Packaging.DebUOS.NuGet", "DebUOS\Packaging.DebUOS.NuGet\Packaging.DebUOS.NuGet.csproj", "{977A7109-C37B-4CFC-875B-77E7149EFCCF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -471,6 +481,54 @@ Global
{4360E6F1-680C-45D7-A4E0-1663A237709A}.Release|x64.Build.0 = Release|Any CPU
{4360E6F1-680C-45D7-A4E0-1663A237709A}.Release|x86.ActiveCfg = Release|Any CPU
{4360E6F1-680C-45D7-A4E0-1663A237709A}.Release|x86.Build.0 = Release|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Debug|x64.Build.0 = Debug|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Debug|x86.Build.0 = Debug|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Release|x64.ActiveCfg = Release|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Release|x64.Build.0 = Release|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Release|x86.ActiveCfg = Release|Any CPU
+ {C69F9A99-8110-4379-A22C-176B3E05E481}.Release|x86.Build.0 = Release|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Debug|x64.Build.0 = Debug|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Debug|x86.Build.0 = Debug|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Release|x64.ActiveCfg = Release|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Release|x64.Build.0 = Release|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Release|x86.ActiveCfg = Release|Any CPU
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C}.Release|x86.Build.0 = Release|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Debug|x64.Build.0 = Debug|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Debug|x86.Build.0 = Debug|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Release|x64.ActiveCfg = Release|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Release|x64.Build.0 = Release|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Release|x86.ActiveCfg = Release|Any CPU
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD}.Release|x86.Build.0 = Release|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Debug|x64.Build.0 = Debug|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Debug|x86.Build.0 = Debug|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Release|x64.ActiveCfg = Release|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Release|x64.Build.0 = Release|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Release|x86.ActiveCfg = Release|Any CPU
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -491,6 +549,10 @@ Global
{E252DBCC-6FC9-42FF-86DB-E31D076F954F} = {2C43AF3C-C3B2-4B8A-81E2-DA6D8A53356A}
{93037B85-0A27-4A6E-B485-2F4544ABEFDF} = {2C43AF3C-C3B2-4B8A-81E2-DA6D8A53356A}
{4360E6F1-680C-45D7-A4E0-1663A237709A} = {3C8DD4B9-55DB-450E-B06A-B7C4EB3A9CF1}
+ {C69F9A99-8110-4379-A22C-176B3E05E481} = {AC990428-ACB7-46A9-B66A-AF0557A7D0C6}
+ {125615F2-0873-4C66-8FB3-E334D2F5BE9C} = {AC990428-ACB7-46A9-B66A-AF0557A7D0C6}
+ {A5FDA79F-411A-4AC4-8B59-DA7E122ABAAD} = {AC990428-ACB7-46A9-B66A-AF0557A7D0C6}
+ {977A7109-C37B-4CFC-875B-77E7149EFCCF} = {AC990428-ACB7-46A9-B66A-AF0557A7D0C6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A6E377C6-EDE0-4135-951F-78C62B63DE96}
diff --git a/README.md b/README.md
index 9e19f2d..35cffb6 100644
--- a/README.md
+++ b/README.md
@@ -452,6 +452,54 @@ SyncTool sync [参数]
- `-a` 或 `-Address` : 同步服务的地址。必填,格式如 `http://127.0.0.1:56621` 等地址
- `-f` 或 `-Folder` : 本地同步的文件夹。可选,不填默认为工作路径
+### 制作符合 UOS 规范的 deb 安装包的工具
+
+可以打包出符合 UOS 规范的 deb 安装包的工具
+
+提供两个不同的使用方式,分别是命令行方式和配合 csproj 的 NuGet 包方式
+
+命令行方式通过 dotnet tool 方式使用,先使用以下命令安装工具
+
+```
+dotnet tool update -g Packaging.DebUOS.Tool
+```
+
+将已经准备好的符合 UOS 安装包文件组织规范的文件夹打包为 deb 安装包:
+
+```
+dotnet dpkg-debuos -b C:\lindexi\DebPacking -o C:\lindexi\UOS\Foo.deb
+```
+
+配合 csproj 的 NuGet 包方式:
+
+请通过 NuGet 管理器或采用如下代码编辑 csproj 文件安装 `Packaging.DebUOS` 库
+
+```xml
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+```
+
+接着添加一些必要的配置参数,比如用来配置 UOS 的 AppId 的 `UOSAppId` 属性,如以下代码示例。更多的属性配置请参阅下文
+
+```xml
+
+ com.xx.xxx
+
+```
+
+通过如下命令行发布时,即可打出符合 UOS 规范的 deb 包
+
+```
+dotnet publish -t:CreateDebUOS -c release -r linux-x64 --self-contained
+```
+
+更多请参阅 [Packaging.DebUOS 工具](./DebUOS)
+
+
## 相似的项目
[dotnetcore/FlubuCore: A cross platform build and deployment automation system for building projects and executing deployment scripts using C# code.](https://github.com/dotnetcore/FlubuCore )
@@ -460,4 +508,5 @@ SyncTool sync [参数]
排名无先后
-- [nmklotas/GitLabApiClient: GitLab API client](https://github.com/nmklotas/GitLabApiClient )
\ No newline at end of file
+- [nmklotas/GitLabApiClient: GitLab API client](https://github.com/nmklotas/GitLabApiClient )
+- [https://github.com/quamotion/dotnet-packaging](https://github.com/quamotion/dotnet-packaging)
\ No newline at end of file
diff --git a/build/Version.props b/build/Version.props
index 4c320dc..e1307c6 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -1,5 +1,5 @@
- 1.2.1
+ 1.2.1-alpha25
\ No newline at end of file