Skip to content

Commit

Permalink
Added MsApp archive
Browse files Browse the repository at this point in the history
  • Loading branch information
anpetroc committed Jan 20, 2024
1 parent 8449db4 commit 889ead2
Show file tree
Hide file tree
Showing 39 changed files with 462 additions and 91 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
with:
dotnet-version: |
3.1.x
6.0.x
7.0.x
- run: dotnet --info

Expand Down
4 changes: 2 additions & 2 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ jobs:

steps:
- task: UseDotNet@2
displayName: 'Use dotnet SDK 6.0'
displayName: 'Use dotnet SDK 7.0'
inputs:
version: 6.0.x
version: 7.0.x
installationPath: '$(Agent.ToolsDirectory)/dotnet'

- task: UseDotNet@2
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "6.0.0",
"version": "7.0.0",
"rollForward": "latestMinor"
}
}
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
Expand Down
2 changes: 1 addition & 1 deletion src/PAModel/Checksum/ChecksumMaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public bool IsJsonDoubleEncoded
if (s.Count >= 2)
{
var path = string.Join("\\", s.Reverse());
if (_jsonDouble.Any(x => path.EndsWith(x)))
if (_jsonDouble.Any(path.EndsWith))
{
return true;
}
Expand Down
8 changes: 4 additions & 4 deletions src/PAModel/Checksum/IHashMaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ internal class Sha256HashMaker : IHashMaker, IDisposable
{
private readonly IncrementalHash _hash;

private static readonly byte[] _startObj = new byte[] { (byte)'{' };
private static readonly byte[] _endObj = new byte[] { (byte)'}' };
private static readonly byte[] _startArray = new byte[] { (byte)'[' };
private static readonly byte[] _endArray = new byte[] { (byte)']' };
private static readonly byte[] _startObj = "{"u8.ToArray();
private static readonly byte[] _endObj = "}"u8.ToArray();
private static readonly byte[] _startArray = "["u8.ToArray();
private static readonly byte[] _endArray = "]"u8.ToArray();
private static readonly byte[] _null = new byte[] { 254 };
private static readonly byte[] _true = new byte[] { 1 };
private static readonly byte[] _false = new byte[] { 0 };
Expand Down
2 changes: 1 addition & 1 deletion src/PAModel/MergeTool/ControlDiffVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private Dictionary<string, ControlState> GetSubtreeStates(BlockNode node)

private IEnumerable<ControlState> GetSubtreeStatesImpl(BlockNode node)
{
var childstates = node.Children?.SelectMany(child => GetSubtreeStatesImpl(child)) ?? Enumerable.Empty<ControlState>();
var childstates = node.Children?.SelectMany(GetSubtreeStatesImpl) ?? Enumerable.Empty<ControlState>();

if (!_childStateStore.TryGetControlState(node.Name.Identifier, out var state))
return childstates;
Expand Down
1 change: 1 addition & 0 deletions src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@


<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="[13.0.1,)" />
<PackageReference Include="System.Text.Json" Version="[6.0,)" />
<PackageReference Include="System.Text.Encodings.Web" Version="6.0.0" />
Expand Down
33 changes: 33 additions & 0 deletions src/PAModel/MsApp/IMsappArchive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO.Compression;

namespace Microsoft.PowerPlatform.Formulas.Tools.MsApp;

/// <summary>
/// base interface for MsappArchive
/// </summary>
public interface IMsappArchive
{
/// <summary>
/// Creates a new entry in the archive with the given name.
/// </summary>
/// <param name="entryName"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidOperationException"></exception>
ZipArchiveEntry CreateEntry(string entryName);

/// <summary>
/// Returns the entry in the archive with the given name.
/// </summary>
/// <param name="entryName"></param>
/// <returns>the entry or null when not found.</returns>
ZipArchiveEntry GetEntry(string entryName);

/// <summary>
/// Provides access to the underlying zip archive.
/// </summary>
ZipArchive ZipArchive { get; }
}
231 changes: 231 additions & 0 deletions src/PAModel/MsApp/MsappArchive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Logging;

namespace Microsoft.PowerPlatform.Formulas.Tools.MsApp;

/// <summary>
/// Represents a .msapp file.
/// </summary>
public class MsappArchive : IMsappArchive, IDisposable
{
#region Fields

private Lazy<IDictionary<string, ZipArchiveEntry>> _canonicalEntries;
private bool _isDisposed;
private readonly ILogger<MsappArchive> _logger;
private FileStream _fileStream;

#endregion

#region Constants

public const string ControlsDirectory = "Controls";
public const string ComponentsDirectory = "Components";
public const string AppTestDirectory = "AppTests";
public const string ReferencesDirectory = "References";
public const string ResourcesDirectory = "Resources";

#endregion

#region Constructors

public MsappArchive(string fileName, ILogger<MsappArchive> logger = null)
{
_logger = logger;
_fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
Initialize(_fileStream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);

}

public MsappArchive(Stream stream, ILogger<MsappArchive> logger = null)
: this(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null, logger)
{
}

public MsappArchive(Stream stream, ZipArchiveMode mode, ILogger<MsappArchive> logger = null)
: this(stream, mode, leaveOpen: false, entryNameEncoding: null, logger)
{
}

/// <summary>
/// Constructor
/// </summary>
/// <param name="stream"></param>
/// <param name="mode"></param>
/// <param name="leaveOpen">true to leave the stream open after the System.IO.Compression.ZipArchive object is disposed; otherwise, false</param>
/// <param name="logger"></param>
public MsappArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, ILogger<MsappArchive> logger = null)
: this(stream, mode, leaveOpen, null, logger)
{
}

public MsappArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding entryNameEncoding, ILogger<MsappArchive> logger = null)
{
_logger = logger;
Initialize(stream, mode, leaveOpen, entryNameEncoding);
}

private void Initialize(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding entryNameEncoding)
{
ZipArchive = new ZipArchive(stream, mode, leaveOpen, entryNameEncoding);
_canonicalEntries = new Lazy<IDictionary<string, ZipArchiveEntry>>
(() =>
{
var canonicalEntries = new Dictionary<string, ZipArchiveEntry>();
// If we're creating a new archive, there are no entries to canonicalize.
if (mode == ZipArchiveMode.Create)
return canonicalEntries;
foreach (var entry in ZipArchive.Entries)
{
if (!canonicalEntries.TryAdd(NormalizePath(entry.FullName), entry))
_logger?.LogInformation($"Duplicate entry found in archive: {entry.FullName}");
}
return canonicalEntries;
});
}

#endregion

#region Properties

/// <summary>
/// Canonical entries in the archive. Keys are normalized paths (lowercase, forward slashes, no trailing slash).
/// </summary>
public IReadOnlyDictionary<string, ZipArchiveEntry> CanonicalEntries => _canonicalEntries.Value.AsReadOnly();

/// <inheritdoc/>
public ZipArchive ZipArchive { get; private set; }

/// <summary>
/// Total sum of decompressed sizes of all entries in the archive.
/// </summary>
public long DecompressedSize => ZipArchive.Entries.Sum(zipArchiveEntry => zipArchiveEntry.Length);

/// <summary>
/// Total sum of compressed sizes of all entries in the archive.
/// </summary>
public long CompressedSize => ZipArchive.Entries.Sum(zipArchiveEntry => zipArchiveEntry.CompressedLength);

#endregion

#region Methods

/// <summary>
/// Returns all entries in the archive that are in the given directory.
/// </summary>
/// <param name="directoryName"></param>
/// <returns></returns>
public IEnumerable<ZipArchiveEntry> GetDirectoryEntries(string directoryName)
{
_ = directoryName ?? throw new ArgumentNullException(nameof(directoryName));

directoryName = NormalizePath(directoryName);

#if FEATUREGATE_DOCUMENTPREVIEWFLAGS_CANVASYAMLPERSISTENCE
var yamlDirectoryName = FileUtils.NormalizePath(Path.Combine("src", directoryName));
#endif

foreach (var entry in CanonicalEntries)
{
if (entry.Key.StartsWith(directoryName + '/'))
{
yield return entry.Value;
}

#if FEATUREGATE_DOCUMENTPREVIEWFLAGS_CANVASYAMLPERSISTENCE
if (entry.Key.StartsWith(yamlDirectoryName + '/'))
{
yield return entry.Value;
}
#endif
}
}

/// <inheritdoc/>
public ZipArchiveEntry GetEntry(string entryName)
{
if (string.IsNullOrWhiteSpace(entryName))
return null;

entryName = NormalizePath(entryName);
if (CanonicalEntries.TryGetValue(entryName, out var entry))
return entry;

return null;
}

/// <summary>
/// Returns the entry in the archive with the given name or throws if it does not exist.
/// </summary>
/// <param name="entryName"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="FileNotFoundException"></exception>
public ZipArchiveEntry GetRequiredEntry(string entryName)
{
var entry = GetEntry(entryName) ??
throw new FileNotFoundException($"Entry '{entryName}' not found in msapp archive.");

return entry;
}

/// <inheritdoc/>
public ZipArchiveEntry CreateEntry(string entryName)
{
if (string.IsNullOrWhiteSpace(entryName))
throw new ArgumentException("Entry name cannot be null or whitespace.", nameof(entryName));

var canonicalEntryName = NormalizePath(entryName);
if (_canonicalEntries.Value.ContainsKey(canonicalEntryName))
throw new InvalidOperationException($"Entry {entryName} already exists in the archive.");

var entry = ZipArchive.CreateEntry(entryName);
_canonicalEntries.Value.Add(canonicalEntryName, entry);

return entry;
}

public static string NormalizePath(string path)
{
return path.Trim().Replace('\\', '/').Trim('/').ToLowerInvariant();
}

#endregion

#region IDisposable

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
ZipArchive?.Dispose();
_fileStream?.Dispose();
}

ZipArchive = null;
_fileStream = null;
_canonicalEntries = null;
_isDisposed = true;
}
}

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

