diff --git a/src/PackageUploader.UI.Test/ViewModel/ExtractIdInformationFromValidatorLogTest.cs b/src/PackageUploader.UI.Test/ViewModel/ExtractIdInformationFromValidatorLogTest.cs
index f7a40536..7640de49 100644
--- a/src/PackageUploader.UI.Test/ViewModel/ExtractIdInformationFromValidatorLogTest.cs
+++ b/src/PackageUploader.UI.Test/ViewModel/ExtractIdInformationFromValidatorLogTest.cs
@@ -28,7 +28,7 @@ public TestableValidatorLogViewModel(
UploadingProgressPercentageProvider uploadingProgressPercentageProvider,
ErrorModelProvider errorModelProvider,
string xmlContent)
- : base(packageModelProvider, uploaderService, windowService, uploadingProgressPercentageProvider, errorModelProvider)
+ : base(packageModelProvider, uploaderService, windowService, uploadingProgressPercentageProvider, errorModelProvider, new PathConfigurationProvider())
{
_xmlContent = xmlContent;
TestSubValFilePath = Path.GetTempFileName();
diff --git a/src/PackageUploader.UI.Test/ViewModel/GenerateUploaderConfigTest.cs b/src/PackageUploader.UI.Test/ViewModel/GenerateUploaderConfigTest.cs
index 43122ae0..abb3587f 100644
--- a/src/PackageUploader.UI.Test/ViewModel/GenerateUploaderConfigTest.cs
+++ b/src/PackageUploader.UI.Test/ViewModel/GenerateUploaderConfigTest.cs
@@ -30,7 +30,7 @@ public TestableUploaderConfigViewModel(
IWindowService windowService,
UploadingProgressPercentageProvider uploadingProgressPercentageProvider,
ErrorModelProvider errorModelProvider)
- : base(packageModelProvider, uploaderService, windowService, uploadingProgressPercentageProvider, errorModelProvider)
+ : base(packageModelProvider, uploaderService, windowService, uploadingProgressPercentageProvider, errorModelProvider, new PathConfigurationProvider())
{
}
diff --git a/src/PackageUploader.UI.Test/ViewModel/PackageUploadViewModelTest.cs b/src/PackageUploader.UI.Test/ViewModel/PackageUploadViewModelTest.cs
index 63d7dbcd..215b2f20 100644
--- a/src/PackageUploader.UI.Test/ViewModel/PackageUploadViewModelTest.cs
+++ b/src/PackageUploader.UI.Test/ViewModel/PackageUploadViewModelTest.cs
@@ -63,7 +63,8 @@ public void Setup()
_mockPackageUploaderService.Object,
_mockWindowService.Object,
_uploadingProgressPercentageProvider,
- _errorModelProvider
+ _errorModelProvider,
+ new PathConfigurationProvider()
);
}
@@ -87,8 +88,9 @@ public void Test_BranchOrFlightDisplayName()
_mockPackageUploaderService.Object,
_mockWindowService.Object,
_uploadingProgressPercentageProvider,
- _errorModelProvider
- );
+ _errorModelProvider,
+ new PathConfigurationProvider()
+ );
viewModel2.BranchOrFlightDisplayName = "Test";
Assert.AreEqual("Test", viewModel2.BranchOrFlightDisplayName);
}
@@ -124,7 +126,8 @@ public void Test_BranchAndFlightNames()
_mockPackageUploaderService.Object,
_mockWindowService.Object,
_uploadingProgressPercentageProvider,
- _errorModelProvider
+ _errorModelProvider,
+ new PathConfigurationProvider()
);
viewModel2.BranchAndFlightNames = names; // tests the former value is successfully retrieved
@@ -146,7 +149,8 @@ public void Test_MarketGroupNames()
_mockPackageUploaderService.Object,
_mockWindowService.Object,
_uploadingProgressPercentageProvider,
- _errorModelProvider
+ _errorModelProvider,
+ new PathConfigurationProvider()
);
viewModel2.MarketGroupNames = names; // tests the former value is successfully retrieved
diff --git a/src/PackageUploader.UI.Test/ViewModel/TestablePackageUploadViewModel.cs b/src/PackageUploader.UI.Test/ViewModel/TestablePackageUploadViewModel.cs
index 1216de53..3bb82cc5 100644
--- a/src/PackageUploader.UI.Test/ViewModel/TestablePackageUploadViewModel.cs
+++ b/src/PackageUploader.UI.Test/ViewModel/TestablePackageUploadViewModel.cs
@@ -43,7 +43,7 @@ public TestablePackageUploadViewModel(
IWindowService windowService,
UploadingProgressPercentageProvider uploadingProgressPercentageProvider,
ErrorModelProvider errorModelProvider)
- : base(packageModelProvider, uploaderService, windowService, uploadingProgressPercentageProvider, errorModelProvider)
+ : base(packageModelProvider, uploaderService, windowService, uploadingProgressPercentageProvider, errorModelProvider, new PathConfigurationProvider())
{
}
diff --git a/src/PackageUploader.UI/Model/Xvc/XvcFile.cs b/src/PackageUploader.UI/Model/Xvc/XvcFile.cs
index a7602574..39f9882e 100644
--- a/src/PackageUploader.UI/Model/Xvc/XvcFile.cs
+++ b/src/PackageUploader.UI/Model/Xvc/XvcFile.cs
@@ -60,6 +60,84 @@ private static UInt32 NumberOfHashPagesForLevel(UInt64 dataPages, Int32 level)
return (UInt32)((dataPages + divisor - 1) / divisor);
}
+ private static readonly byte[] ZipLocalFileSignature = [0x50, 0x4B, 0x03, 0x04];
+ private static readonly byte[] ZipEocdSignature = [0x50, 0x4B, 0x05, 0x06];
+ private const int FirstReadSize = 4096;
+ private const int LastReadSize = 65 * 1024;
+ private const int MinFileNameOffset = 30;
+
+ ///
+ /// Detects whether a .msixvc file is in MSIXVC2 format by checking for ZIP signatures.
+ /// MSIXVC2 packages are ZIP-based and contain standard ZIP headers, while MSIXVC1
+ /// packages use a proprietary binary format without ZIP signatures.
+ ///
+ public static bool IsLikelyMsixvc2Package(string packagePath)
+ {
+ try
+ {
+ if (!packagePath.EndsWith(".msixvc", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ var fileInfo = new FileInfo(packagePath);
+ if (!fileInfo.Exists || fileInfo.Length < FirstReadSize)
+ return false;
+
+ using var stream = File.OpenRead(packagePath);
+
+ // Check first bytes for ZIP local file header at offset 0
+ var firstBuffer = new byte[Math.Min(FirstReadSize, fileInfo.Length)];
+ stream.ReadExactly(firstBuffer, 0, firstBuffer.Length);
+
+ if (firstBuffer.Length >= MinFileNameOffset &&
+ firstBuffer[0] == ZipLocalFileSignature[0] &&
+ firstBuffer[1] == ZipLocalFileSignature[1] &&
+ firstBuffer[2] == ZipLocalFileSignature[2] &&
+ firstBuffer[3] == ZipLocalFileSignature[3])
+ {
+ return true;
+ }
+
+ // Check last 65KB for ZIP End of Central Directory signature
+ long lastChunkStart = Math.Max(0, fileInfo.Length - LastReadSize);
+ int lastChunkSize = (int)(fileInfo.Length - lastChunkStart);
+ var lastBuffer = new byte[lastChunkSize];
+ stream.Position = lastChunkStart;
+ stream.ReadExactly(lastBuffer, 0, lastChunkSize);
+
+ if (LastIndexOfSignature(lastBuffer, ZipEocdSignature) >= 0)
+ return true;
+
+ return false;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ private static int LastIndexOfSignature(byte[] buffer, byte[] signature)
+ {
+ if (buffer.Length < signature.Length)
+ return -1;
+
+ for (int i = buffer.Length - signature.Length; i >= 0; i--)
+ {
+ bool match = true;
+ for (int j = 0; j < signature.Length; j++)
+ {
+ if (buffer[i + j] != signature[j])
+ {
+ match = false;
+ break;
+ }
+ }
+ if (match)
+ return i;
+ }
+
+ return -1;
+ }
+
public static void GetBuildAndKeyId(string packagePath, out Guid buildId, out Guid keyId)
{
using FileStream stream = File.OpenRead(packagePath);
diff --git a/src/PackageUploader.UI/Resources/Images/PackagePlaceholder.png b/src/PackageUploader.UI/Resources/Images/PackagePlaceholder.png
new file mode 100644
index 00000000..aa12d3e2
Binary files /dev/null and b/src/PackageUploader.UI/Resources/Images/PackagePlaceholder.png differ
diff --git a/src/PackageUploader.UI/View/PackageUploadView.xaml b/src/PackageUploader.UI/View/PackageUploadView.xaml
index c307d248..be089404 100644
--- a/src/PackageUploader.UI/View/PackageUploadView.xaml
+++ b/src/PackageUploader.UI/View/PackageUploadView.xaml
@@ -178,22 +178,31 @@
Height="32"
Margin="0,0,0,10"/>
-
-
-
-
+
+
+
@@ -496,7 +505,12 @@
+ Foreground="{DynamicResource SecondaryTextBrush}"
+ Visibility="{Binding IsMsixvc2Package, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Invert}"/>
+
@@ -511,11 +525,15 @@
-
+
+
+
+ Foreground="{DynamicResource SecondaryTextBrush}"
+ Visibility="{Binding IsMsixvc2Package, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Invert}"/>
+
+ TextWrapping="Wrap"
+ Visibility="{Binding IsMsixvc2Package, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Invert}"/>
+
+ Foreground="{DynamicResource SecondaryTextBrush}"
+ Visibility="{Binding IsMsixvc2Package, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Invert}"/>
+
@@ -563,7 +596,10 @@
Visibility="{Binding IsCompactMode, Converter={StaticResource BooleanToVisibilityConverter}}">
-
+
+
@@ -574,8 +610,8 @@
-
-
+
+
+
+
diff --git a/src/PackageUploader.UI/ViewModel/PackageUploadViewModel.cs b/src/PackageUploader.UI/ViewModel/PackageUploadViewModel.cs
index fe777238..54e05977 100644
--- a/src/PackageUploader.UI/ViewModel/PackageUploadViewModel.cs
+++ b/src/PackageUploader.UI/ViewModel/PackageUploadViewModel.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Diagnostics;
+using System.IO.Compression;
using System.Windows.Input;
using System.Xml;
using PackageUploader.ClientApi;
@@ -32,6 +33,7 @@ public partial class PackageUploadViewModel : BaseViewModel
private readonly IWindowService _windowService;
public readonly UploadingProgressPercentageProvider _uploadingProgressPercentageProvider;
private readonly ErrorModelProvider _errorModelProvider;
+ private readonly PathConfigurationProvider _pathConfigurationService;
private GameProduct? _gameProduct = null;
private IReadOnlyCollection? _branchesAndFlights = null;
@@ -370,6 +372,46 @@ public string PackageErrorMessage
set => SetProperty(ref _packageErrorMessage, value);
}
+ private string _msixvc2InfoMessage = string.Empty;
+ public string Msixvc2InfoMessage
+ {
+ get => _msixvc2InfoMessage;
+ set => SetProperty(ref _msixvc2InfoMessage, value);
+ }
+
+ private bool _isMsixvc2Package = false;
+ public bool IsMsixvc2Package
+ {
+ get => _isMsixvc2Package;
+ set
+ {
+ if (SetProperty(ref _isMsixvc2Package, value))
+ {
+ CheckCanExecuteUploadCommand();
+ }
+ }
+ }
+
+ private string _makePkg2UnavailableMessage = string.Empty;
+ public string MakePkg2UnavailableMessage
+ {
+ get => _makePkg2UnavailableMessage;
+ set => SetProperty(ref _makePkg2UnavailableMessage, value);
+ }
+
+ public string PackageIdentityName
+ {
+ get => Package.PackageIdentityName;
+ set
+ {
+ if (Package.PackageIdentityName != value)
+ {
+ Package.PackageIdentityName = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
private string _branchOrFlightErrorMessage = string.Empty;
public string BranchOrFlightErrorMessage
{
@@ -411,13 +453,15 @@ public PackageUploadViewModel(PackageModelProvider packageModelService,
IPackageUploaderService uploaderService,
IWindowService windowService,
UploadingProgressPercentageProvider uploadingProgressPercentageProvider,
- ErrorModelProvider errorModelProvider)
+ ErrorModelProvider errorModelProvider,
+ PathConfigurationProvider pathConfigurationService)
{
_packageModelService = packageModelService;
_uploaderService = uploaderService;
_windowService = windowService;
_uploadingProgressPercentageProvider = uploadingProgressPercentageProvider;
_errorModelProvider = errorModelProvider;
+ _pathConfigurationService = pathConfigurationService;
// Initialize commands with RelayCommand
UploadPackageCommand = new RelayCommand(UploadPackageProcessAsync, () => IsUploadReady());
@@ -440,6 +484,12 @@ public PackageUploadViewModel(PackageModelProvider packageModelService,
private bool IsUploadReady()
{
+ // MSIXVC2 packages require makepkg2 tools
+ if (IsMsixvc2Package && !string.IsNullOrEmpty(MakePkg2UnavailableMessage))
+ {
+ return false;
+ }
+
return File.Exists(PackageFilePath) &&
_gameProduct != null &&
!string.IsNullOrEmpty(MarketGroupName) &&
@@ -511,7 +561,31 @@ private void ProcessSelectedPackage()
{
return;
}
-
+
+ // Detect MSIXVC2 packages (created by makepkg2) before attempting legacy extraction
+ if (XvcFile.IsLikelyMsixvc2Package(PackageFilePath))
+ {
+ IsMsixvc2Package = true;
+ Msixvc2InfoMessage = "MSIXVC2 package detected. Upload is supported and will use the makepkg2 upload tool.";
+
+ // Check if makepkg2 tools are installed
+ string makePkg2Path = _pathConfigurationService.MakePkg2Path;
+ if (string.IsNullOrEmpty(makePkg2Path) || !File.Exists(makePkg2Path))
+ {
+ MakePkg2UnavailableMessage = Resources.Strings.MainPage.MakePkg2NotFoundErrorMsg;
+ }
+
+ try
+ {
+ ExtractMsixvc2PackageInformation(PackageFilePath);
+ }
+ catch (Exception ex)
+ {
+ PackageErrorMessage = $"{Resources.Strings.PackageUpload.ErrorExtractingInfoErrMsg} {ex.Message}";
+ }
+ return;
+ }
+
try
{
// Extract package information
@@ -534,6 +608,10 @@ private void ResetPackage()
MarketGroupName = string.Empty;
PackageErrorMessage = string.Empty;
+ Msixvc2InfoMessage = string.Empty;
+ MakePkg2UnavailableMessage = string.Empty;
+ IsMsixvc2Package = false;
+ PackageIdentityName = string.Empty;
ResetProductInfo();
}
@@ -653,6 +731,64 @@ private void ExtractPackageInformation(string packagePath)
}
}
+ private void ExtractMsixvc2PackageInformation(string packagePath)
+ {
+ long fileLength = GetFileSize(packagePath);
+ double bytesInMB = 1024.0 * 1024.0;
+ double bytesInGB = bytesInMB * 1024.0;
+ PackageSize = fileLength > bytesInGB
+ ? string.Format("{0:0.##} GB", fileLength / bytesInGB)
+ : string.Format("{0:0.##} MB", fileLength / bytesInMB);
+
+ string? tempConfigPath = null;
+ try
+ {
+ using var archive = ZipFile.OpenRead(packagePath);
+ var configEntry = archive.Entries.FirstOrDefault(e =>
+ e.Name.Equals("MicrosoftGame.config", StringComparison.OrdinalIgnoreCase));
+
+ if (configEntry == null)
+ {
+ PackageErrorMessage = "MicrosoftGame.config not found in MSIXVC2 package.";
+ return;
+ }
+
+ tempConfigPath = Path.GetTempFileName();
+ configEntry.ExtractToFile(tempConfigPath, overwrite: true);
+
+ var gameConfig = new PartialGameConfigModel(tempConfigPath);
+
+ PackageIdentityName = gameConfig.Identity.Name ?? string.Empty;
+ PackagePreviewImage = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/PackagePlaceholder.png"));
+
+ try
+ {
+ PackageType = gameConfig.GetDeviceFamily();
+ }
+ catch
+ {
+ // Device family not available
+ }
+
+ if (!string.IsNullOrEmpty(gameConfig.StoreId))
+ {
+ BigId = gameConfig.StoreId;
+ IsPackageMissingStoreId = false;
+ }
+ else
+ {
+ BigId = string.Empty;
+ IsPackageMissingStoreId = true;
+ PackageErrorMessage = Resources.Strings.PackageUpload.PackageHasNoBigIdConfigureMsftGameCfgErrMsg;
+ }
+ }
+ finally
+ {
+ if (tempConfigPath != null && File.Exists(tempConfigPath))
+ File.Delete(tempConfigPath);
+ }
+ }
+
// Virtual methods to make the class more testable
///
@@ -818,6 +954,13 @@ private async void GetProductInfoAsync()
private async void UploadPackageProcessAsync()
{
+ // For MSIXVC2 packages, reroute to makepkg2 upload
+ if (IsMsixvc2Package)
+ {
+ StartMsixvc2Upload();
+ return;
+ }
+
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
_windowService.NavigateTo(typeof(PackageUploadingView));
@@ -960,6 +1103,72 @@ private void OnCancelButton()
_windowService.NavigateTo(typeof(MainPageView));
}
+ ///
+ /// Reroutes MSIXVC2 .msixvc package upload to makepkg2 upload tool.
+ /// Sets PackageModel properties and navigates to the MSIXVC2 uploading progress screen.
+ ///
+ private void StartMsixvc2Upload()
+ {
+ string makePkg2Path = _pathConfigurationService.MakePkg2Path;
+ if (string.IsNullOrEmpty(makePkg2Path) || !File.Exists(makePkg2Path))
+ {
+ SetErrorAndGoToErrorPage("makepkg2 Not Found",
+ "makepkg2.exe was not found. Please install the Microsoft.Xbox.Packaging.Tools.makepkg2 NuGet package.");
+ return;
+ }
+
+ string uploadArgs = BuildMsixvc2UploadArguments();
+
+ IGamePackageBranch? branchOrFlight = GetBranchOrFlightFromUISelection();
+
+ Package.BigId = BigId;
+ Package.PackageType = PackageType;
+ Package.PackagePreviewImage = PackagePreviewImage;
+ Package.PackageName = ProductName;
+ Package.Destination = BranchOrFlightDisplayName;
+ Package.Market = MarketGroupName;
+ Package.PackageIdentityName = PackageIdentityName;
+ Package.FolderSize = PackageSize;
+ Package.UploadArguments = uploadArgs;
+ Package.MakePkg2Path = makePkg2Path;
+ if (branchOrFlight != null)
+ {
+ Package.BranchId = branchOrFlight.CurrentDraftInstanceId;
+ }
+
+ _windowService.NavigateTo(typeof(Msixvc2UploadingView));
+ }
+
+ internal string BuildMsixvc2UploadArguments()
+ {
+ string packageDir = Path.GetDirectoryName(PackageFilePath) ?? string.Empty;
+ var args = $"upload /pd \"{packageDir}\"";
+
+ if (BranchOrFlightDisplayName.StartsWith("Branch: "))
+ {
+ string branchName = BranchOrFlightDisplayName[(BranchOrFlightDisplayName.IndexOf(':') + 2)..];
+ args += $" /branch \"{branchName}\"";
+ }
+ else if (BranchOrFlightDisplayName.StartsWith("Flight: "))
+ {
+ string flightName = BranchOrFlightDisplayName[(BranchOrFlightDisplayName.IndexOf(':') + 2)..];
+ args += $" /flight \"{flightName}\"";
+ }
+
+ if (!string.IsNullOrEmpty(MarketGroupName))
+ {
+ args += $" /market \"{MarketGroupName}\"";
+ }
+
+ if (!string.IsNullOrEmpty(BigId) && BigId != "None")
+ {
+ args += $" /storeid \"{BigId}\"";
+ }
+
+ args += " /auth CacheableBrowser";
+ return args;
+ }
+
private void SetErrorAndGoToErrorPage(string errorTitle, string errorDescription)
{
_errorModelProvider.Error.MainMessage = errorTitle;