param( [Parameter(Mandatory = $true)] [string]$OutputDir ) $ErrorActionPreference = "Stop" function Relative-Path([string]$Base, [string]$Path) { $basePath = [System.IO.Path]::GetFullPath($Base).TrimEnd([char[]]@('\', '/')) $fullPath = [System.IO.Path]::GetFullPath($Path) $prefix = $basePath + [System.IO.Path]::DirectorySeparatorChar if (-not $fullPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { throw "$fullPath is not inside $basePath" } $fullPath.Substring($prefix.Length) } $adbCandidates = @() if ($env:LOCALAPPDATA) { $adbCandidates += Join-Path $env:LOCALAPPDATA "Android\Sdk\platform-tools\adb.exe" } if ($env:ANDROID_HOME) { $adbCandidates += Join-Path $env:ANDROID_HOME "platform-tools\adb.exe" } $adbCandidates = @($adbCandidates | Where-Object { Test-Path -LiteralPath $_ }) if ($adbCandidates.Count -eq 0) { throw "adb.exe was not found under LOCALAPPDATA or ANDROID_HOME" } $adb = $adbCandidates[0] $devices = @(& $adb devices | Select-Object -Skip 1 | Where-Object { $_ -match "\tdevice$" }) if ($devices.Count -ne 1) { throw "Expected exactly one authorized device; found $($devices.Count). Check 'adb devices'." } $destination = [System.IO.Path]::GetFullPath($OutputDir) if (Test-Path -LiteralPath $destination) { if (@(Get-ChildItem -LiteralPath $destination -Force).Count -ne 0) { throw "Refusing to merge a recovery pull into non-empty directory: $destination" } } else { New-Item -ItemType Directory -Path $destination | Out-Null } $runtimeDir = Join-Path $destination "runtime" $captureDir = Join-Path $destination "captures" New-Item -ItemType Directory -Path $runtimeDir | Out-Null New-Item -ItemType Directory -Path $captureDir | Out-Null $runtimeRemote = "/data/local/tmp/dinovision" $capturesRemote = "/sdcard/Android/data/rust.dinovision_xr/files/captures" # adb pull is read-only on the headset. Pull the whole runtime directory first # so unknown historical files are preserved rather than guessed at by name. $runtimePresent = $false & $adb shell "test -d $runtimeRemote" if ($LASTEXITCODE -eq 0) { $runtimePresent = $true & $adb pull "$runtimeRemote/." $runtimeDir if ($LASTEXITCODE -ne 0) { throw "Failed to pull $runtimeRemote" } } $capturesPresent = $false & $adb shell "test -d $capturesRemote" if ($LASTEXITCODE -eq 0) { $capturesPresent = $true & $adb pull "$capturesRemote/." $captureDir if ($LASTEXITCODE -ne 0) { throw "Failed to pull $capturesRemote" } } $metadata = [ordered]@{ schema_version = 1 pulled_utc = [DateTime]::UtcNow.ToString("o") device = (& $adb get-serialno).Trim() product = (& $adb shell getprop ro.product.name).Trim() model = (& $adb shell getprop ro.product.model).Trim() build_fingerprint = (& $adb shell getprop ro.build.fingerprint).Trim() runtime_directory_present = $runtimePresent capture_directory_present = $capturesPresent files = @( Get-ChildItem -LiteralPath $destination -File -Recurse | ForEach-Object { [ordered]@{ path = (Relative-Path $destination $_.FullName).Replace("\", "/") bytes = $_.Length sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() } } ) } $manifestPath = Join-Path $destination "recovery_manifest.json" $metadata | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding utf8 Write-Host "Recovered $($metadata.files.Count) files to $destination" Write-Host "Manifest: $manifestPath"