#endregion
}
2 changes: 1 addition & 1 deletion src/PAModel/PAConvert/PAWriterVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public override void Visit(BlockNode node, Context context)

context._yaml.WriteNewline();

foreach (var child in node.Children.OrderBy(child => GetZIndex(child)))
foreach (var child in node.Children.OrderBy(GetZIndex))
{
child.Accept(this, context);
}
Expand Down
12 changes: 6 additions & 6 deletions src/PAModel/Serializers/MsAppSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ private static IEnumerable<FileEntry> GetMsAppFiles(this CanvasDocument app, Err
DataSources = app.GetDataSources()
.SelectMany(x => x.Value)
.Where(x => !x.IsDataComponent)
.OrderBy(x => app._entropy.GetOrder(x))
.OrderBy(app._entropy.GetOrder)
.ToArray()
};
yield return ToFile(FileKind.DataSources, dataSources);
Expand Down Expand Up @@ -737,9 +737,9 @@ private static IEnumerable<FileEntry> GetMsAppFiles(this CanvasDocument app, Err
var pcfTemplates = app._templates.PcfTemplates ?? Array.Empty<PcfTemplateJson>();
app._templates = new TemplatesJson()
{
ComponentTemplates = componentTemplates.Any() ? componentTemplates.OrderBy(x => app._entropy.GetComponentOrder(x)).ToArray() : null,
UsedTemplates = app._templates.UsedTemplates.OrderBy(x => app._entropy.GetOrder(x)).ToArray(),
PcfTemplates = pcfTemplates.Any() ? pcfTemplates.OrderBy(x => app._entropy.GetPcfVersioning(x)).ToArray() : null
ComponentTemplates = componentTemplates.Any() ? componentTemplates.OrderBy(app._entropy.GetComponentOrder).ToArray() : null,
UsedTemplates = app._templates.UsedTemplates.OrderBy(app._entropy.GetOrder).ToArray(),
PcfTemplates = pcfTemplates.Any() ? pcfTemplates.OrderBy(app._entropy.GetPcfVersioning).ToArray() : null
};

yield return ToFile(FileKind.Templates, app._templates);
Expand Down Expand Up @@ -788,7 +788,7 @@ private static IEnumerable<FileEntry> GetMsAppFiles(this CanvasDocument app, Err
yield return ToFile(FileKind.ComponentsMetadata, new ComponentsMetadataJson
{
Components = componentsMetadata
.OrderBy(x => app._entropy.GetOrder(x))
.OrderBy(app._entropy.GetOrder)
.ToArray()
});
}
Expand All @@ -798,7 +798,7 @@ private static IEnumerable<FileEntry> GetMsAppFiles(this CanvasDocument app, Err
yield return ToFile(FileKind.DataComponentTemplates, new DataComponentTemplatesJson
{
ComponentTemplates = dcTemplate
.OrderBy(x => app._entropy.GetOrder(x))
.OrderBy(app._entropy.GetOrder)
.ToArray()
});
}
Expand Down
Loading

0 comments on commit 889ead2

Please sign in to comment.