@echo off chcp 65001 >nul setlocal title Mise a jour manuelle Blablaland N-500 set "N500_MANUAL_UPDATE_FILE=%~f0" powershell.exe -NoProfile -STA -Command "$file=$env:N500_MANUAL_UPDATE_FILE; $text=[IO.File]::ReadAllText($file,[Text.Encoding]::UTF8); $marker=':'+ '__N500_MANUAL_UPDATE__'; $index=$text.IndexOf($marker); if($index -lt 0){throw 'Fichier de mise a jour incomplet.'}; & ([ScriptBlock]::Create($text.Substring($index+$marker.Length)))" set "N500_MANUAL_UPDATE_EXIT=%ERRORLEVEL%" if not "%N500_MANUAL_UPDATE_EXIT%"=="0" ( echo. echo Mise a jour arretee. Consulte le message affiche ci-dessus. pause ) exit /b %N500_MANUAL_UPDATE_EXIT% :__N500_MANUAL_UPDATE__ # Candidate 1.7.4 template. pin-package.ps1 must bind the final verified ZIP before building CMD files. # Blablaland N-500 1.7.4 - migration manuelle transparente, Windows PowerShell 5.1. # Aucun programme externe n'est lancé. Les protections Windows restent actives. [CmdletBinding()] param([switch]$LibraryOnly, [switch]$VerifyOnly, [string]$PortableZipPath) function Get-BlablalandPackageInfo { [pscustomobject]@{Version='1.7.4';Url='https://wolfqc-bbl.duckdns.org/downloads/Blablaland-N500-Portable-1.7.4.zip';Bytes=[long]9943374;Sha256='53410424ba2c15a7e0e4010b25708ab5a40e502dd5412bccdf3f47609251668d';ManifestSha256='98a6fd8bcf4ed0e61f691a12e9d4639c254518f960b6f4cae7520adb65fd7991';Executable='Blablaland-N500-Public.exe'} } function Get-BlablalandFullPath([string]$Path) { if ([string]::IsNullOrWhiteSpace($Path)) { throw 'Chemin vide refusé.' } $full=[IO.Path]::GetFullPath($Path) if ($full.StartsWith('\\') -or $full -notmatch '^[A-Za-z]:\\' -or $full.Substring(2).Contains(':')) { throw 'Un chemin normal sur un disque local est nécessaire.' } return $full.TrimEnd([char[]]'\/') } function Test-BlablalandSamePath([string]$Left,[string]$Right) { if ([string]::IsNullOrWhiteSpace($Left) -or [string]::IsNullOrWhiteSpace($Right)) { return $false } try { return [string]::Equals((Get-BlablalandFullPath ([Environment]::ExpandEnvironmentVariables($Left))),(Get-BlablalandFullPath $Right),[StringComparison]::OrdinalIgnoreCase) } catch { return $false } } function Test-BlablalandInside([string]$Path,[string]$Root) { return (Get-BlablalandFullPath $Path).StartsWith((Get-BlablalandFullPath $Root)+'\',[StringComparison]::OrdinalIgnoreCase) } function Assert-BlablalandNoReparse([string]$Path) { $cursor=[IO.Path]::GetFullPath($Path) while ($cursor) { if (Test-Path -LiteralPath $cursor) { if (([IO.File]::GetAttributes($cursor) -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Lien ou point de jonction refusé : $cursor" } } if ($cursor -eq [IO.Path]::GetPathRoot($cursor)) { break } $cursor=[IO.Path]::GetDirectoryName($cursor) } } function Assert-BlablalandPublicPath([string]$Path) { $plain=[regex]::Replace($Path.Normalize([Text.NormalizationForm]::FormD),'\p{Mn}','').ToLowerInvariant() if ($plain.Contains('prive') -or $plain.Contains('blablafantasy')) { throw 'Le lanceur privé et les installations des autres serveurs sont exclus de cette migration.' } } function Get-BlablalandStreamHash([IO.Stream]$Stream) { $algorithm=[Security.Cryptography.SHA256]::Create() try { return ([BitConverter]::ToString($algorithm.ComputeHash($Stream))).Replace('-','').ToLowerInvariant() } finally { $algorithm.Dispose() } } function Get-BlablalandFileHash([string]$Path) { $stream=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read) try { return Get-BlablalandStreamHash $stream } finally { $stream.Dispose() } } function Get-BlablalandBytesHash([byte[]]$Bytes) { $stream=[IO.MemoryStream]::new($Bytes,$false) try { return Get-BlablalandStreamHash $stream } finally { $stream.Dispose() } } function New-BlablalandFile([string]$Path,[byte[]]$Bytes) { Assert-BlablalandNoReparse $Path $stream=[IO.File]::Open($Path,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None) try { $stream.Write($Bytes,0,$Bytes.Length) } finally { $stream.Dispose() } } function Set-BlablalandInternetZone([string]$Path) { Assert-BlablalandNoReparse $Path $drive=[IO.DriveInfo]::new([IO.Path]::GetPathRoot($Path)) if ($drive.DriveFormat -ne 'NTFS' -and $drive.DriveFormat -ne 'ReFS') { return $false } $origin=(Get-BlablalandPackageInfo).Url # Conserver la provenance Internet pour les contrôles de Windows et de l'antivirus. $zone="[ZoneTransfer]`r`nZoneId=3`r`nHostUrl=$origin`r`n" Set-Content -LiteralPath $Path -Stream Zone.Identifier -Value $zone -NoNewline -Encoding ASCII if ((Get-Content -LiteralPath $Path -Stream Zone.Identifier -Raw) -cne $zone) { throw 'La provenance Internet du fichier nʼa pas été conservée.' } return $true } function Assert-BlablalandArchivePath([string]$RelativePath) { if ([string]::IsNullOrWhiteSpace($RelativePath) -or $RelativePath.Contains('\') -or $RelativePath.Contains(':') -or $RelativePath.StartsWith('/') -or [IO.Path]::IsPathRooted($RelativePath)) { throw 'Chemin dʼarchive refusé.' } foreach ($segment in $RelativePath.Split('/')) { if ([string]::IsNullOrWhiteSpace($segment) -or $segment -eq '.' -or $segment -eq '..' -or $segment.EndsWith('.') -or $segment.EndsWith(' ') -or $segment.IndexOfAny([IO.Path]::GetInvalidFileNameChars()) -ge 0) { throw 'Nom de fichier dʼarchive refusé.' } if ($segment.Split('.')[0] -match '^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$') { throw 'Nom Windows réservé dans lʼarchive.' } } } function Get-BlablalandAllowedFiles { @('Blablaland-N500-Public.exe','public-client.swf','chat-public-air.swf','external-public-air.swf','mimetype','META-INF/signatures.xml','META-INF/AIR/application.xml','META-INF/AIR/hash','META-INF/AIR/license.txt','Adobe AIR/Versions/1.0/Adobe AIR.dll','Adobe AIR/Versions/1.0/Resources/Adobe AIR.vch','Adobe AIR/Versions/1.0/Resources/adobecp.vch','Adobe AIR/Versions/1.0/Resources/CaptiveAppEntry.exe','Adobe AIR/Versions/1.0/Resources/CaptiveCmdEntry.exe','Adobe AIR/Versions/1.0/Resources/DDrawFontSupport.dll','Adobe AIR/Versions/1.0/Resources/Licenses/cairo/COPYING','Adobe AIR/Versions/1.0/Resources/Licenses/cairo/COPYING-LGPL-2.1','Adobe AIR/Versions/1.0/Resources/Licenses/cairo/COPYING-MPL-1.1','Adobe AIR/Versions/1.0/Resources/Licenses/pcre2/COPYING','Adobe AIR/Versions/1.0/Resources/Licenses/pixman/COPYING') } function Read-BlablalandArchive([IO.Compression.ZipArchive]$Archive) { $info=Get-BlablalandPackageInfo if ($Archive.Entries.Count -ne 21) { throw 'Nombre de fichiers inattendu dans lʼarchive.' } $entry=$Archive.GetEntry('package-manifest.json') if (!$entry -or $entry.Length -gt 65536) { throw 'Manifeste absent ou trop volumineux.' } $stream=$entry.Open();$memory=[IO.MemoryStream]::new() try { $stream.CopyTo($memory);$manifestBytes=$memory.ToArray() } finally { $stream.Dispose();$memory.Dispose() } if ((Get-BlablalandBytesHash $manifestBytes) -cne $info.ManifestSha256) { throw 'Le manifeste public ne correspond pas à la version attendue.' } $manifest=[Text.Encoding]::UTF8.GetString($manifestBytes).TrimStart([char]0xFEFF)|ConvertFrom-Json if ($manifest.product -cne 'Blablaland : Server N-500' -or $manifest.version -cne $info.Version -or $manifest.applicationId -cne 'org.duckdns.wolfqcbbl.public.air' -or $manifest.executable -cne $info.Executable -or @($manifest.files).Count -ne 20) { throw 'Identité du manifeste refusée.' } $allowed=@(Get-BlablalandAllowedFiles);$expected=@{} foreach ($file in $manifest.files) { Assert-BlablalandArchivePath $file.path if ($allowed -cnotcontains $file.path -or $expected.ContainsKey($file.path) -or $file.bytes -lt 0 -or $file.bytes -gt 1GB -or $file.sha256 -cnotmatch '^[a-f0-9]{64}$') { throw 'Entrée du manifeste refusée.' } $expected[$file.path]=$file } $seen=@{};$expanded=[long]0 foreach ($entry in $Archive.Entries) { Assert-BlablalandArchivePath $entry.FullName $kind=($entry.ExternalAttributes -shr 16) -band 0xF000 if (($kind -ne 0 -and $kind -ne 0x8000) -or ($entry.ExternalAttributes -band 0x400) -ne 0 -or $seen.ContainsKey($entry.FullName)) { throw 'Lien, dossier spécial ou doublon refusé dans lʼarchive.' } $seen[$entry.FullName]=$true;$expanded+=$entry.Length if ($expanded -gt 1GB) { throw 'Archive trop volumineuse.' } if ($entry.FullName -ceq 'package-manifest.json') { $expectedHash=$info.ManifestSha256;$expectedBytes=$manifestBytes.Length } elseif ($expected.ContainsKey($entry.FullName) -and $expected[$entry.FullName].path -ceq $entry.FullName) { $expectedHash=$expected[$entry.FullName].sha256;$expectedBytes=$expected[$entry.FullName].bytes } else { throw "Fichier inattendu dans lʼarchive : $($entry.FullName)" } if ($entry.Length -ne $expectedBytes) { throw "Taille incorrecte : $($entry.FullName)" } $stream=$entry.Open() try { if ((Get-BlablalandStreamHash $stream) -cne $expectedHash) { throw "Fichier corrompu : $($entry.FullName)" } } finally { $stream.Dispose() } } return [pscustomobject]@{ok=$true;manifest=$manifest;expected=$expected;fileCount=20;archiveEntries=21;expandedBytes=$expanded;sha256=$info.Sha256;manifestBytes=$manifestBytes} } function Open-BlablalandVerifiedZip([string]$Path) { $full=Get-BlablalandFullPath $Path;Assert-BlablalandNoReparse $full;$info=Get-BlablalandPackageInfo $stream=[IO.File]::Open($full,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read) try { if ($stream.Length -ne $info.Bytes -or (Get-BlablalandStreamHash $stream) -cne $info.Sha256) { throw 'Lʼarchive ne correspond pas au fichier public attendu (taille ou SHA-256).' } $stream.Position=0 return [IO.Compression.ZipArchive]::new($stream,[IO.Compression.ZipArchiveMode]::Read,$false) } catch { $stream.Dispose();throw } } function Test-BlablalandPortable { [CmdletBinding()]param([Parameter(Mandatory=$true)][string]$PortableZipPath) $archive=Open-BlablalandVerifiedZip $PortableZipPath try { return Read-BlablalandArchive $archive } finally { $archive.Dispose() } } function Receive-BlablalandPortable { $info=Get-BlablalandPackageInfo $base=Get-BlablalandFullPath ([IO.Path]::GetTempPath());Assert-BlablalandNoReparse $base $folder=Join-Path $base ('N500-Portable-'+[Guid]::NewGuid().ToString('N')) if (Test-Path -LiteralPath $folder) { throw 'Le dossier temporaire existe déjà.' } [void][IO.Directory]::CreateDirectory($folder);Assert-BlablalandNoReparse $folder $path=Join-Path $folder 'Blablaland-N500-Portable-1.7.4.zip' Write-Host "Téléchargement de lʼarchive publique : $($info.Url)" Write-Host "Destination temporaire : $path" [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $request=[Net.HttpWebRequest]::Create($info.Url) $request.AllowAutoRedirect=$false;$request.Timeout=30000;$request.ReadWriteTimeout=30000;$response=$null try { $response=$request.GetResponse() if ([int]$response.StatusCode -ne 200 -or $response.ResponseUri.AbsoluteUri -cne $info.Url -or ($response.ContentLength -ge 0 -and $response.ContentLength -ne $info.Bytes)) { throw 'Réponse inattendue ou redirection refusée.' } $inputStream=$response.GetResponseStream() try { $output=[IO.File]::Open($path,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None) try { $buffer=New-Object byte[] 65536;$received=[long]0 while (($count=$inputStream.Read($buffer,0,$buffer.Length)) -gt 0) { $received+=$count;if ($received -gt $info.Bytes) { throw 'Archive trop volumineuse.' };$output.Write($buffer,0,$count) } } finally { $output.Dispose() } } finally { $inputStream.Dispose() } } finally { if ($response) { $response.Dispose() } if ([IO.File]::Exists($path)) { $marked=Set-BlablalandInternetZone $path;if (!$marked) { Write-Warning 'Ce système de fichiers ne conserve pas la marque Internet du ZIP.' } } } $verified=Test-BlablalandPortable $path Write-Host 'Archive publique et ses 20 fichiers vérifiés.' return $path } function Assert-BlablalandLegacy([string]$OldExecutable) { $exe=Get-BlablalandFullPath $OldExecutable Assert-BlablalandNoReparse $exe;Assert-BlablalandPublicPath $exe if (![IO.File]::Exists($exe)) { throw 'Le lanceur sélectionné est introuvable.' } $root=[IO.Path]::GetDirectoryName($exe) if ((Get-BlablalandFullPath $root) -eq ([IO.Path]::GetPathRoot($root).TrimEnd('\'))) { throw 'Un lanceur à la racine du disque est refusé.' } if ([IO.Path]::GetFileName($exe) -ieq 'Blablaland-N500-Public.exe') { foreach ($leaf in @('public-client.swf','chat-public-air.swf','external-public-air.swf','META-INF/AIR/application.xml')) { $file=Join-Path $root $leaf;Assert-BlablalandNoReparse $file if (![IO.File]::Exists($file)) { throw 'Le dossier du lanceur public est incomplet.' } } $descriptor=Join-Path $root 'META-INF/AIR/application.xml' if ([IO.FileInfo]::new($descriptor).Length -gt 65536) { throw 'Descripteur AIR trop volumineux.' } $settings=[Xml.XmlReaderSettings]::new();$settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit;$settings.XmlResolver=$null $doc=[Xml.XmlDocument]::new();$doc.XmlResolver=$null $reader=[Xml.XmlReader]::Create($descriptor,$settings) try { $doc.Load($reader) } finally { $reader.Dispose() } $id=$doc.SelectSingleNode("/*[local-name()='application']/*[local-name()='id']") $version=$doc.SelectSingleNode("/*[local-name()='application']/*[local-name()='versionNumber']") $filename=$doc.SelectSingleNode("/*[local-name()='application']/*[local-name()='filename']") $parsed=$null if (!$id -or $id.InnerText -cne 'org.duckdns.wolfqcbbl.public.air' -or !$filename -or $filename.InnerText -cne 'Blablaland-N500-Public' -or !$version -or ![Version]::TryParse($version.InnerText,[ref]$parsed) -or $parsed -gt [Version]'1.7.4') { throw 'Ce dossier AIR nʼest pas une version publique compatible (une version plus récente est refusée).' } return [pscustomobject]@{executable=$exe;directory=$root;kind='public-air';version=$parsed.ToString()} } if (@('Blablaland Desktop.exe','Blablaland.exe') -inotcontains [IO.Path]::GetFileName($exe)) { throw 'Ce fichier nʼest pas un ancien lanceur Blablaland Desktop reconnu.' } $asar=Join-Path $root 'resources/app.asar';Assert-BlablalandNoReparse $asar if (![IO.File]::Exists($asar)) { throw 'Sélectionne lʼancien lanceur Electron avec son dossier resources et app.asar.' } $versionInfo=[Diagnostics.FileVersionInfo]::GetVersionInfo($exe) if (!$versionInfo.ProductName -or $versionInfo.ProductName.IndexOf('Blablaland',[StringComparison]::OrdinalIgnoreCase) -lt 0) { throw 'Lʼidentité Blablaland de lʼancien lanceur nʼa pas été confirmée.' } return [pscustomobject]@{executable=$exe;directory=$root;kind='electron';version=$versionInfo.ProductVersion} } function Get-BlablalandMigrationParent([string]$OldExecutable) { $root=[IO.Path]::GetDirectoryName((Get-BlablalandFullPath $OldExecutable)) $parent=[IO.Path]::GetDirectoryName($root);$folderVersion=$null if ([IO.Path]::GetFileName($OldExecutable) -ieq 'Blablaland-N500-Public.exe' -and [Version]::TryParse([IO.Path]::GetFileName($root),[ref]$folderVersion) -and [IO.Path]::GetFileName($parent) -ieq 'Blablaland-N500-Public') { $parent=[IO.Path]::GetDirectoryName($parent) } return $parent } function Get-BlablalandDefaultDestination([string]$OldExecutable) { $base=Join-Path (Get-BlablalandMigrationParent $OldExecutable) 'Blablaland-N500-Public' for ($index=0;$index -lt 1000;$index++) { $name=if ($index -eq 0) { '1.7.4' } else { '1.7.4-copie-'+$index } $candidate=Join-Path $base $name if (!(Test-Path -LiteralPath $candidate)) { return $candidate } } throw 'Aucun nom de dossier de destination libre.' } function Assert-BlablalandEmptyDestination([string]$Path,[string]$OldExecutable) { $destination=Get-BlablalandFullPath $Path $oldDirectory=[IO.Path]::GetDirectoryName((Get-BlablalandFullPath $OldExecutable)) $parent=Get-BlablalandMigrationParent $OldExecutable if (!(Test-BlablalandInside $destination $parent) -or (Test-BlablalandSamePath $destination $oldDirectory) -or (Test-BlablalandInside $destination $oldDirectory) -or (Test-BlablalandInside $oldDirectory $destination)) { throw 'Le nouveau lanceur doit être installé séparément à côté de lʼancien.' } Assert-BlablalandPublicPath $destination;Assert-BlablalandNoReparse $destination if ([IO.File]::Exists($destination) -or ([IO.Directory]::Exists($destination) -and @([IO.Directory]::EnumerateFileSystemEntries($destination)).Count -gt 0)) { throw 'Le dossier de destination doit être nouveau ou complètement vide.' } return $destination } function Read-BlablalandShortcut([string]$Path) { $shell=$null;$link=$null try { $shell=New-Object -ComObject WScript.Shell;$link=$shell.CreateShortcut($Path) return [pscustomobject]@{TargetPath=[string]$link.TargetPath;Arguments=[string]$link.Arguments;WorkingDirectory=[string]$link.WorkingDirectory;IconLocation=[string]$link.IconLocation;Description=[string]$link.Description} } finally { if ($link) { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($link) };if ($shell) { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($shell) } } } function Test-BlablalandStandardShortcut($Values,[string]$Target) { if (![string]::IsNullOrWhiteSpace($Values.Arguments)) { return $false } if (![string]::IsNullOrWhiteSpace($Values.WorkingDirectory) -and !(Test-BlablalandSamePath $Values.WorkingDirectory ([IO.Path]::GetDirectoryName($Target)))) { return $false } $icon=([string]$Values.IconLocation).Trim() if ($icon.Length -gt 0 -and $icon -ne ',0') { $separator=$icon.LastIndexOf(',');$iconPath=$icon if ($separator -ge 0) { $iconIndex=0 if (![int]::TryParse($icon.Substring($separator+1).Trim(),[ref]$iconIndex) -or $iconIndex -ne 0) { return $false } $iconPath=$icon.Substring(0,$separator) } if (!(Test-BlablalandSamePath ($iconPath.Trim().Trim('"')) $Target)) { return $false } } $descriptions=@('','Blablaland','Blablaland Desktop','Blablaland-N500-Public','Blablaland N-500','Blablaland N-500 — En ligne','Blablaland : Server N-500','Blablaland : Server N-500 — en ligne') return $descriptions -icontains ([string]$Values.Description).Trim() } function Test-BlablalandStartupPath([string]$Path) { if ((Get-BlablalandFullPath $Path).Split('\') -icontains 'Startup') { return $true } foreach ($folder in @([Environment]::GetFolderPath('Startup'),[Environment]::GetFolderPath('CommonStartup'))) { if ($folder -and ((Test-BlablalandSamePath $Path $folder) -or (Test-BlablalandInside $Path $folder))) { return $true } } return $false } function Get-BlablalandShortcutFiles([string]$Root) { Assert-BlablalandNoReparse $Root foreach ($entry in [IO.Directory]::EnumerateFileSystemEntries($Root)) { if (Test-BlablalandStartupPath $entry) { continue } $attributes=[IO.File]::GetAttributes($entry) if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue } if (($attributes -band [IO.FileAttributes]::Directory) -ne 0) { Get-BlablalandShortcutFiles $entry } elseif ([IO.Path]::GetExtension($entry) -ieq '.lnk') { $entry } } } function Get-BlablalandShortcutPlan([string]$OldExecutable,[string]$NewExecutable,[string[]]$ShortcutRoots) { $roots=[Collections.Generic.List[string]]::new();$skipped=[Collections.Generic.List[object]]::new() $targets=[Collections.Generic.List[object]]::new();$preserved=[Collections.Generic.List[string]]::new();$seen=@{} foreach ($rootInput in $ShortcutRoots) { if ([string]::IsNullOrWhiteSpace($rootInput)) { continue } try { $root=Get-BlablalandFullPath $rootInput;Assert-BlablalandNoReparse $root if (!(Test-Path -LiteralPath $root -PathType Container) -or (Test-BlablalandStartupPath $root)) { throw 'Dossier absent ou dossier de démarrage automatique exclu.' } if ((Get-BlablalandFullPath $root) -eq [IO.Path]::GetPathRoot($root).TrimEnd('\')) { throw 'Une racine de disque ne peut pas contenir les raccourcis de migration.' } if ($seen.ContainsKey($root)) { continue };$seen[$root]=$true;$roots.Add($root) foreach ($file in @(Get-BlablalandShortcutFiles $root)) { if ($seen.ContainsKey($file)) { continue };$seen[$file]=$true try { Assert-BlablalandNoReparse $file;$values=Read-BlablalandShortcut $file if (Test-BlablalandSamePath $values.TargetPath $OldExecutable) { if (Test-BlablalandStandardShortcut $values $OldExecutable) { $bytes=[IO.File]::ReadAllBytes($file) $targets.Add([pscustomobject]@{path=$file;before=$bytes;beforeHash=(Get-BlablalandBytesHash $bytes)}) } else { $preserved.Add($file) } } } catch { $preserved.Add($file) } } } catch { $skipped.Add([pscustomobject]@{path=$rootInput;reason=$_.Exception.Message}) } } $newShortcut=$null if ($targets.Count -eq 0 -and $roots.Count -gt 0) { for ($index=0;$index -lt 1000;$index++) { $suffix=if ($index -eq 0) { '' } else { ' ('+$index+')' } $candidate=Join-Path $roots[0] ('Blablaland N-500 — En ligne'+$suffix+'.lnk') if (!(Test-Path -LiteralPath $candidate)) { $newShortcut=$candidate;break } } if (!$newShortcut) { throw 'Aucun nom de raccourci libre.' } } return [pscustomobject]@{targets=$targets.ToArray();preserved=$preserved.ToArray();skippedRoots=$skipped.ToArray();safeRoots=$roots.ToArray();newShortcut=$newShortcut;oldExecutable=$OldExecutable;newExecutable=$NewExecutable} } function Write-BlablalandShortcut([string]$Path,[string]$Target) { Assert-BlablalandNoReparse $Path;$shell=$null;$link=$null try { $shell=New-Object -ComObject WScript.Shell;$link=$shell.CreateShortcut($Path) $link.TargetPath=$Target;$link.Arguments='';$link.WorkingDirectory=[IO.Path]::GetDirectoryName($Target) $link.IconLocation=$Target+',0';$link.Description='Blablaland : Server N-500 — en ligne';$link.Save() } finally { if ($link) { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($link) };if ($shell) { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($shell) } } } function Write-BlablalandJournal([string]$Directory,$Plan,$Changes,[string]$State) { Assert-BlablalandNoReparse $Directory $record=[ordered]@{state=$State;utc=[DateTime]::UtcNow.ToString('o');oldExecutable=$Plan.oldExecutable;newExecutable=$Plan.newExecutable;shortcuts=@($Changes|ForEach-Object { [ordered]@{path=$_.path;backup=$_.backup;created=$_.created;writtenHash=$_.writtenHash;applied=$_.applied} })} $text=$record|ConvertTo-Json -Depth 6;$path=Join-Path $Directory 'migration-journal.json' $temporary=Join-Path $Directory ('journal-'+[Guid]::NewGuid().ToString('N')+'.tmp') New-BlablalandFile $temporary ([Text.UTF8Encoding]::new($false).GetBytes($text));Assert-BlablalandNoReparse $path if ([IO.File]::Exists($path)) { [IO.File]::Replace($temporary,$path,[NullString]::Value) } else { [IO.File]::Move($temporary,$path) } } function Invoke-BlablalandMigration { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$OldExecutable, [Parameter(Mandatory=$true)][string]$DestinationDirectory, [Parameter(Mandatory=$true)][string]$PortableZipPath, [Parameter(Mandatory=$true)][AllowEmptyCollection()][string[]]$ShortcutRoots, [scriptblock]$ConfirmAction ) $ErrorActionPreference='Stop' $legacy=Assert-BlablalandLegacy $OldExecutable $destination=Assert-BlablalandEmptyDestination $DestinationDirectory $legacy.executable $newExe=Join-Path $destination (Get-BlablalandPackageInfo).Executable $plan=Get-BlablalandShortcutPlan $legacy.executable $newExe $ShortcutRoots $archive=Open-BlablalandVerifiedZip $PortableZipPath $changes=[Collections.Generic.List[object]]::new();$backup=$null;$writesStarted=$false;$zoneSupported=$true try { $package=Read-BlablalandArchive $archive if ($ConfirmAction -and !(& $ConfirmAction $plan)) { return [pscustomobject]@{ok=$false;cancelled=$true;writesStarted=$false;oldFilesChanged=$false;gameLaunched=$false} } [void](Assert-BlablalandLegacy $legacy.executable) [void](Assert-BlablalandEmptyDestination $destination $legacy.executable) foreach ($target in $plan.targets) { Assert-BlablalandNoReparse $target.path if ((Get-BlablalandFileHash $target.path) -cne $target.beforeHash) { throw 'Un raccourci a changé depuis la confirmation. Relance la migration.' } } $writesStarted=$true [void][IO.Directory]::CreateDirectory($destination);Assert-BlablalandNoReparse $destination foreach ($entry in $archive.Entries) { $target=[IO.Path]::GetFullPath((Join-Path $destination $entry.FullName.Replace('/','\'))) if (!(Test-BlablalandInside $target $destination)) { throw 'Sortie du dossier dʼinstallation refusée.' } $parent=[IO.Path]::GetDirectoryName($target);Assert-BlablalandNoReparse $parent [void][IO.Directory]::CreateDirectory($parent);Assert-BlablalandNoReparse $target $source=$entry.Open() try { $output=[IO.File]::Open($target,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None) try { $source.CopyTo($output) } finally { $output.Dispose() } } finally { $source.Dispose() } $expectedHash=if ($entry.FullName -ceq 'package-manifest.json') { (Get-BlablalandPackageInfo).ManifestSha256 } else { $package.expected[$entry.FullName].sha256 } if ((Get-BlablalandFileHash $target) -cne $expectedHash) { throw "Vérification du fichier installé échouée : $($entry.FullName)" } if (!(Set-BlablalandInternetZone $target)) { $zoneSupported=$false } } # Une protection peut retirer un composant pendant la copie : vérifier # l'ensemble encore présent avant de toucher aux raccourcis. foreach ($entry in $archive.Entries) { $installed=Join-Path $destination $entry.FullName.Replace('/','\') Assert-BlablalandNoReparse $installed $expectedHash=if ($entry.FullName -ceq 'package-manifest.json') { (Get-BlablalandPackageInfo).ManifestSha256 } else { $package.expected[$entry.FullName].sha256 } if (![IO.File]::Exists($installed) -or [IO.FileInfo]::new($installed).Length -ne $entry.Length -or (Get-BlablalandFileHash $installed) -cne $expectedHash) { throw ('Un composant installé est absent ou a changé. Consulte les messages de Windows ou de ton antivirus : '+$entry.FullName) } } if ($plan.targets.Count -gt 0 -or $plan.newShortcut) { $backup=Join-Path (Get-BlablalandMigrationParent $legacy.executable) ('Blablaland-N500-Migration-Sauvegardes\'+[DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss')+'-'+[Guid]::NewGuid().ToString('N').Substring(0,8)) Assert-BlablalandNoReparse $backup;Assert-BlablalandPublicPath $backup if (Test-Path -LiteralPath $backup) { throw 'Dossier de sauvegarde déjà présent.' } [void][IO.Directory]::CreateDirectory($backup);Assert-BlablalandNoReparse $backup foreach ($target in $plan.targets) { $stored=Join-Path $backup ($changes.Count.ToString('D3')+'.lnk');New-BlablalandFile $stored $target.before $changes.Add([pscustomobject]@{path=$target.path;before=$target.before;beforeHash=$target.beforeHash;backup=$stored;created=$false;writtenHash=$null;applied=$false}) } if ($plan.newShortcut) { $changes.Add([pscustomobject]@{path=$plan.newShortcut;before=$null;beforeHash=$null;backup=$null;created=$true;writtenHash=$null;applied=$false}) } Write-BlablalandJournal $backup $plan $changes 'prepared' foreach ($change in $changes) { Assert-BlablalandNoReparse $change.path $near=Join-Path ([IO.Path]::GetDirectoryName($change.path)) ('.N500-migration-'+[Guid]::NewGuid().ToString('N')+'.lnk') Assert-BlablalandNoReparse $near if ($change.created) { New-BlablalandFile $near ([byte[]]@()) } else { New-BlablalandFile $near $change.before } try { # Partir de lʼancien raccourci garde ses autres propriétés standard. Write-BlablalandShortcut $near $newExe $check=Read-BlablalandShortcut $near if (!(Test-BlablalandSamePath $check.TargetPath $newExe) -or !(Test-BlablalandStandardShortcut $check $newExe)) { throw 'Le nouveau raccourci nʼa pas été vérifié.' } $change.writtenHash=Get-BlablalandFileHash $near Assert-BlablalandNoReparse $change.path if ($change.created) { if (Test-Path -LiteralPath $change.path) { throw 'Le nom du nouveau raccourci a été utilisé entre-temps.' } [IO.File]::Move($near,$change.path) } else { if ((Get-BlablalandFileHash $change.path) -cne $change.beforeHash) { throw 'Un raccourci a été modifié extérieurement. Il est conservé.' } [IO.File]::Replace($near,$change.path,[NullString]::Value) } $change.applied=$true if ((Get-BlablalandFileHash $change.path) -cne $change.writtenHash) { throw 'Vérification finale du raccourci échouée.' } Write-BlablalandJournal $backup $plan $changes 'updating' } finally { if ([IO.File]::Exists($near)) { Assert-BlablalandNoReparse $near;[IO.File]::Delete($near) } } } Write-BlablalandJournal $backup $plan $changes 'complete' } return [pscustomobject]@{ok=$true;cancelled=$false;version='1.7.4';oldExecutable=$legacy.executable;destination=$destination;newExecutable=$newExe;shortcutsUpdated=@($plan.targets).Count;shortcutCreated=$plan.newShortcut;preservedCustomShortcuts=@($plan.preserved).Count;skippedShortcutRoots=$plan.skippedRoots;backup=$backup;internetZonePreserved=$zoneSupported;oldFilesChanged=$false;gameLaunched=$false;processKilled=$false;filesVerified=20} } catch { $failure=$_;$rollbackErrors=[Collections.Generic.List[string]]::new() for ($index=$changes.Count-1;$index -ge 0;$index--) { $change=$changes[$index] if (!$change.applied) { continue } try { Assert-BlablalandNoReparse $change.path if (![IO.File]::Exists($change.path) -or (Get-BlablalandFileHash $change.path) -cne $change.writtenHash) { throw "Un changement externe du raccourci est conservé : $($change.path)" } if ($change.created) { [IO.File]::Delete($change.path) } else { $near=Join-Path ([IO.Path]::GetDirectoryName($change.path)) ('.N500-restauration-'+[Guid]::NewGuid().ToString('N')+'.lnk') New-BlablalandFile $near $change.before if ((Get-BlablalandFileHash $near) -cne $change.beforeHash) { throw 'Copie de restauration incohérente.' } Assert-BlablalandNoReparse $change.path [IO.File]::Replace($near,$change.path,[NullString]::Value) if ((Get-BlablalandFileHash $change.path) -cne $change.beforeHash) { throw 'Restauration du raccourci non vérifiée.' } } } catch { $rollbackErrors.Add($_.Exception.Message) } } if ($backup) { try { Write-BlablalandJournal $backup $plan $changes 'rolled-back-or-stopped' } catch { $rollbackErrors.Add($_.Exception.Message) } } $failure.Exception.Data['BackupDirectory']=$backup;$failure.Exception.Data['RollbackErrors']=$rollbackErrors.ToArray() $failure.Exception.Data['WritesStarted']=$writesStarted;$failure.Exception.Data['DestinationDirectory']=$destination throw $failure } finally { $archive.Dispose() } } function Find-BlablalandLegacyFromShortcuts([string[]]$ShortcutRoots) { $candidates=@{} foreach ($root in $ShortcutRoots) { try { if (!$root -or !(Test-Path -LiteralPath $root -PathType Container)) { continue } Assert-BlablalandNoReparse $root foreach ($file in @(Get-BlablalandShortcutFiles $root)) { try { $values=Read-BlablalandShortcut $file $legacy=Assert-BlablalandLegacy $values.TargetPath $candidates[$legacy.executable]=$legacy.executable } catch { } } } catch { } } return @($candidates.Values) } function Invoke-BlablalandInteractive([string]$ZipPath,[switch]$VerifyArchiveOnly) { if ($VerifyArchiveOnly) { if (!$ZipPath) { $ZipPath=Receive-BlablalandPortable } $result=Test-BlablalandPortable $ZipPath return [pscustomobject]@{ok=$true;verifiedOnly=$true;path=$ZipPath;sha256=$result.sha256;fileCount=$result.fileCount;installed=$false;gameLaunched=$false} } Add-Type -AssemblyName System.Windows.Forms [Windows.Forms.Application]::EnableVisualStyles() $roots=@([Environment]::GetFolderPath('DesktopDirectory'),[Environment]::GetFolderPath('Programs')) $candidates=@(Find-BlablalandLegacyFromShortcuts $roots) if ($candidates.Count -eq 1) { $oldExe=$candidates[0];Write-Host "Lanceur retrouvé dans tes raccourcis : $oldExe" } else { $dialog=[Windows.Forms.OpenFileDialog]::new() $dialog.Title='Choisis ton ancien lanceur Blablaland Desktop ou N500 Public' $dialog.Filter='Lanceur Blablaland (*.exe)|*.exe';$dialog.CheckFileExists=$true;$dialog.Multiselect=$false if ($candidates.Count -gt 0) { $dialog.InitialDirectory=[IO.Path]::GetDirectoryName($candidates[0]);$dialog.FileName=$candidates[0] } try { if ($dialog.ShowDialog() -ne [Windows.Forms.DialogResult]::OK) { return };$oldExe=$dialog.FileName } finally { $dialog.Dispose() } } [void](Assert-BlablalandLegacy $oldExe) $destination=Get-BlablalandDefaultDestination $oldExe if (!$ZipPath) { $ZipPath=Receive-BlablalandPortable } $confirm={ param($plan) $list=if ($plan.targets.Count -gt 0) { ($plan.targets|ForEach-Object { $_.path }) -join "`n" } elseif ($plan.newShortcut) { 'Créer : '+$plan.newShortcut } else { 'Aucun emplacement sûr : aucun raccourci ne sera modifié.' } $skipped=($plan.skippedRoots|ForEach-Object { $_.path+' : '+$_.reason }) -join "`n" $message="Ancien lanceur conservé :`n$($plan.oldExecutable)`n`nNouveau lanceur public 1.7.4 :`n$($plan.newExecutable)`n`nRaccourcis concernés :`n$list`n`nRaccourcis personnalisés ou illisibles laissés intacts : $($plan.preserved.Count)" if ($skipped) { $message+="`n`nDossiers de raccourcis ignorés :`n$skipped" } $message+="`n`nLes anciens fichiers et données restent intacts. Aucun jeu ne sera fermé ou lancé. Les contrôles Windows et antivirus restent actifs.`n`nContinuer ?" return [Windows.Forms.MessageBox]::Show($message,'Blablaland N-500 — mise à jour manuelle',[Windows.Forms.MessageBoxButtons]::OKCancel,[Windows.Forms.MessageBoxIcon]::Information) -eq [Windows.Forms.DialogResult]::OK } $result=Invoke-BlablalandMigration -OldExecutable $oldExe -DestinationDirectory $destination -PortableZipPath $ZipPath -ShortcutRoots $roots -ConfirmAction $confirm if ($result.cancelled) { Write-Host 'Migration annulée. Aucune installation ni aucun raccourci modifié.';return } $message="Mise à jour terminée. Lʼancien lanceur et ses données sont conservés.`n`nNouveau lanceur :`n$($result.newExecutable)`n`nAucun jeu nʼa été lancé automatiquement." if ($result.backup) { $message+="`n`nSauvegarde des raccourcis :`n$($result.backup)" } if (!$result.internetZonePreserved) { $message+="`n`nCe système de fichiers ne conserve pas la marque de provenance Internet. Les contrôles antivirus restent actifs." } [void][Windows.Forms.MessageBox]::Show($message,'Blablaland N-500',[Windows.Forms.MessageBoxButtons]::OK,[Windows.Forms.MessageBoxIcon]::Information) return $result } # Assemblages système uniquement ; aucune compilation de code. Add-Type -AssemblyName System.IO.Compression if (!$LibraryOnly) { $ErrorActionPreference='Stop' try { Invoke-BlablalandInteractive -ZipPath $PortableZipPath -VerifyArchiveOnly:$VerifyOnly } catch { $message='Mise à jour arrêtée : '+$_.Exception.Message+"`n`nLʼancien lanceur reste intact. La copie séparée éventuelle du nouveau lanceur est conservée." if ($_.Exception.Data['BackupDirectory']) { $message+="`nSauvegardes : "+$_.Exception.Data['BackupDirectory'] } if ($_.Exception.Data['RollbackErrors']) { $message+="`nRestauration à vérifier : "+($_.Exception.Data['RollbackErrors'] -join '; ') } Write-Host $message -ForegroundColor Red if (!$VerifyOnly) { Add-Type -AssemblyName System.Windows.Forms;[void][Windows.Forms.MessageBox]::Show($message,'Blablaland N-500',[Windows.Forms.MessageBoxButtons]::OK,[Windows.Forms.MessageBoxIcon]::Error) } exit 1 } }