Skip to content

Add type package validation - #890

Open
Mingzhe Jiang (jiangmingzhe) wants to merge 14 commits into
Azure:mainfrom
jiangmingzhe:mingzhejiang/type-package-validation-init
Open

Mingzhe Jiang (jiangmingzhe) wants to merge 14 commits into
Azure:mainfrom
jiangmingzhe:mingzhejiang/type-package-validation-init

Conversation

@jiangmingzhe

@jiangmingzhe Mingzhe Jiang (jiangmingzhe) commented Aug 3, 2026

Copy link
Copy Markdown
Member

Adds a new library for validating serialized Bicep type packages before publishing or consumption.

Functionality

  • Supports directories, index.json, archive files, and archive streams.
  • Validates JSON structure, references, type graphs, semantic constraints, and package hygiene.
  • Supports canonical-writer and compatible-reader modes.
  • Produces stable, source-located diagnostics.
  • Adds Bicep Types v1 format awareness.

Structure

  • Packaging/: input and archive handling
  • Structural/: JSON shape validation
  • Graph/: reference and target validation
  • Semantic/: value constraints
  • Policy/: compatibility rules
  • Hygiene/: unreachable/unexpected files
  • Diagnostics/: diagnostic models and ordering

Also adds the projects to the solution and introduces a suite of unit-test and golden-sample samples.

/// validation is now implemented (see <c>BCPVT030</c>–<c>BCPVT033</c>); this code is retained for
/// API stability but is no longer emitted.
/// </summary>
public const string ArchiveValidationNotImplemented = "BCPVT001";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codes seem to be duplicated in this file. Can we make it so they are only in one place (just like in Bicep)?

}

// 2. Package-relative path.
var cmp = string.CompareOrdinal(x.Path ?? string.Empty, y.Path ?? string.Empty);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://learn.microsoft.com/en-us/dotnet/api/system.string.compareordinal?view=net-10.0, the string.CompareOrdinal() method accepts nulls already.

public sealed class TypeValidationDiagnostic
{
private static readonly IReadOnlyList<TypeValidationDiagnosticRelatedLocation> NoRelatedLocations =
new TypeValidationDiagnosticRelatedLocation[0];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We can just do NoRelatedLocations = [];

return ParseTar(tarBytes);
}

private static byte[] Decompress(byte[] archiveBytes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one was found by AI.

Decompress copies the entire gzip payload into an unbounded MemoryStream, then duplicates it with ToArray(). Archive entries are subsequently copied again.

A small malicious types.tgz could expand to gigabytes and terminate the validating process with OutOfMemoryException. This is especially concerning because package validation is intended to handle third-party packages safely.

Recommended fix: Enforce configurable limits for:

  • Total decompressed bytes
  • Individual member size
  • Member count
  • Input stream size

Exceeding a limit should produce the existing fatal container diagnostic rather than attempting further allocation.

string refValue = refNode.StringValue ?? string.Empty;

// Validate the $ref string syntax
if (!ReferencePath.TryParse(refValue, out string packagePath, out int index))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think references can only appear in index.json, but not type files. Should we validate that?

TypePackageValidationOptions options)
{
if (documents == null) { throw new ArgumentNullException(nameof(documents)); }
if (options == null) { throw new ArgumentNullException(nameof(options)); }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have nullable enabled? If yes, the null checks can be removed. I think AI generated code can sometimes be too defensive.

{
if (packageRelativePath == null) { throw new ArgumentNullException(nameof(packageRelativePath)); }

string key = DirectoryPackageFileSystem.NormalizeSeparators(packageRelativePath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we canonicalize package-relative paths before using them as cache keys? NormalizeSeparators leaves forms such as ./types.json distinct from types.json, while the package file systems resolve both to the same physical file. This can load and validate one file multiple times under different keys. It also causes GetReachedFilePaths() to contain ./types.json while EnumerateFiles() returns types.json, resulting in a false BCPVT033 when unreachable-file validation is enabled. Could we use one canonical representation—removing . segments and redundant separators—for cache keys, reached paths, and file enumeration (or reject non-canonical references consistently)?

public TypeKindDescriptor(string discriminator, TypeFieldDescriptor[] fields)
{
Discriminator = discriminator;
Fields = fields ?? new TypeFieldDescriptor[0];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can simplify collection initialization: fields ?? [];

public string? Path { get; }

/// <summary>JSON pointer into the related file, when available.</summary>
public string? JsonPointer { get; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be beneficial to use JsonPointer from JsonPointer.Net? If we decide to use it we'd need to pin the version to 6.0 to avoid the EULA cert issue.

@gary-x-li

Copy link
Copy Markdown
Contributor

Just want to make sure I understand the intended behavior when a package is produced by a newer Azure.Bicep.Types than the validator. Because the catalog is a closed set ($type kinds, per-kind fields, BuiltInType.kind range, enum/flag masks), it looks like a newer package could be hard-rejected. For example, a future bicep-types adds an optional field to StringType, a 3P author emits it, and a consumer still on the older validator reports BCPVT013 and rejects the whole package. Same shape for a new $type kind (BCPVT008), a new BuiltInType.kind value (BCPVT024), or a new flag/enum bit (BCPVT027/028).

To be clear, I don't think this fires often. The bulk of package churn (new resources, API versions, properties on a resource body) is instance data within the existing kinds and would pass fine. It's specifically the rarer meta-schema evolution (like the scopeTypereadableScopes/writableScopes split this PR encodes) that would trip it, and when it does it tends to affect all newer packages at once. Co-location keeps the source in sync, but a consumer runs an independently-versioned validator against artifacts produced elsewhere by producers it may not control, so some runtime skew seems possible.

So mostly a question: is that newer-package-older-validator case a scenario this library intends to handle, and if so what's the expected behavior?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants