2026-09-06 16:31:55 -07:00
# Requires -Version 5.1
# Read-only dependency preflight. Does not build, download, or change a Cargo target.
2026-09-10 11:38:10 -07:00
function Get-ArcadiaNotices([string]$Repository , [ string ] $Target , [ string ] $NoticeManifest , [ string ] $LlvmMinGWRoot , [ string[] ] $RootPackages = @ ( 'oa-desktop' , 'oa-server' )) {
if ( $RootPackages . Count -eq 0 -or @ ( $RootPackages | Where-Object { $_ -notin @ ( 'oa-desktop' , 'oa-server' ) }). Count -gt 0 ) { throw 'Notice roots must be oa-desktop and/or oa-server.' }
if ( $LlvmMinGWRoot -and $Target -ne 'x86_64-pc-windows-gnullvm' ) { throw 'LLVM-MinGW runtime notices apply only to the gnullvm target.' }
2026-09-06 16:31:55 -07:00
function Hash-File([string]$Path ) {
$stream = [ IO.File ]:: OpenRead ( $Path )
$sha = [ Security.Cryptography.SHA256 ]:: Create ()
try { return [ BitConverter ]:: ToString ( $sha . ComputeHash ( $stream )). Replace ( '-' , '' ). ToLowerInvariant () }
finally { $sha . Dispose (); $stream . Dispose () }
}
function Under-Root([string]$Root , [ string ] $Relative ) {
$base = [ IO.Path ]:: GetFullPath ( $Root ). TrimEnd ( '\' , '/' ) + [ IO.Path ]:: DirectorySeparatorChar
$path = [ IO.Path ]:: GetFullPath (( Join-Path $base $Relative ))
2026-09-10 11:38:10 -07:00
$comparison = if ([ IO.Path ]:: DirectorySeparatorChar -eq '/' ) { [ StringComparison ]:: Ordinal } else { [ StringComparison ]:: OrdinalIgnoreCase }
if ( -not $path . StartsWith ( $base , $comparison )) { throw "Notice path escapes source root: $Relative " }
2026-09-06 16:31:55 -07:00
if ( -not [ IO.File ]:: Exists ( $path )) { throw "Required notice/source missing: $path " }
return $path
}
$inputs = [ ordered ] @ {}
$hashes = @ {}
$failures = New-Object 'Collections.Generic.List[string]'
$records = New-Object 'Collections.Generic.List[object]'
$supplements = @ { packages = @ (); assets = @ (); runtimes = @ () }
if ( Test-Path -LiteralPath $NoticeManifest -PathType Leaf ) { $supplements = Get-Content -LiteralPath $NoticeManifest -Raw | ConvertFrom-Json }
$supplementRoot = Split-Path -Parent ([ IO.Path ]:: GetFullPath ( $NoticeManifest ))
function Add-Notice([string]$Source , [ string ] $Destination ) {
if ( $inputs . Contains ( $Destination ) -and $inputs [ $Destination ] -ne $Source ) { throw "Duplicate notice destination: $Destination " }
$inputs [ $Destination ] = $Source
$hashes [ $Destination ] = Hash-File $Source
return [ ordered ] @ { path = $Destination ; sha256 = $hashes [ $Destination ] }
}
function Add-Supplements($Entries , [ string ] $Prefix ) {
foreach ( $entry in $Entries ) {
if ( $entry . sha256 -notmatch '^[a-f0-9]{64}$' -or $entry . url -notmatch '^https://' ) { throw 'Supplement requires SHA-256 and an HTTPS provenance URL.' }
$source = Under-Root $supplementRoot $entry . file
if (( Hash-File $source ) -ne $entry . sha256 ) { throw "Supplement changed: $( $entry . file ) " }
$record = Add-Notice $source " $Prefix / $([ IO.Path ]:: GetFileName ( $source )) "
$record [ 'origin' ] = $entry . url
$record
}
}
$lock = Under-Root $Repository 'Cargo.lock'
$lockHash = Hash-File $lock
2026-09-10 11:38:10 -07:00
# Shared compiler inputs live outside individual crate directories.
$workspaceInputs = [ ordered ] @ {}
foreach ( $relative in @ ( 'Cargo.toml' , 'Cargo.lock' , 'LICENSE' , 'docs/openapi.json' )) {
$workspaceInputs [ $relative ] = Hash-File ( Under-Root $Repository $relative )
}
foreach ( $file in @ ( Get-ChildItem -LiteralPath ( Join-Path $Repository 'build' ) -Recurse -File | Sort-Object FullName )) {
if (( $file . Attributes -band [ IO.FileAttributes ]:: ReparsePoint ) -ne 0 ) { throw 'Shared build input reparse point requires review.' }
$relative = $file . FullName . Substring ([ IO.Path ]:: GetFullPath ( $Repository ). TrimEnd ( '\' , '/' ). Length + 1 ). Replace ( '\' , '/' )
$workspaceInputs [ $relative ] = Hash-File $file . FullName
}
if ( -not $workspaceInputs . Contains ( 'build/windows_manifest.rs' )) { throw 'Shared Windows build manifest input is missing.' }
2026-09-06 16:31:55 -07:00
$locked = @ {}
foreach ( $block in ([ IO.File ]:: ReadAllText ( $lock ) -split '(?m)^\[\[package\]\]\s*$' )) {
$fields = @ {}
foreach ( $field in [ regex ]:: Matches ( $block , '(?m)^(name|version|source|checksum) = "([^"]+)"\s*$' )) { $fields [ $field . Groups [ 1 ]. Value ] = $field . Groups [ 2 ]. Value }
if ( $fields . ContainsKey ( 'source' )) { $locked [ " $( $fields . name ) | $( $fields . version ) | $( $fields . source ) " ] = $fields }
}
$raw = @ (& cargo metadata - -locked - -offline - -manifest-path ( Join-Path $Repository 'Cargo.toml' ) - -filter-platform $Target - -format-version 1 )
if ( $LASTEXITCODE -ne 0 ) { throw 'Locked offline Cargo metadata failed; prepare the matching dependency cache first.' }
$metadata = ( $raw -join " `n " ) | ConvertFrom-Json
if (( Hash-File $lock ) -ne $lockHash ) { throw 'Cargo.lock changed during notice preflight.' }
$packages = @ {}; $nodes = @ {}
foreach ( $package in $metadata . packages ) { $packages [ $package . id ] = $package }
foreach ( $node in $metadata . resolve . nodes ) { $nodes [ $node . id ] = $node }
$queue = New-Object 'Collections.Generic.Queue[string]'
2026-09-10 11:38:10 -07:00
foreach ( $name in @ ( $RootPackages | Sort-Object -Unique )) {
2026-09-06 16:31:55 -07:00
$roots = @ ( $metadata . packages | Where-Object { $_ . name -eq $name -and $metadata . workspace_members -contains $_ . id })
if ( $roots . Count -ne 1 ) { throw "Expected one workspace package: $name " }
$queue . Enqueue ( $roots [ 0 ]. id )
}
$selected = New-Object 'Collections.Generic.HashSet[string]'
while ( $queue . Count -gt 0 ) {
$id = $queue . Dequeue ()
if ( -not $selected . Add ( $id )) { continue }
if ( -not $nodes . ContainsKey ( $id )) { throw "Unresolved dependency node: $id " }
foreach ( $dep in $nodes [ $id ]. deps ) {
if ( @ ( $dep . dep_kinds | Where-Object { $_ . kind -ne 'dev' }). Count -gt 0 ) { $queue . Enqueue ( $dep . pkg ) }
}
}
foreach ( $id in @ ( $selected | Sort-Object )) {
$package = $packages [ $id ]
$root = Split-Path -Parent $package . manifest_path
$key = " $( $package . name ) - $( $package . version ) "
if ( $key -notmatch '^[A-Za-z0-9._+-]+$' ) { throw "Unsafe package identity: $key " }
$prefix = "licenses/dependencies/ $key "
$registryChecksum = $null
if ( $package . source ) {
if ( $package . source -notlike 'registry+*' ) { $failures . Add ( "Unsupported source requiring deliberate review: $id " ); continue }
$lockKey = " $( $package . name ) | $( $package . version ) | $( $package . source ) "
if ( -not $locked . ContainsKey ( $lockKey )) { throw "Source is absent from Cargo.lock: $id " }
$registryChecksum = $locked [ $lockKey ]. checksum
if ( $registryChecksum -notmatch '^[a-f0-9]{64}$' ) { throw "Missing registry checksum: $key " }
$registryRoot = Split-Path -Parent ( Split-Path -Parent ( Split-Path -Parent $root ))
$archivePath = Under-Root $registryRoot "cache/ $([ IO.Path ]:: GetFileName (( Split-Path -Parent $root ))) / $key .crate"
if (( Hash-File $archivePath ) -ne $registryChecksum ) { throw "Cached crate archive differs from Cargo.lock: $key " }
}
# Hash actual local files (not merely Cargo's cached checksum claims).
$sourceHashes = [ ordered ] @ {}
$notices = New-Object 'Collections.Generic.List[object]'
$fontReferences = New-Object 'Collections.Generic.HashSet[string]'
foreach ( $file in @ ( Get-ChildItem -LiteralPath $root -Recurse -File | Sort-Object FullName )) {
if (( $file . Attributes -band [ IO.FileAttributes ]:: ReparsePoint ) -ne 0 ) { throw "Source reparse point requires review: $( $file . FullName ) " }
$relative = $file . FullName . Substring ( $root . Length + 1 ). Replace ( '\' , '/' )
if ( $relative -match '^target/' -or $file . Name -eq '.cargo-ok' ) { continue }
$sourceHashes [ $relative ] = Hash-File $file . FullName
if ( $file . Name -match '(^|[-._])(LICENSE|LICENCE|COPYING|NOTICE|OFL|UNLICENSE|COPYRIGHT)([-._]|$)' ) {
$notices . Add (( Add-Notice $file . FullName " $prefix / $relative " ))
}
# Literal embedded fonts in production/build source, including feature-gated ones
# conservatively. Tests/examples/benches are excluded, never all font files by glob.
if ( $file . Extension -eq '.rs' -and $relative -notmatch '^(tests|examples|benches)/' ) {
$text = [ IO.File ]:: ReadAllText ( $file . FullName )
2026-09-10 11:38:10 -07:00
# Full-line comments, including rustdoc examples, are not compiled
# font embeds. Keep real code and feature-gated macros conservative.
$text = [ regex ]:: Replace ( $text , '(?m)^[\t ]*//[^\r\n]*' , '' )
2026-09-06 16:31:55 -07:00
foreach ( $match in [ regex ]:: Matches ( $text , 'include_bytes!\s*\(\s*"([^"]+\.(?:ttf|otf|woff2?))"' )) {
2026-09-10 11:38:10 -07:00
$fontPath = [ IO.Path ]:: GetFullPath (( Join-Path $file . DirectoryName $match . Groups [ 1 ]. Value ))
$fontRoot = [ IO.Path ]:: GetFullPath ( $root ). TrimEnd ( '\' , '/' ) + [ IO.Path ]:: DirectorySeparatorChar
$fontComparison = if ([ IO.Path ]:: DirectorySeparatorChar -eq '/' ) { [ StringComparison ]:: Ordinal } else { [ StringComparison ]:: OrdinalIgnoreCase }
if ( -not $fontPath . StartsWith ( $fontRoot , $fontComparison )) { throw "Embedded font path escapes crate source root: $key / $relative " }
$font = Under-Root $root $fontPath . Substring ( $fontRoot . Length )
2026-09-06 16:31:55 -07:00
[ void ] $fontReferences . Add ( $font . Substring ( $root . Length + 1 ). Replace ( '\' , '/' ))
}
}
}
if ( -not $package . source ) { $notices . Add (( Add-Notice ( Under-Root $Repository 'LICENSE' ) " $prefix /workspace-LICENSE" )) }
$matching = @ ( $supplements . packages | Where-Object { $_ . name -eq $package . name -and $_ . version -eq $package . version })
foreach ( $supplement in $matching ) {
if ( $supplement . checksum -ne $registryChecksum ) { throw "Supplement is for different registry source: $key " }
foreach ( $notice in @ ( Add-Supplements $supplement . notices " $prefix /supplement" )) { $notices . Add ( $notice ) }
}
if ( -not $package . license -and -not $package . license_file ) { $failures . Add ( "No license declaration: $key " ) }
if ( $package . license_file ) {
$licensePath = Under-Root $root $package . license_file
$notices . Add (( Add-Notice $licensePath " $prefix /declared-license/ $([ IO.Path ]:: GetFileName ( $licensePath )) " ))
}
if ( $notices . Count -eq 0 ) { $failures . Add ( "No notice text: $key ( $( $package . license ) ); provide checksum-bound supplement" ) }
$assets = New-Object 'Collections.Generic.List[object]'
foreach ( $relative in @ ( $fontReferences | Sort-Object )) {
$assetHash = Hash-File ( Under-Root $root $relative )
$matches = @ ( $supplements . assets | Where-Object { $_ . name -eq $package . name -and $_ . version -eq $package . version -and $_ . path -eq $relative })
if ( $matches . Count -ne 1 -or $matches [ 0 ]. sha256 -ne $assetHash ) { $failures . Add ( "Embedded font requires exact notice mapping: $key / $relative ( $assetHash )" ); continue }
$assetNotices = @ ( Add-Supplements $matches [ 0 ]. notices " $prefix /assets/ $([ IO.Path ]:: GetFileName ( $relative )) " )
if ( $assetNotices . Count -eq 0 ) { $failures . Add ( "No embedded font notice: $key / $relative " ) }
$assets . Add ([ ordered ] @ { path = $relative ; sha256 = $assetHash ; license = $matches [ 0 ]. license ; notices = $assetNotices })
}
$records . Add ([ ordered ] @ { id = $id ; name = $package . name ; version = $package . version ; license = $package . license ; license_file = $package . license_file ; repository = $package . repository ; source = $package . source ; registry_checksum = $registryChecksum ; features = $nodes [ $id ]. features ; local_source_sha256 = $sourceHashes ; notices = @ ( $notices . ToArray ()); embedded_fonts = @ ( $assets . ToArray ()) })
}
$rustVersion = @ (& rustc -Vv )
if ( $LASTEXITCODE -ne 0 ) { throw 'Unable to identify the selected Rust compiler.' }
$sysroot = @ (& rustc - -print sysroot )
if ( $LASTEXITCODE -ne 0 -or $sysroot . Count -ne 1 ) { throw 'Unable to locate the selected Rust distribution.' }
$targetLib = @ (& rustc - -print target-libdir - -target $Target )
if ( $LASTEXITCODE -ne 0 -or $targetLib . Count -ne 1 -or -not ( Test-Path -LiteralPath $targetLib [ 0 ] -PathType Container )) { throw "Rust target libraries missing: $Target " }
$rustNotices = @ ()
foreach ( $path in @ ( 'share/doc/rust/COPYRIGHT.html' , 'share/doc/rust/COPYRIGHT-library.html' )) {
$rustNotices += Add-Notice ( Under-Root $sysroot [ 0 ] $path ) "licenses/rust/ $([ IO.Path ]:: GetFileName ( $path )) "
}
foreach ( $path in @ ( 'share/doc/COPYING3' , 'share/doc/COPYING.RUNTIME' )) {
if ( Test-Path -LiteralPath ( Join-Path $sysroot [ 0 ] $path ) -PathType Leaf ) { $rustNotices += Add-Notice ( Join-Path $sysroot [ 0 ] $path ) "licenses/rust/ $([ IO.Path ]:: GetFileName ( $path )) " }
}
$rustLibraries = [ ordered ] @ {}
foreach ( $file in @ ( Get-ChildItem -LiteralPath $targetLib [ 0 ] -File | Sort-Object Name )) { $rustLibraries [ $file . Name ] = Hash-File $file . FullName }
$runtime = $null
if ( $LlvmMinGWRoot ) {
$runtimeHash = Hash-File ( Under-Root $LlvmMinGWRoot 'x86_64-w64-mingw32/bin/libunwind.dll' )
$matches = @ ( $supplements . runtimes | Where-Object { $_ . sha256 -eq $runtimeHash -and $_ . name -eq 'libunwind.dll' })
if ( $matches . Count -ne 1 ) { $failures . Add ( "libunwind.dll requires exact component notice mapping: $runtimeHash " ) }
else {
$runtimeNotices = @ ( Add-Supplements $matches [ 0 ]. notices 'licenses/llvm-libunwind' )
if ( -not $runtimeNotices . Count ) { $failures . Add ( 'Missing libunwind component notices' ) }
$runtime = [ ordered ] @ { name = 'libunwind.dll' ; sha256 = $runtimeHash ; provenance = $matches [ 0 ]. provenance ; notices = $runtimeNotices }
}
}
if ( $failures . Count -gt 0 ) { throw ( "Notice preflight unresolved: `n - " + ( $failures -join " `n - " )) }
2026-09-10 11:38:10 -07:00
return [ pscustomobject ] @ { Inputs = $inputs ; Hashes = $hashes ; Inventory = [ ordered ] @ { format_version = 1 ; target = $Target ; root_packages = @ ( $RootPackages | Sort-Object -Unique ); cargo_lock_sha256 = $lockHash ; workspace_build_inputs_sha256 = $workspaceInputs ; scope = 'Target-filtered locked offline metadata; transitive normal/build edges from the stated root packages. Workspace feature unification may include a conservative superset. No binary/source equivalence or universal license audit claimed.' ; packages = @ ( $records . ToArray ()); rust = [ ordered ] @ { version = $rustVersion ; notices = $rustNotices ; target_libraries_sha256 = $rustLibraries }; runtime = $runtime } }
}
# Compatibility entry point for the reviewed Windows pipeline and its callers.
function Get-WindowsNotices([string]$Repository , [ string ] $Target , [ string ] $NoticeManifest , [ string ] $LlvmMinGWRoot ) {
Get-ArcadiaNotices $Repository $Target $NoticeManifest $LlvmMinGWRoot
2026-09-06 16:31:55 -07:00
}