[DEPRECATED] Go implementation of plcbundle

cmd verify

+357 -61
+355 -59
cmd/plcbundle/commands/verify.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "os" 7 + "time" 6 8 7 - flag "github.com/spf13/pflag" 8 - 9 + "github.com/spf13/cobra" 9 10 "tangled.org/atscan.net/plcbundle/bundle" 11 + "tangled.org/atscan.net/plcbundle/cmd/plcbundle/ui" 10 12 ) 11 13 12 - // VerifyCommand handles the verify subcommand 13 - func VerifyCommand(args []string) error { 14 - fs := flag.NewFlagSet("verify", flag.ExitOnError) 15 - bundleNum := fs.Int("bundle", 0, "specific bundle to verify (0 = verify chain)") 16 - verbose := fs.Bool("v", false, "verbose output") 14 + func NewVerifyCommand() *cobra.Command { 15 + var ( 16 + bundleNum int 17 + rangeStr string 18 + chain bool 19 + parallel bool 20 + workers int 21 + ) 17 22 18 - if err := fs.Parse(args); err != nil { 19 - return err 20 - } 23 + cmd := &cobra.Command{ 24 + Use: "verify [flags]", 25 + Short: "Verify bundle integrity and chain", 26 + Long: `Verify bundle integrity and cryptographic chain 21 27 22 - mgr, dir, err := getManager("") 23 - if err != nil { 24 - return err 25 - } 26 - defer mgr.Close() 28 + Verifies bundle file hashes, content integrity, and chain linkage. 29 + By default, verifies the entire chain. You can verify specific bundles 30 + or ranges using flags. 27 31 28 - fmt.Printf("Working in: %s\n", dir) 32 + Verification checks: 33 + • Compressed file hash matches index 34 + • Content hash matches index 35 + • File sizes match metadata 36 + • Chain parent references are correct 37 + • Chain hash calculations are valid`, 29 38 30 - ctx := context.Background() 39 + Example: ` # Verify entire chain 40 + plcbundle verify 41 + plcbundle verify --chain 42 + 43 + # Verify specific bundle 44 + plcbundle verify --bundle 42 31 45 32 - if *bundleNum > 0 { 33 - return verifySingleBundle(ctx, mgr, *bundleNum, *verbose) 46 + # Verify range of bundles 47 + plcbundle verify --range 1-100 48 + 49 + # Verbose output 50 + plcbundle verify --chain -v 51 + 52 + # Parallel verification (faster for ranges) 53 + plcbundle verify --range 1-1000 --parallel --workers 8`, 54 + 55 + RunE: func(cmd *cobra.Command, args []string) error { 56 + verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose") 57 + 58 + mgr, dir, err := getManagerFromCommand(cmd, "") 59 + if err != nil { 60 + return err 61 + } 62 + defer mgr.Close() 63 + 64 + if !verbose { 65 + fmt.Fprintf(os.Stderr, "Working in: %s\n\n", dir) 66 + } 67 + 68 + ctx := context.Background() 69 + 70 + // Determine what to verify 71 + if rangeStr != "" { 72 + return verifyRange(ctx, mgr, rangeStr, verbose, parallel, workers) 73 + } 74 + 75 + if bundleNum > 0 { 76 + return verifySingleBundle(ctx, mgr, bundleNum, verbose) 77 + } 78 + 79 + // Default: verify entire chain 80 + return verifyChain(ctx, mgr, verbose) 81 + }, 34 82 } 35 83 36 - return verifyChain(ctx, mgr, *verbose) 84 + cmd.Flags().IntVarP(&bundleNum, "bundle", "b", 0, "Verify specific bundle number") 85 + cmd.Flags().StringVarP(&rangeStr, "range", "r", "", "Verify bundle range (e.g., '1-100')") 86 + cmd.Flags().BoolVarP(&chain, "chain", "c", false, "Verify entire chain (default)") 87 + cmd.Flags().BoolVar(&parallel, "parallel", false, "Use parallel verification for ranges") 88 + cmd.Flags().IntVar(&workers, "workers", 4, "Number of parallel workers") 89 + 90 + return cmd 37 91 } 38 92 39 93 func verifySingleBundle(ctx context.Context, mgr *bundle.Manager, bundleNum int, verbose bool) error { 40 - fmt.Printf("Verifying bundle %06d...\n", bundleNum) 94 + fmt.Fprintf(os.Stderr, "Verifying bundle %06d...\n", bundleNum) 41 95 96 + start := time.Now() 42 97 result, err := mgr.VerifyBundle(ctx, bundleNum) 98 + elapsed := time.Since(start) 99 + 43 100 if err != nil { 44 101 return fmt.Errorf("verification failed: %w", err) 45 102 } 46 103 47 104 if result.Valid { 48 - fmt.Printf("✓ Bundle %06d is valid\n", bundleNum) 105 + fmt.Fprintf(os.Stderr, "✓ Bundle %06d is valid (%s)\n", bundleNum, elapsed.Round(time.Millisecond)) 49 106 if verbose { 50 - fmt.Printf(" File exists: %v\n", result.FileExists) 51 - fmt.Printf(" Hash match: %v\n", result.HashMatch) 52 - fmt.Printf(" Hash: %s...\n", result.LocalHash[:16]) 107 + fmt.Fprintf(os.Stderr, "\nDetails:\n") 108 + fmt.Fprintf(os.Stderr, " File exists: %v\n", result.FileExists) 109 + fmt.Fprintf(os.Stderr, " Hash match: %v\n", result.HashMatch) 110 + fmt.Fprintf(os.Stderr, " Hash: %s...%s\n", result.LocalHash[:16], result.LocalHash[len(result.LocalHash)-16:]) 111 + fmt.Fprintf(os.Stderr, " Verification time: %s\n", elapsed) 53 112 } 54 113 return nil 55 114 } 56 115 57 - fmt.Printf("✗ Bundle %06d is invalid\n", bundleNum) 116 + fmt.Fprintf(os.Stderr, "✗ Bundle %06d is invalid (%s)\n", bundleNum, elapsed.Round(time.Millisecond)) 58 117 if result.Error != nil { 59 - fmt.Printf(" Error: %v\n", result.Error) 118 + fmt.Fprintf(os.Stderr, " Error: %v\n", result.Error) 60 119 } 61 120 if !result.FileExists { 62 - fmt.Printf(" File not found\n") 121 + fmt.Fprintf(os.Stderr, " File not found\n") 63 122 } 64 123 if !result.HashMatch && result.FileExists { 65 - fmt.Printf(" Expected hash: %s...\n", result.ExpectedHash[:16]) 66 - fmt.Printf(" Actual hash: %s...\n", result.LocalHash[:16]) 124 + fmt.Fprintf(os.Stderr, " Expected hash: %s...\n", result.ExpectedHash[:16]) 125 + fmt.Fprintf(os.Stderr, " Actual hash: %s...\n", result.LocalHash[:16]) 67 126 } 68 127 return fmt.Errorf("bundle verification failed") 69 128 } ··· 73 132 bundles := index.GetBundles() 74 133 75 134 if len(bundles) == 0 { 76 - fmt.Println("No bundles to verify") 135 + fmt.Fprintf(os.Stderr, "No bundles to verify\n") 77 136 return nil 78 137 } 79 138 80 - fmt.Printf("Verifying chain of %d bundles...\n\n", len(bundles)) 139 + fmt.Fprintf(os.Stderr, "Verifying chain of %d bundles...\n\n", len(bundles)) 140 + 141 + start := time.Now() 142 + 143 + var progress *ui.ProgressBar 144 + if !verbose { 145 + progress = ui.NewProgressBar(len(bundles)) 146 + } 81 147 82 148 verifiedCount := 0 83 149 errorCount := 0 84 - lastPercent := -1 150 + var firstError error 85 151 86 152 for i, meta := range bundles { 87 153 bundleNum := meta.BundleNumber 88 154 89 - percent := (i * 100) / len(bundles) 90 - if percent != lastPercent || verbose { 91 - if verbose { 92 - fmt.Printf(" [%3d%%] Verifying bundle %06d...", percent, bundleNum) 93 - } else if percent%10 == 0 && percent != lastPercent { 94 - fmt.Printf(" [%3d%%] Verified %d/%d bundles...\n", percent, i, len(bundles)) 95 - } 96 - lastPercent = percent 155 + if verbose { 156 + fmt.Fprintf(os.Stderr, " Verifying bundle %06d... ", bundleNum) 97 157 } 98 158 99 159 result, err := mgr.VerifyBundle(ctx, bundleNum) 100 160 if err != nil { 101 161 if verbose { 102 - fmt.Printf(" ERROR\n") 162 + fmt.Fprintf(os.Stderr, "ERROR\n") 103 163 } 104 - fmt.Printf("\n✗ Failed to verify bundle %06d: %v\n", bundleNum, err) 164 + fmt.Fprintf(os.Stderr, "\n✗ Failed to verify bundle %06d: %v\n", bundleNum, err) 105 165 errorCount++ 166 + if firstError == nil { 167 + firstError = err 168 + } 106 169 continue 107 170 } 108 171 109 172 if !result.Valid { 110 173 if verbose { 111 - fmt.Printf(" INVALID\n") 174 + fmt.Fprintf(os.Stderr, "INVALID\n") 112 175 } 113 - fmt.Printf("\n✗ Bundle %06d hash verification failed\n", bundleNum) 176 + fmt.Fprintf(os.Stderr, "\n✗ Bundle %06d hash verification failed\n", bundleNum) 114 177 if result.Error != nil { 115 - fmt.Printf(" Error: %v\n", result.Error) 178 + fmt.Fprintf(os.Stderr, " Error: %v\n", result.Error) 179 + if firstError == nil { 180 + firstError = result.Error 181 + } 116 182 } 117 183 errorCount++ 118 184 continue 119 185 } 120 186 187 + // Verify chain link 121 188 if i > 0 { 122 189 prevMeta := bundles[i-1] 123 190 if meta.Parent != prevMeta.Hash { 124 191 if verbose { 125 - fmt.Printf(" CHAIN BROKEN\n") 192 + fmt.Fprintf(os.Stderr, "CHAIN BROKEN\n") 126 193 } 127 - fmt.Printf("\n✗ Chain broken at bundle %06d\n", bundleNum) 128 - fmt.Printf(" Expected parent: %s...\n", prevMeta.Hash[:16]) 129 - fmt.Printf(" Actual parent: %s...\n", meta.Parent[:16]) 194 + fmt.Fprintf(os.Stderr, "\n✗ Chain broken at bundle %06d\n", bundleNum) 195 + fmt.Fprintf(os.Stderr, " Expected parent: %s...\n", prevMeta.Hash[:16]) 196 + fmt.Fprintf(os.Stderr, " Actual parent: %s...\n", meta.Parent[:16]) 130 197 errorCount++ 198 + if firstError == nil { 199 + firstError = fmt.Errorf("chain broken at bundle %d", bundleNum) 200 + } 131 201 continue 132 202 } 133 203 } 134 204 135 205 if verbose { 136 - fmt.Printf(" ✓\n") 206 + fmt.Fprintf(os.Stderr, "✓\n") 137 207 } 138 208 verifiedCount++ 209 + 210 + if progress != nil { 211 + progress.Set(i + 1) 212 + } 139 213 } 140 214 141 - fmt.Println() 215 + if progress != nil { 216 + progress.Finish() 217 + } 218 + 219 + elapsed := time.Since(start) 220 + 221 + fmt.Fprintf(os.Stderr, "\n") 142 222 if errorCount == 0 { 143 - fmt.Printf("✓ Chain is valid (%d bundles verified)\n", verifiedCount) 144 - fmt.Printf(" First bundle: %06d\n", bundles[0].BundleNumber) 145 - fmt.Printf(" Last bundle: %06d\n", bundles[len(bundles)-1].BundleNumber) 146 - fmt.Printf(" Chain head: %s...\n", bundles[len(bundles)-1].Hash[:16]) 223 + fmt.Fprintf(os.Stderr, "✓ Chain is valid (%d bundles verified)\n", verifiedCount) 224 + fmt.Fprintf(os.Stderr, " First bundle: %06d\n", bundles[0].BundleNumber) 225 + fmt.Fprintf(os.Stderr, " Last bundle: %06d\n", bundles[len(bundles)-1].BundleNumber) 226 + fmt.Fprintf(os.Stderr, " Chain head: %s...%s\n", 227 + bundles[len(bundles)-1].Hash[:16], 228 + bundles[len(bundles)-1].Hash[len(bundles[len(bundles)-1].Hash)-16:]) 229 + 230 + // Timing information 231 + fmt.Fprintf(os.Stderr, "\nPerformance:\n") 232 + fmt.Fprintf(os.Stderr, " Time: %s\n", elapsed.Round(time.Millisecond)) 233 + if elapsed.Seconds() > 0 { 234 + bundlesPerSec := float64(verifiedCount) / elapsed.Seconds() 235 + fmt.Fprintf(os.Stderr, " Throughput: %.1f bundles/sec\n", bundlesPerSec) 236 + 237 + // Calculate data throughput 238 + index := mgr.GetIndex() 239 + stats := index.GetStats() 240 + if totalSize, ok := stats["total_size"].(int64); ok && totalSize > 0 { 241 + mbPerSec := float64(totalSize) / elapsed.Seconds() / (1024 * 1024) 242 + fmt.Fprintf(os.Stderr, " Data rate: %.1f MB/sec (compressed)\n", mbPerSec) 243 + } 244 + } 147 245 return nil 148 246 } 149 247 150 - fmt.Printf("✗ Chain verification failed\n") 151 - fmt.Printf(" Verified: %d/%d bundles\n", verifiedCount, len(bundles)) 152 - fmt.Printf(" Errors: %d\n", errorCount) 248 + fmt.Fprintf(os.Stderr, "✗ Chain verification failed\n") 249 + fmt.Fprintf(os.Stderr, " Verified: %d/%d bundles\n", verifiedCount, len(bundles)) 250 + fmt.Fprintf(os.Stderr, " Errors: %d\n", errorCount) 251 + fmt.Fprintf(os.Stderr, " Time: %s\n", elapsed.Round(time.Millisecond)) 153 252 return fmt.Errorf("chain verification failed") 154 253 } 254 + 255 + func verifyRange(ctx context.Context, mgr *bundle.Manager, rangeStr string, verbose bool, parallel bool, workers int) error { 256 + start, end, err := parseBundleRange(rangeStr) 257 + if err != nil { 258 + return err 259 + } 260 + 261 + fmt.Fprintf(os.Stderr, "Verifying bundles %06d - %06d", start, end) 262 + if parallel { 263 + fmt.Fprintf(os.Stderr, " (parallel, %d workers)", workers) 264 + } 265 + fmt.Fprintf(os.Stderr, "\n\n") 266 + 267 + total := end - start + 1 268 + overallStart := time.Now() 269 + 270 + var verifyErr error 271 + if parallel { 272 + verifyErr = verifyRangeParallel(ctx, mgr, start, end, workers, verbose) 273 + } else { 274 + verifyErr = verifyRangeSequential(ctx, mgr, start, end, total, verbose) 275 + } 276 + 277 + elapsed := time.Since(overallStart) 278 + 279 + // Add timing summary 280 + fmt.Fprintf(os.Stderr, "\nPerformance:\n") 281 + fmt.Fprintf(os.Stderr, " Time: %s\n", elapsed.Round(time.Millisecond)) 282 + if elapsed.Seconds() > 0 { 283 + bundlesPerSec := float64(total) / elapsed.Seconds() 284 + fmt.Fprintf(os.Stderr, " Throughput: %.1f bundles/sec\n", bundlesPerSec) 285 + 286 + // Estimate average time per bundle 287 + avgTime := elapsed / time.Duration(total) 288 + fmt.Fprintf(os.Stderr, " Avg/bundle: %s\n", avgTime.Round(time.Millisecond)) 289 + } 290 + 291 + return verifyErr 292 + } 293 + 294 + func verifyRangeSequential(ctx context.Context, mgr *bundle.Manager, start, end, total int, verbose bool) error { 295 + var progress *ui.ProgressBar 296 + if !verbose { 297 + progress = ui.NewProgressBar(total) 298 + } 299 + 300 + verified := 0 301 + failed := 0 302 + failedBundles := []int{} 303 + 304 + for bundleNum := start; bundleNum <= end; bundleNum++ { 305 + result, err := mgr.VerifyBundle(ctx, bundleNum) 306 + 307 + if verbose { 308 + fmt.Fprintf(os.Stderr, "Bundle %06d: ", bundleNum) 309 + } 310 + 311 + if err != nil { 312 + if verbose { 313 + fmt.Fprintf(os.Stderr, "ERROR - %v\n", err) 314 + } 315 + failed++ 316 + failedBundles = append(failedBundles, bundleNum) 317 + } else if !result.Valid { 318 + if verbose { 319 + fmt.Fprintf(os.Stderr, "INVALID - %v\n", result.Error) 320 + } 321 + failed++ 322 + failedBundles = append(failedBundles, bundleNum) 323 + } else { 324 + if verbose { 325 + fmt.Fprintf(os.Stderr, "✓\n") 326 + } 327 + verified++ 328 + } 329 + 330 + if progress != nil { 331 + progress.Set(bundleNum - start + 1) 332 + } 333 + } 334 + 335 + if progress != nil { 336 + progress.Finish() 337 + } 338 + 339 + fmt.Fprintf(os.Stderr, "\n") 340 + if failed == 0 { 341 + fmt.Fprintf(os.Stderr, "✓ All %d bundles verified successfully\n", verified) 342 + return nil 343 + } 344 + 345 + fmt.Fprintf(os.Stderr, "✗ Verification failed\n") 346 + fmt.Fprintf(os.Stderr, " Verified: %d/%d\n", verified, total) 347 + fmt.Fprintf(os.Stderr, " Failed: %d\n", failed) 348 + 349 + if len(failedBundles) > 0 && len(failedBundles) <= 20 { 350 + fmt.Fprintf(os.Stderr, "\nFailed bundles: ") 351 + for i, num := range failedBundles { 352 + if i > 0 { 353 + fmt.Fprintf(os.Stderr, ", ") 354 + } 355 + fmt.Fprintf(os.Stderr, "%06d", num) 356 + } 357 + fmt.Fprintf(os.Stderr, "\n") 358 + } else if len(failedBundles) > 20 { 359 + fmt.Fprintf(os.Stderr, "\nFailed bundles: %06d, %06d, ... and %d more\n", 360 + failedBundles[0], failedBundles[1], len(failedBundles)-2) 361 + } 362 + 363 + return fmt.Errorf("verification failed for %d bundles", failed) 364 + } 365 + 366 + func verifyRangeParallel(ctx context.Context, mgr *bundle.Manager, start, end, workers int, verbose bool) error { 367 + type job struct { 368 + bundleNum int 369 + } 370 + 371 + type result struct { 372 + bundleNum int 373 + valid bool 374 + err error 375 + } 376 + 377 + total := end - start + 1 378 + jobs := make(chan job, total) 379 + results := make(chan result, total) 380 + 381 + // Start workers 382 + for w := 0; w < workers; w++ { 383 + go func() { 384 + for j := range jobs { 385 + vr, err := mgr.VerifyBundle(ctx, j.bundleNum) 386 + results <- result{ 387 + bundleNum: j.bundleNum, 388 + valid: err == nil && vr.Valid, 389 + err: err, 390 + } 391 + } 392 + }() 393 + } 394 + 395 + // Send jobs 396 + for bundleNum := start; bundleNum <= end; bundleNum++ { 397 + jobs <- job{bundleNum: bundleNum} 398 + } 399 + close(jobs) 400 + 401 + // Collect results with progress 402 + progress := ui.NewProgressBar(total) 403 + verified := 0 404 + failed := 0 405 + failedBundles := []int{} 406 + 407 + for i := 0; i < total; i++ { 408 + res := <-results 409 + 410 + if res.valid { 411 + verified++ 412 + } else { 413 + failed++ 414 + failedBundles = append(failedBundles, res.bundleNum) 415 + if verbose { 416 + fmt.Fprintf(os.Stderr, "\nBundle %06d: FAILED - %v\n", res.bundleNum, res.err) 417 + } 418 + } 419 + 420 + progress.Set(i + 1) 421 + } 422 + 423 + progress.Finish() 424 + 425 + fmt.Fprintf(os.Stderr, "\n") 426 + if failed == 0 { 427 + fmt.Fprintf(os.Stderr, "✓ All %d bundles verified successfully\n", verified) 428 + return nil 429 + } 430 + 431 + fmt.Fprintf(os.Stderr, "✗ Verification failed\n") 432 + fmt.Fprintf(os.Stderr, " Verified: %d/%d\n", verified, total) 433 + fmt.Fprintf(os.Stderr, " Failed: %d\n", failed) 434 + 435 + if len(failedBundles) > 0 && len(failedBundles) <= 20 { 436 + fmt.Fprintf(os.Stderr, "\nFailed bundles: ") 437 + for i, num := range failedBundles { 438 + if i > 0 { 439 + fmt.Fprintf(os.Stderr, ", ") 440 + } 441 + fmt.Fprintf(os.Stderr, "%06d", num) 442 + } 443 + fmt.Fprintf(os.Stderr, "\n") 444 + } else if len(failedBundles) > 20 { 445 + fmt.Fprintf(os.Stderr, "\nFailed bundles: %06d, %06d, ... and %d more\n", 446 + failedBundles[0], failedBundles[1], len(failedBundles)-2) 447 + } 448 + 449 + return fmt.Errorf("verification failed for %d bundles", failed) 450 + }
+2 -2
cmd/plcbundle/main.go
··· 55 55 // Status & info (root level) 56 56 cmd.AddCommand(commands.NewStatusCommand()) 57 57 /*cmd.AddCommand(commands.NewLogCommand()) 58 - cmd.AddCommand(commands.NewGapsCommand()) 58 + cmd.AddCommand(commands.NewGapsCommand())*/ 59 59 cmd.AddCommand(commands.NewVerifyCommand()) 60 - cmd.AddCommand(commands.NewDiffCommand()) 60 + /*cmd.AddCommand(commands.NewDiffCommand()) 61 61 cmd.AddCommand(commands.NewStatsCommand()) 62 62 cmd.AddCommand(commands.NewInspectCommand()) 63 63