using System.Diagnostics; using System.IO.Compression; using System.Security.Cryptography; using System.Text.Json; using Microsoft.Data.Sqlite; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Models; if(args.Length is <4 or >5||args[0] is not("import" or "activate" or "activate-move" or "reference" or "reopen" or "seal"))throw new ArgumentException("import|reference | reopen | seal | activate|activate-move "); var source=Path.GetFullPath(args[1]);var destination=Path.GetFullPath(args[2]);var report=Path.GetFullPath(args[3]); Directory.CreateDirectory(Path.GetDirectoryName(report)!); var timer=Stopwatch.StartNew();var json=new JsonSerializerOptions(ToolboxSettings.JsonOptions){WriteIndented=false}; void Report(object value){var temporary=report+".tmp";File.WriteAllText(temporary,JsonSerializer.Serialize(value,new JsonSerializerOptions{WriteIndented=true}));File.Move(temporary,report,true);} SqliteConnection Open(string file,SqliteOpenMode mode){var connection=new SqliteConnection(new SqliteConnectionStringBuilder{DataSource=file,Mode=mode,Pooling=false,DefaultTimeout=60}.ToString());connection.Open();return connection;} object[] Health(SqliteConnection connection) { using var command=connection.CreateCommand();command.CommandText="PRAGMA integrity_check"; using(var reader=command.ExecuteReader()){while(reader.Read())if(reader.GetString(0)!="ok")throw new InvalidDataException(reader.GetString(0));} command.CommandText="SELECT (SELECT count(*) FROM stations),(SELECT count(*) FROM snapshots),sqlite_version()"; long stationCount,snapshotCount;string version; using(var rows=command.ExecuteReader()){rows.Read();stationCount=rows.GetInt64(0);snapshotCount=rows.GetInt64(1);version=rows.GetString(2);} command.CommandText="SELECT count(*),max(q.timestamp),coalesce(sum(q.source!=2),0),coalesce(sum(s.station IS NULL OR s.timestamp!=q.timestamp),0) FROM quotes q LEFT JOIN snapshots s ON q.station=s.station"; using var quotes=command.ExecuteReader();quotes.Read(); if(stationCount==0||quotes.GetInt64(0)==0||quotes.GetInt64(2)!=0||quotes.GetInt64(3)!=0)throw new InvalidDataException("Incomplete imported cache or invalid provenance"); return [stationCount,quotes.GetInt64(0),snapshotCount,quotes.GetInt64(1),quotes.GetInt64(2),quotes.GetInt64(3),version]; } object[] Summary(SqliteConnection connection) { using var command=connection.CreateCommand(); command.CommandText="SELECT (SELECT count(*) FROM stations),(SELECT count(*) FROM quotes),(SELECT count(*) FROM snapshots),(SELECT max(timestamp) FROM quotes),sqlite_version()"; using var reader=command.ExecuteReader();reader.Read();var values=new object[reader.FieldCount];reader.GetValues(values);return values; } string ExclusiveSha256(string path) { using var file=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.None,4*1024*1024,FileOptions.SequentialScan); return Convert.ToHexStringLower(SHA256.HashData(file)); } if(args[0]=="seal") { using var activation=JsonDocument.Parse(File.ReadAllText(destination));var accepted=activation.RootElement; var sourceInfo=new FileInfo(source); if(accepted.GetProperty("status").GetString()!="activated"||!Path.GetFullPath(accepted.GetProperty("generation").GetString()!).Equals(source,StringComparison.OrdinalIgnoreCase) ||accepted.GetProperty("sourceSha256").GetString()!=accepted.GetProperty("generationSha256").GetString() ||sourceInfo.Length!=accepted.GetProperty("sourceBytes").GetInt64()||sourceInfo.LastWriteTimeUtc!=accepted.GetProperty("sourceModified").GetDateTime().ToUniversalTime()) throw new InvalidDataException("Generation differs from verified activation evidence"); var summary=accepted.GetProperty("reopenedSummary"); var health=new MarketCacheHealth(summary[0].GetInt64(),summary[1].GetInt64(),summary[2].GetInt64(),summary[4].GetString()!); new MarketRepository(source).RecordVerifiedGeneration(health,summary[3].GetInt64(),accepted.GetProperty("generationSha256").GetString()!); var probes=new List(); for(var attempt=1;attempt<=2;attempt++) { SqliteConnection.ClearAllPools();var startupRepository=new MarketRepository(accepted.GetProperty("original").GetString()!);var watch=Stopwatch.StartNew();startupRepository.Initialize();var initializeMs=watch.Elapsed.TotalMilliseconds; watch.Restart();var stats=startupRepository.Statistics();for(var sample=0;sample<9;sample++)startupRepository.Statistics();var statisticsMs=watch.Elapsed.TotalMilliseconds; if(stats.Stations!=health.Stations||stats.Quotes!=health.Quotes||stats.Latest?.ToUnixTimeMilliseconds()!=summary[3].GetInt64()||startupRepository.DatabasePath!=source) throw new InvalidDataException("Actual startup did not reopen the verified application generation"); probes.Add(new{attempt,initializeMs,tenStatisticsMs=statisticsMs,startupRepository.DatabasePath,stats.Stations,stats.Quotes,stats.Latest}); } Report(new{status="sealed-and-started",at=DateTimeOffset.UtcNow,source,activationReceipt=destination,proof=source+".verified-generation.json",health,probes,sourceBytes=sourceInfo.Length,elapsedSeconds=timer.Elapsed.TotalSeconds}); Console.WriteLine(JsonSerializer.Serialize(new{status="sealed-and-started",probes}));return; } if(args[0]=="reopen") { using var first=JsonDocument.Parse(File.ReadAllText(destination));var receipt=first.RootElement; var workspace=Path.GetFullPath(Path.Combine(Environment.CurrentDirectory,"artifacts"))+Path.DirectorySeparatorChar; if(!source.StartsWith(workspace,StringComparison.OrdinalIgnoreCase)||receipt.GetProperty("status").GetString()!="verified" ||!Path.GetFullPath(receipt.GetProperty("destination").GetString()!).Equals(source,StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Independent reopen requires this workspace's completed first integrity receipt"); var before=new FileInfo(source);var sourceBytes=before.Length;var modified=before.LastWriteTimeUtc; if(File.Exists(source+"-wal")&&new FileInfo(source+"-wal").Length!=0)throw new IOException("Independent import still has WAL content"); using(var exclusive=File.OpenHandle(source,FileMode.Open,FileAccess.Read,FileShare.None)){} var health=receipt.GetProperty("health").EnumerateArray().ToArray(); if(health.Length!=7||health[4].GetInt64()!=0||health[5].GetInt64()!=0)throw new InvalidDataException("First full integrity/provenance pass did not succeed"); object[] actual;long appliedCache,appliedMmap;string quickCheck; using(var input=Open(source,SqliteOpenMode.ReadOnly)) { using var configure=input.CreateCommand();configure.CommandText="PRAGMA query_only=ON; PRAGMA cache_size=-524288;";configure.ExecuteNonQuery(); configure.CommandText="PRAGMA cache_size";appliedCache=(long)configure.ExecuteScalar()!; configure.CommandText="PRAGMA mmap_size=2147483648";appliedMmap=(long)configure.ExecuteScalar()!; Report(new{status="reopening",pid=Environment.ProcessId,source,firstIntegrityReceipt=destination,connectionMode="ReadOnly",cacheKiB=appliedCache,mmapBytes=appliedMmap,at=DateTimeOffset.UtcNow}); Console.WriteLine($"Read-only reopen: cache={appliedCache}KiB mmap={appliedMmap}bytes"); configure.CommandText="PRAGMA quick_check(1)";quickCheck=(string)configure.ExecuteScalar()!; if(quickCheck!="ok")throw new InvalidDataException("Reopened cache failed quick_check: "+quickCheck); actual=Summary(input); } var expected=new[]{health[0],health[1],health[2],health[3],health[6]}; if(expected.Where((value,index)=>value.ToString()!=Convert.ToString(actual[index],System.Globalization.CultureInfo.InvariantCulture)).Any() ||new FileInfo(source).Length!=sourceBytes||File.GetLastWriteTimeUtc(source)!=modified) throw new InvalidDataException("Reopened cache differs from completed full integrity/provenance pass"); Report(new{status="verified",at=DateTimeOffset.UtcNow,source=receipt.GetProperty("source").GetString(),destination=source, compressedBytes=receipt.GetProperty("compressedBytes").GetInt64(),health=health.Select(value=>value.ValueKind==JsonValueKind.Number?(object)value.GetInt64():value.GetString()!).ToArray(), firstFullIntegrityReceipt=destination,verification="independent-read-only-reopen",connectionMode="ReadOnly",cacheKiB=appliedCache,mmapBytes=appliedMmap,quickCheck,reopenedSummary=actual, sourceBytes,sourceModified=modified,sourceUnchanged=true,elapsedSeconds=timer.Elapsed.TotalSeconds}); Console.WriteLine("Read-only reopen verified: "+source);return; } if(args[0].StartsWith("activate",StringComparison.Ordinal)) { // Only a completely built standalone SQLite file is eligible. No production file is overwritten. if(args.Length!=5)throw new ArgumentException("Activation requires the final verified import report"); using var importReport=JsonDocument.Parse(File.ReadAllText(Path.GetFullPath(args[4]))); var imported=importReport.RootElement; if(imported.GetProperty("status").GetString()!="verified"||!Path.GetFullPath(imported.GetProperty("destination").GetString()!).Equals(source,StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Activation receipt does not identify this completed import"); var sourceDump=Path.GetFullPath(imported.GetProperty("source").GetString()!); if(!File.Exists(sourceDump)||new FileInfo(sourceDump).Length!=imported.GetProperty("compressedBytes").GetInt64()) throw new InvalidDataException("Completed original source file does not match the import receipt"); if(source.Equals(destination,StringComparison.OrdinalIgnoreCase))throw new ArgumentException("Source and original path must differ"); var sourceInfo=new FileInfo(source);var sourceBytes=sourceInfo.Length;var sourceModified=sourceInfo.LastWriteTimeUtc; if(sourceModified>imported.GetProperty("at").GetDateTimeOffset().UtcDateTime||File.Exists(source+"-wal")&&new FileInfo(source+"-wal").Length!=0) throw new IOException("Completed import was modified or still has WAL content"); using(var exclusive=File.OpenHandle(source,FileMode.Open,FileAccess.ReadWrite,FileShare.None)){} object[] expected; using(var input=Open(source,SqliteOpenMode.ReadWrite)) { using var checkpoint=input.CreateCommand();checkpoint.CommandText="PRAGMA wal_checkpoint(TRUNCATE)"; using(var result=checkpoint.ExecuteReader()) {if(!result.Read()||result.GetInt64(0)!=0||result.GetInt64(1)>0)throw new IOException("Import source WAL checkpoint is still busy; do not move its main file");} expected=Summary(input); } var completeHealth=imported.GetProperty("health").EnumerateArray().ToArray(); if(completeHealth.Length!=7||completeHealth[4].GetInt64()!=0||completeHealth[5].GetInt64()!=0) throw new InvalidDataException("Completed integrity/provenance receipt is invalid"); var receipt=new[]{completeHealth[0],completeHealth[1],completeHealth[2],completeHealth[3],completeHealth[6]}; if(receipt.Length!=expected.Length||receipt.Where((value,index)=>value.ToString()!=Convert.ToString(expected[index],System.Globalization.CultureInfo.InvariantCulture)).Any()) throw new InvalidDataException("Database health differs from the completed import receipt"); Console.WriteLine("Hashing the closed, fully verified import before activation"); var sourceSha256=ExclusiveSha256(source); if(new FileInfo(source).Length!=sourceBytes||File.GetLastWriteTimeUtc(source)!=sourceModified) throw new IOException("Completed import changed while validating activation"); var previous=File.Exists(destination+".active")?File.ReadAllText(destination+".active").Trim():Path.GetFileName(destination); var generation=destination+".recovered-download-"+DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss")+"-"+Guid.NewGuid().ToString("N")[..8]+".db"; var move=args[0]=="activate-move"; if(move) { var workspace=Path.GetFullPath(Path.Combine(Environment.CurrentDirectory,"artifacts"))+Path.DirectorySeparatorChar; var applicationData=Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox"))+Path.DirectorySeparatorChar; if(!source.StartsWith(workspace,StringComparison.OrdinalIgnoreCase)||!generation.StartsWith(applicationData,StringComparison.OrdinalIgnoreCase) ||!Path.GetPathRoot(source)!.Equals(Path.GetPathRoot(generation),StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Move requires a validated workspace artifact and application-data generation on the same volume"); Console.WriteLine(JsonSerializer.Serialize(new{operation="move-verified-generation",source,generation,sourceBytes=new FileInfo(source).Length})); File.Move(source,generation,false); } else File.Copy(source,generation,false); object[] health;using(var check=Open(generation,SqliteOpenMode.ReadOnly))health=Summary(check); if(!health.SequenceEqual(expected))throw new InvalidDataException("Published generation differs from the completed import source"); Console.WriteLine("Hashing the reopened generation before publishing the active marker"); var generationSha256=ExclusiveSha256(generation); if(generationSha256!=sourceSha256||new FileInfo(generation).Length!=sourceBytes||File.GetLastWriteTimeUtc(generation)!=sourceModified) throw new InvalidDataException("Generation bytes changed during activation"); new MarketRepository(generation).RecordVerifiedGeneration(new(Convert.ToInt64(health[0]),Convert.ToInt64(health[1]),Convert.ToInt64(health[2]),(string)health[4]),Convert.ToInt64(health[3]),generationSha256); var marker=destination+".active";var temporary=marker+"."+Guid.NewGuid().ToString("N")+".tmp"; using(var markerStream=new FileStream(temporary,FileMode.CreateNew,FileAccess.Write,FileShare.Read,4096,FileOptions.WriteThrough)) {markerStream.Write(System.Text.Encoding.UTF8.GetBytes(Path.GetFileName(generation)));markerStream.Flush(true);} File.Move(temporary,marker,true); Report(new{status="activated",at=DateTimeOffset.UtcNow,source,sourceMoved=move,sourceDump,completedImportReport=Path.GetFullPath(args[4]),original=destination,previous,generation,sourceBytes,sourceModified,sourceSha256,generationSha256,reopenedSummary=health,integrityVerifiedByCompletedImport=true,startupProof=generation+".verified-generation.json",elapsedSeconds=timer.Elapsed.TotalSeconds}); Console.WriteLine(generation);return; } if(File.Exists(destination)||File.Exists(destination+"-wal")||File.Exists(destination+"-shm"))throw new IOException("Import requires a new isolated destination"); Directory.CreateDirectory(Path.GetDirectoryName(destination)!); var repository=new MarketRepository(destination);repository.Initialize(); if(args[0]=="reference") { using var http=new HttpClient();var result=await new SpanshImporter(repository,http).ImportFileAsync(source); SqliteConnection.ClearAllPools();using var check=Open(destination,SqliteOpenMode.ReadOnly);var health=Health(check); Report(new{status="reference-verified",source,destination,result,health,elapsedSeconds=timer.Elapsed.TotalSeconds});return; } long systems=0,stations=0,snapshots=0,quotes=0,invalidStations=0,futureSnapshots=0,olderSnapshots=0; DateTimeOffset? newest=null;var now=DateTimeOffset.UtcNow;var lastReport=TimeSpan.Zero; object? verifiedReport=null; await using var file=File.OpenRead(source); await using Stream stream=source.EndsWith(".gz",StringComparison.OrdinalIgnoreCase)?new GZipStream(file,CompressionMode.Decompress,true):file; using(var connection=Open(destination,SqliteOpenMode.ReadWrite)) { using(var settings=connection.CreateCommand()) {settings.CommandText="PRAGMA synchronous=NORMAL; PRAGMA cache_size=-131072; DROP INDEX idx_quotes_commodity; DROP INDEX idx_quotes_time;";settings.ExecuteNonQuery();} SqliteTransaction transaction=connection.BeginTransaction(); var commands=new List(); SqliteCommand Prepare(string sql,params string[] names) {var command=connection.CreateCommand();command.Transaction=transaction;command.CommandText=sql;foreach(var name in names)command.Parameters.AddWithValue(name,DBNull.Value);command.Prepare();commands.Add(command);return command;} var selectStation=Prepare("SELECT json FROM stations WHERE id=$id","$id"); var saveStation=Prepare("INSERT OR REPLACE INTO stations VALUES($id,$name,$system,$x,$y,$z,$json)","$id","$name","$system","$x","$y","$z","$json"); var selectSnapshot=Prepare("SELECT timestamp FROM snapshots WHERE station=$id","$id"); var deleteQuotes=Prepare("DELETE FROM quotes WHERE station=$id","$id"); var saveQuote=Prepare("INSERT INTO quotes VALUES($id,$key,$name,$buy,$sell,$supply,$demand,$time,$source,$json)","$id","$key","$name","$buy","$sell","$supply","$demand","$time","$source","$json"); var saveSnapshot=Prepare("INSERT OR REPLACE INTO snapshots VALUES($id,$time,$source)","$id","$time","$source"); void Put(SqliteCommand command,params object?[] values){for(var index=0;index(stream)) { var name=system.Text("name");var position=system.Position("coords");var security=JsonFields.Security(system.Text("security")); foreach(var item in system.Get("stations").Items().Concat(system.Get("bodies").Items().SelectMany(body=>body.Get("stations").Items())).DistinctBy(item=>item.Long("id"))) { var market=item.Get("market");var time=market.Time("updateTime")??item.Time("updateTime")??system.Time("date"); var type=item.Text("type");var pads=item.Get("landingPads"); var incoming=new Station{MarketId=item.Long("id"),Name=item.Text("name"),SystemName=name,Position=position,Security=security, DistanceToArrivalLs=item.Number("distanceToArrival"),Type=type, MaxLandingPad=pads.Long("large")>0?LandingPad.Large:pads.Long("medium")>0?LandingPad.Medium:pads.Long("small")>0?LandingPad.Small:LandingPad.Unknown, IsPlanetary=type.Contains("Planetary",StringComparison.OrdinalIgnoreCase)||type.Contains("Settlement",StringComparison.OrdinalIgnoreCase),IsFleetCarrier=type.Contains("Carrier",StringComparison.OrdinalIgnoreCase), RequiresPermit=system.Flag("permitRequired"),IsRestricted=item.Text("dockingAccess") is "friends" or "squadron" or "none",UpdatedAt=time??DateTimeOffset.MinValue}; if(incoming.MarketId<=0||incoming.Name.Length==0||name.Length==0){invalidStations++;continue;} Put(selectStation,incoming.MarketId);var old=selectStation.ExecuteScalar() is string text?JsonSerializer.Deserialize(text,json):null; var station=incoming; if(old!=null) { var newer=incoming.UpdatedAt>=old.UpdatedAt;var moved=!incoming.SystemName.Equals(old.SystemName,StringComparison.OrdinalIgnoreCase); if(!newer&&moved)station=old; else station=(newer?incoming:old) with{Name=incoming.Name,SystemName=newer?incoming.SystemName:old.SystemName,Position=newer&&moved?incoming.Position:incoming.Position??old.Position, DistanceToArrivalLs=incoming.DistanceToArrivalLs??old.DistanceToArrivalLs,Security=incoming.Security!=SecurityLevel.Unknown?incoming.Security:old.Security, MaxLandingPad=incoming.MaxLandingPad!=LandingPad.Unknown?incoming.MaxLandingPad:old.MaxLandingPad,Type=incoming.Type.Length>0?incoming.Type:old.Type, IsFleetCarrier=incoming.IsFleetCarrier||old.IsFleetCarrier,IsPlanetary=incoming.IsPlanetary||old.IsPlanetary,RequiresPermit=incoming.RequiresPermit||old.RequiresPermit,IsRestricted=incoming.IsRestricted||old.IsRestricted}; } Put(saveStation,station.MarketId,station.Name,station.SystemName,station.Position?.X,station.Position?.Y,station.Position?.Z,JsonSerializer.Serialize(station,json));saveStation.ExecuteNonQuery();stations++; if(time==null||market.Get("commodities").ValueKind!=JsonValueKind.Array)continue; if(time>now.AddMinutes(5)){futureSnapshots++;continue;} Put(selectSnapshot,incoming.MarketId);if(selectSnapshot.ExecuteScalar() is long previous&&previous>time.Value.ToUnixTimeMilliseconds()){olderSnapshots++;continue;} Put(deleteQuotes,incoming.MarketId);deleteQuotes.ExecuteNonQuery(); var prohibited=item.Get("prohibitedCommodities").Items().Concat(market.Get("prohibitedCommodities").Items()).Where(value=>value.ValueKind==JsonValueKind.String).Select(value=>CommodityCatalog.Key(value.GetString()!)).ToHashSet(); foreach(var quote in market.Get("commodities").Items().Select(value=>MarketParser.ParseQuote(value,incoming.MarketId,time.Value,DataSource.Spansh) with{Category=value.Text("category")}).Select(value=>value with{Illegal=value.Illegal||prohibited.Contains(value.Commodity)}).DistinctBy(value=>value.Commodity)) { if(string.IsNullOrWhiteSpace(quote.Commodity))continue; Put(saveQuote,quote.MarketId,quote.Commodity,quote.DisplayName,quote.BuyPrice,quote.SellPrice,quote.Supply,quote.Demand,time.Value.ToUnixTimeMilliseconds(),(int)DataSource.Spansh,JsonSerializer.Serialize(quote,json));saveQuote.ExecuteNonQuery();quotes++; } Put(saveSnapshot,incoming.MarketId,time.Value.ToUnixTimeMilliseconds(),(int)DataSource.Spansh);saveSnapshot.ExecuteNonQuery();snapshots++; if(newest==null||time>newest)newest=time; } systems++; if(systems%100==0){transaction.Commit();transaction.Dispose();transaction=connection.BeginTransaction();foreach(var command in commands)command.Transaction=transaction;} if(timer.Elapsed-lastReport>TimeSpan.FromSeconds(5)) {lastReport=timer.Elapsed;Report(new{status="importing",pid=Environment.ProcessId,source,destination,systems,stations,snapshots,quotes,newest,compressedBytesRead=file.Position,compressedBytes=file.Length,elapsedSeconds=timer.Elapsed.TotalSeconds});Console.WriteLine($"systems={systems} stations={stations} quotes={quotes} compressed={file.Position}/{file.Length}");} } transaction.Commit();transaction.Dispose();foreach(var command in commands){command.Transaction=null;command.Dispose();} Report(new{status="indexing",pid=Environment.ProcessId,source,destination,systems,stations,snapshots,quotes,compressedBytesRead=file.Position,compressedBytes=file.Length,elapsedSeconds=timer.Elapsed.TotalSeconds}); using var indexes=connection.CreateCommand();indexes.CommandText="CREATE INDEX idx_quotes_commodity ON quotes(commodity); CREATE INDEX idx_quotes_time ON quotes(timestamp DESC); ANALYZE; PRAGMA wal_checkpoint(TRUNCATE);";indexes.ExecuteNonQuery(); Report(new{status="validating",pid=Environment.ProcessId,source,destination,systems,stations,snapshots,quotes,elapsedSeconds=timer.Elapsed.TotalSeconds}); var health=Health(connection); verifiedReport=new{status="verified",at=DateTimeOffset.UtcNow,source,destination,systems,stationRecords=stations,snapshotRecords=snapshots,quoteRecords=quotes,invalidStations,futureSnapshots,olderSnapshots,newest,health,compressedBytes=file.Length,elapsedSeconds=timer.Elapsed.TotalSeconds}; } catch(Exception error) {transaction.Dispose();Report(new{status="failed",source,destination,systems,stations,snapshots,quotes,error=error.ToString(),elapsedSeconds=timer.Elapsed.TotalSeconds});throw;} } SqliteConnection.ClearAllPools(); Report(new{status="validating-reopen",pid=Environment.ProcessId,source,destination,systems,stations,snapshots,quotes,elapsedSeconds=timer.Elapsed.TotalSeconds}); try{using var reopened=Open(destination,SqliteOpenMode.ReadOnly);Health(reopened);Report(verifiedReport!);} catch(Exception error){Report(new{status="failed-reopen",source,destination,error=error.ToString()});throw;} Console.WriteLine("Complete and verified: "+destination);