15 Commits
1.2.0 ... 1.4.1

Author SHA1 Message Date
Matt Nadareski
e6976796c2 Initial attempt at Delphi models 2024-04-02 10:58:20 -04:00
Deterous
295d8c7612 XboxOne/XboxSX catalog.js Model (#5)
* XboxOne catalog.js model

* Split Catalog object, bump version

* Minor fixes

* Custom JsonConverter for launchPackage

* Make launchPackage an abstract object

* Don't ignore packages

* Fix field types for Catalog/Package
2024-04-02 07:54:57 -07:00
Matt Nadareski
4dd184583c Revert XML tag for OfflineList duplicate ID 2024-03-19 15:30:45 -04:00
Matt Nadareski
081c9c9245 Bump version 2024-03-12 16:21:06 -04:00
Matt Nadareski
b974380ccf Fix SoftwareList.Disk field name 2024-03-12 15:28:58 -04:00
Matt Nadareski
41ed2cbc9a Fix XML element name for duplicateId 2024-03-12 00:07:47 -04:00
Matt Nadareski
2cfcb49e35 Fix missing OfflineList field 2024-03-11 23:35:22 -04:00
Matt Nadareski
b3f3f12b3e Use "main" instead of "master" 2024-02-27 19:06:26 -05:00
Matt Nadareski
b41700ff92 Update copyright date 2024-02-27 17:18:02 -05:00
Matt Nadareski
e8a357546b Add nuget package and PR workflows 2024-02-27 17:17:50 -05:00
Matt Nadareski
68f0201c11 Add SafeDisc encrypted file entry model 2023-11-30 19:11:52 -05:00
Matt Nadareski
25b6493249 Bump version 2023-11-21 11:15:24 -05:00
Matt Nadareski
a551363c0b Support .NET Framework 2.0 2023-11-20 23:44:05 -05:00
Matt Nadareski
2fd92aea8f Support .NET Framework 3.5 2023-11-20 21:10:43 -05:00
Matt Nadareski
a61b3d0ed9 Add IS Archive V3 models 2023-11-15 14:30:25 -05:00
23 changed files with 663 additions and 8 deletions

43
.github/workflows/build_nupkg.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Nuget Pack
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Pack
run: dotnet pack
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: 'Nuget Package'
path: 'bin/Release/*.nupkg'
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: 'bin/Release/*.nupkg'
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True

17
.github/workflows/check_pr.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Build PR
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build

14
Delphi/Constants.cs Normal file
View File

@@ -0,0 +1,14 @@
/// <remarks>
/// Information sourced from https://stackoverflow.com/questions/18720045/what-are-the-list-of-all-possible-values-for-dvclal
/// </remarks>
namespace SabreTools.Models.Delphi
{
public static class Constants
{
public static readonly byte[] DVCLALStandard = [0x23, 0x78, 0x5D, 0x23, 0xB6, 0xA5, 0xF3, 0x19, 0x43, 0xF3, 0x40, 0x02, 0x26, 0xD1, 0x11, 0xC7];
public static readonly byte[] DVCLALProfessional = [0xA2, 0x8C, 0xDF, 0x98, 0x7B, 0x3C, 0x3A, 0x79, 0x26, 0x71, 0x3F, 0x09, 0x0F, 0x2A, 0x25, 0x17];
public static readonly byte[] DVCLALEnterprise = [0x26, 0x3D, 0x4F, 0x38, 0xC2, 0x82, 0x37, 0xB8, 0xF3, 0x24, 0x42, 0x03, 0x17, 0x9B, 0x3A, 0x83];
}
}

View File

@@ -0,0 +1,14 @@
/// <remarks>
/// Information sourced from https://docwiki.embarcadero.com/Libraries/Alexandria/en/System.PackageInfoTable
/// </remarks>
namespace SabreTools.Models.Delphi
{
public class PackageInfoTable
{
public int UnitCount { get; set; }
public PackageUnitEntry[]? UnitInfo { get; set; }
public PackageTypeInfo? TypeInfo { get; set; }
}
}

19
Delphi/PackageTypeInfo.cs Normal file
View File

@@ -0,0 +1,19 @@
/// <remarks>
/// Information sourced from https://docwiki.embarcadero.com/Libraries/Sydney/en/System.TPackageTypeInfo
/// </remarks>
namespace SabreTools.Models.Delphi
{
public class PackageTypeInfo
{
public int TypeCount { get; set; }
/// <remarks>
/// System-dependent pointer type, assumed to be encoded for x86
/// </remarks>
public uint[]? TypeTable { get; set; }
public int UnitCount { get; set; }
public string[]? UnitNames { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
/// <remarks>
/// Information sourced from https://docwiki.embarcadero.com/Libraries/Alexandria/en/System.PackageUnitEntry
/// </remarks>
namespace SabreTools.Models.Delphi
{
public class PackageUnitEntry
{
/// <remarks>
/// System-dependent pointer type, assumed to be encoded for x86
/// </remarks>
public uint Init { get; set; }
/// <remarks>
/// System-dependent pointer type, assumed to be encoded for x86
/// </remarks>
public uint FInit { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
namespace SabreTools.Models.InstallShieldArchiveV3
{
/// <see href="https://github.com/wfr/unshieldv3/blob/master/ISArchiveV3.cpp"/>
public class Archive
{
/// <summary>
/// Archive header information
/// </summary>
public Header? Header { get; set; }
/// <summary>
/// Directories found in the archive
/// </summary>
public Directory[]? Directories { get; set; }
/// <summary>
/// Files found in the archive
/// </summary>
public File[]? Files { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace SabreTools.Models.InstallShieldArchiveV3
{
/// <see href="https://github.com/wfr/unshieldv3/blob/master/ISArchiveV3.cpp"/>
public class Directory
{
public string? Name { get; set; }
public ushort FileCount { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace SabreTools.Models.InstallShieldArchiveV3
{
public enum Attributes : byte
{
READONLY = 0x01,
HIDDEN = 0x02,
SYSTEM = 0x04,
UNCOMPRESSED = 0x10,
ARCHIVE = 0x20,
}
}

View File

@@ -0,0 +1,32 @@
namespace SabreTools.Models.InstallShieldArchiveV3
{
/// <see href="https://github.com/wfr/unshieldv3/blob/master/ISArchiveV3.h"/>
public class File
{
public byte VolumeEnd { get; set; }
public ushort Index { get; set; }
public uint UncompressedSize { get; set; }
public uint CompressedSize { get; set; }
public uint Offset { get; set; }
public uint DateTime { get; set; }
public uint Reserved0 { get; set; }
public ushort ChunkSize { get; set; }
public Attributes Attrib { get; set; }
public byte IsSplit { get; set; }
public byte Reserved1 { get; set; }
public byte VolumeStart { get; set; }
public string? Name { get; set; }
}
}

View File

@@ -0,0 +1,50 @@
namespace SabreTools.Models.InstallShieldArchiveV3
{
/// <see href="https://github.com/wfr/unshieldv3/blob/master/ISArchiveV3.h"/>
public class Header
{
public uint Signature1 { get; set; }
public uint Signature2 { get; set; }
public ushort Reserved0 { get; set; }
public ushort IsMultivolume { get; set; }
public ushort FileCount { get; set; }
public uint DateTime { get; set; }
public uint CompressedSize { get; set; }
public uint UncompressedSize { get; set; }
public uint Reserved1 { get; set; }
/// <remarks>
/// Set in first vol only, zero in subsequent vols
/// </remarks>
public byte VolumeTotal { get; set; }
/// <remarks>
/// [1...n]
/// </remarks>
public byte VolumeNumber { get; set; }
public byte Reserved2 { get; set; }
public uint SplitBeginAddress { get; set; }
public uint SplitEndAddress { get; set; }
public uint TocAddress { get; set; }
public uint Reserved3 { get; set; }
public ushort DirCount { get; set; }
public uint Reserved4 { get; set; }
public uint Reserved5 { get; set; }
}
}

View File

@@ -126,11 +126,11 @@ namespace SabreTools.Models.Metadata
string? asString = Read<string>(key);
if (asString != null)
return new string[] { asString };
return [asString];
asString = this[key]!.ToString();
if (asString != null)
return new string[] { asString };
return [asString];
return null;
}
@@ -140,7 +140,7 @@ namespace SabreTools.Models.Metadata
/// </summary>
private bool ValidateKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
if (string.IsNullOrEmpty(key))
return false;
else if (!ContainsKey(key))
return false;

View File

@@ -27,6 +27,9 @@ namespace SabreTools.Models.OfflineList
[XmlElement("releaseNumber")]
public ReleaseNumber? ReleaseNumber { get; set; }
[XmlElement("imageNumber")]
public ImageNumber? ImageNumber { get; set; }
[XmlElement("languageNumber")]
public LanguageNumber? LanguageNumber { get; set; }

View File

@@ -2,17 +2,17 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.2.0</Version>
<Version>1.4.1</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Common models used by other SabreTools projects</Description>
<Copyright>Copyright (c) Matt Nadareski 2022-2023</Copyright>
<Copyright>Copyright (c) Matt Nadareski 2022-2024</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Models</RepositoryUrl>
@@ -22,10 +22,13 @@
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath=""/>
<None Include="README.md" Pack="true" PackagePath="" />
</ItemGroup>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
<PackageReference Include="MinValueTupleBridge" Version="0.2.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith(`net4`))">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>

13
SafeDisc/Constants.cs Normal file
View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SabreTools.Models.SafeDisc
{
public static class Constants
{
public const uint EncryptedFileEntrySignature1 = 0xA8726B03;
public const uint EncryptedFileEntrySignature2 = 0xEF01996C;
}
}

View File

@@ -0,0 +1,31 @@
namespace SabreTools.Models.SafeDisc
{
/// <see href="http://blog.w4kfu.com/tag/safedisc"/>
public class EncryptedFileEntry
{
/// <summary>
/// 0xA8726B03
/// </summary>
public uint Signature1 { get; set; }
/// <summary>
/// 0xEF01996C
/// </summary>
public uint Signature2 { get; set; }
public uint FileNumber { get; set; }
public uint Offset1 { get; set; }
public uint Offset2 { get; set; }
public uint Unknown1 { get; set; }
public uint Unknown2 { get; set; }
/// <remarks>
/// 0x0D bytes
/// </remarks>
public byte[]? Name { get; set; }
}
}

View File

@@ -21,7 +21,7 @@ namespace SabreTools.Models.SoftwareList
public string? Status { get; set; }
/// <remarks>(yes|no) "no"</remarks>
[XmlAttribute("writeable")]
[XmlAttribute("writable")]
public string? Writeable { get; set; }
#region DO NOT USE IN PRODUCTION

32
Xbox/Attribute.cs Normal file
View File

@@ -0,0 +1,32 @@
using Newtonsoft.Json;
namespace SabreTools.Models.Xbox
{
/// <summary>
/// Extra attributes relating to package, in catalog.js
/// </summary>
public class Attribute
{
/// <summary>
/// "supports4k":
/// True if package supports 4K, false otherwise
/// </summary>
[JsonProperty("supports4k", NullValueHandling = NullValueHandling.Ignore)]
public bool? Supports4K { get; set; }
/// <summary>
/// "supportsHdr":
/// True if package supports HDR, false otherwise
/// </summary>
[JsonProperty("supportsHdr", NullValueHandling = NullValueHandling.Ignore)]
public bool? SupportsHDR { get; set; }
/// <summary>
/// "isXboxOneXEnhanced":
/// True if package is XboxOne X enhanced, false otherwise
/// </summary>
[JsonProperty("isXboxOneXEnhanced", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsXboxOneXEnhanced { get; set; }
}
}

85
Xbox/Catalog.cs Normal file
View File

@@ -0,0 +1,85 @@
using Newtonsoft.Json;
namespace SabreTools.Models.Xbox
{
/// <summary>
/// Contains metadata information about XboxOne and XboxSX discs
/// Stored in a JSON file on the disc at /MSXC/Metadata/catalog.js
/// TODO: Check for unknown fields or values in more catalog.js files
/// </summary>
[JsonObject]
public class Catalog
{
/// <summary>
/// "version":
/// Version of this catalog.js file
/// Known values: 1.0, 2.0, 2.1, 4.0, 4.1 (4.1 not confirmed on a disc)
/// </summary>
[JsonProperty("version")]
public string? Version { get; set; }
/// <summary>
/// "discNumber":
/// Varies for each disc in set
/// 0 is reserved and shouldnt be used
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("discNumber", NullValueHandling = NullValueHandling.Ignore)]
public int? DiscNumber { get; set; }
/// <summary>
/// "discCount":
/// Total number of discs in set
/// Same value for each disc in the set
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("discCount", NullValueHandling = NullValueHandling.Ignore)]
public int? DiscCount { get; set; }
/// <summary>
/// "discSetId":
/// 8 hex character ID for the set itself
/// Same value for each disc in the set
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("discSetId", NullValueHandling = NullValueHandling.Ignore)]
public string? DiscSetID { get; set; }
/// <summary>
/// "bundle":
/// Package details for the bundle itself
/// Known fields used: ProductID, XboxProductID,
/// OneStoreProductID, Titles, VUI, Images
/// Known Versions Present: 2.0, 4.0
/// </summary>
[JsonProperty("bundle", NullValueHandling = NullValueHandling.Ignore)]
public Package? Bundle { get; set; }
/// <summary>
/// "launchPackage":
/// Package name to use as launch package
/// Before 4.0, object=Package with only ContentID filled
/// For 4.0 onwards, object=String, representing filename
/// Known Versions Present: 2.0, 4.0
/// </summary>
[JsonProperty("launchPackage", NullValueHandling = NullValueHandling.Ignore)]
public object? LaunchPackage { get; set; }
/// <summary>
/// "packages":
/// Package details for each package on disc
/// Known Versions Present: 2.1, 4.0
/// </summary>
[JsonProperty("packages")]
public Package[]? Packages { get; set; }
/// <summary>
/// "siblings":
/// List of Package Names that are related to this disc
/// The console picks the correct one to use
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("siblings", NullValueHandling = NullValueHandling.Ignore)]
public string[][]? Siblings { get; set; }
}
}

25
Xbox/Image.cs Normal file
View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace SabreTools.Models.Xbox
{
/// <summary>
/// List of image files associated with a package in catalog.js
/// </summary>
public class Image
{
/// <summary>
/// "size":
/// String representing image size
/// Known values: "100x100", "208x208", "480x480"
/// </summary>
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
public string? Size { get; set; }
/// <summary>
/// "image":
/// File name of image within MSXC/Metadata/<PackageName>/
/// </summary>
[JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)]
public string? Name { get; set; }
}
}

162
Xbox/Package..cs Normal file
View File

@@ -0,0 +1,162 @@
using Newtonsoft.Json;
namespace SabreTools.Models.Xbox
{
/// <summary>
/// Metadata about each package on disc, in catalog.js
/// Packages are stored within /MSXC/
/// </summary>
public class Package
{
/// <summary>
/// "packageName":
/// Package name of variant
/// Matches MSXC/<PackageName> and MSXC/Metdata/<PackageName>
/// Known Versions Present: 2.0, 2.1, 4.0
/// </summary>
[JsonProperty("packageName", NullValueHandling = NullValueHandling.Ignore)]
public string? PackageName { get; set; }
/// <summary>
/// "productId":
/// Hex identifier for package Product ID
/// Known Versions Present: 2.0, 2.1
/// </summary>
[JsonProperty("productId", NullValueHandling = NullValueHandling.Ignore)]
public string? ProductID { get; set; }
/// <summary>
/// "contentId":
/// Hex content identifier
/// Known Versions present: 2.0, 2.1
/// </summary>
[JsonProperty("contentId", NullValueHandling = NullValueHandling.Ignore)]
public string? ContentID { get; set; }
/// <summary>
/// "xboxProductId":
/// Hex product identifier
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("xboxProductId", NullValueHandling = NullValueHandling.Ignore)]
public string? XboxProductID { get; set; }
/// <summary>
/// "oneStoreProductId":
/// Partner Center Product ID
/// 12 character uppercase alphanumeric
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("oneStoreProductId", NullValueHandling = NullValueHandling.Ignore)]
public string? OneStoreProductID { get; set; }
/// <summary>
/// "allowedOneStoreProductIds":
/// List of OneStoreProductID that this package is associated with
/// Used for DLC packages only (Type = "Durable")
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("allowedOneStoreProductIds", NullValueHandling = NullValueHandling.Ignore)]
public string[]? AllowedOneStoreProductIDs { get; set; }
/// <summary>
/// "franchiseGameHubId":
/// Hex identifier
/// Optionally used to mark package as game hub
/// Known Versions Present: 4.1
/// </summary>
[JsonProperty("franchiseGameHubId", NullValueHandling = NullValueHandling.Ignore)]
public string? FranchiseGameHubID { get; set; }
/// <summary>
/// "associatedFranchiseGameHubId":
/// Hex identifier
/// Marks corresponding FranchiseGameHubID that this package is launched with
/// Known Versions Present: 4.1
/// </summary>
[JsonProperty("associatedFranchiseGameHubId", NullValueHandling = NullValueHandling.Ignore)]
public string? AssociatedFranchiseGameHubID { get; set; }
/// <summary>
/// "titleId":
/// 8 hex character package Title ID
/// Known Versions Present: 2.0, 2.1, 4.0
/// </summary>
[JsonProperty("titleId", NullValueHandling = NullValueHandling.Ignore)]
public string? TitleID { get; set; }
/// <summary>
/// "titles"
/// List of name of package for each locale
/// Known Versions Present: 2.0, 2.1, 4.0
/// </summary>
[JsonProperty("titles", NullValueHandling = NullValueHandling.Ignore)]
public Title[]? Titles { get; set; }
/// <summary>
/// "vui":
/// List of Voice User Interface packages titles for each locale
/// Known Versions Present: 2.0, 2.1, 4.0
/// </summary>
[JsonProperty("vui", NullValueHandling = NullValueHandling.Ignore)]
public Title[]? VUI { get; set; }
/// <summary>
/// "images":
/// List of paths to each image in MSXC/Metadata/<PackageName>/
/// Known Versions Present: 2.0, 2.1, 4.0
/// </summary>
[JsonProperty("images", NullValueHandling = NullValueHandling.Ignore)]
public Image[]? Images { get; set; }
/// <summary>
/// "ratings":
/// List of package age ratings for each relevant rating system
/// Known Versions Present: 2.0, 2.1, 4.0
/// </summary>
[JsonProperty("ratings", NullValueHandling = NullValueHandling.Ignore)]
public Rating[]? Ratings { get; set; }
/// <summary>
/// "attributes":
/// Extra attributes associated with this package
/// Known Versions Present: 2.1, 4.0
/// </summary>
[JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)]
public Attribute[]? Attributes { get; set; }
/// <summary>
/// "variants":
/// Alternative packages
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("variants", NullValueHandling = NullValueHandling.Ignore)]
public Package[]? Variants { get; set; }
/// <summary>
/// "generation":
/// Console generation the package is for
/// Known values: "8" (XboxOne), "9" (Xbox Series X|S)
/// Known Versions Present: 4.0
/// </summary>
[JsonProperty("generation", NullValueHandling = NullValueHandling.Ignore)]
public string? Generation { get; set; }
/// <summary>
/// "size":
/// Size of package in bytes
/// Known Versions Present: 2.0, 2.1
/// </summary>
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
public long? Size { get; set; }
/// <summary>
/// "type":
/// Package Type
/// Known values: "Game" (Game package), "Durable" (DLC package)
/// Known Versions Present: 2.0, 2.1, 4.0
/// </summary>
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public string? Type { get; set; }
}
}

26
Xbox/Rating.cs Normal file
View File

@@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace SabreTools.Models.Xbox
{
/// <summary>
/// Package rating for each rating system, in catalog.js
/// </summary>
public class Rating
{
/// <summary>
/// "system":
/// Name of rating system
/// Known values: COB-AU, PEGI, PCBP, USK, China, CERO, ESRB, GCAM, CSRR,
/// COB, DJCTQ, GRB, OFLC, OFLC-NZ, PEGIPortugal, FPB, Microsoft
/// </summary>
[JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)]
public string? System { get; set; }
/// <summary>
/// "value":
/// String representing rating value, depends on rating system
/// </summary>
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
public string? Value { get; set; }
}
}

26
Xbox/Title.cs Normal file
View File

@@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace SabreTools.Models.Xbox
{
/// <summary>
/// Package Title for each locale, for catalog.js
/// </summary>
public class Title
{
/// <summary>
/// "locale":
/// String representing locale that this title is in
/// Known values: "default", "en", "de", "fr", "ar", "zh-hans",
/// "zh-hant", "zh-TW", "zh-HK", "zh-CN", "zh-SG", etc
/// </summary>
[JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)]
public string? Locale { get; set; }
/// <summary>
/// "title":
/// Package title
/// </summary>
[JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)]
public string? Name { get; set; }
}
}