Created
October 15, 2025 13:12
-
-
Save AldeRoberge/124684ecb7e7811cb463853a70283969 to your computer and use it in GitHub Desktop.
MOV File Integrity Checker reads the last bytes of a .mov file and outputs error results. Useful for partially transferred files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Text; | |
using System.Collections.Generic; | |
using System.Linq; | |
public class MovIntegrityChecker | |
{ | |
// Common MOV/MP4 atom types | |
private static readonly HashSet<string> ValidAtomTypes = new HashSet<string> | |
{ | |
"ftyp", "moov", "mdat", "free", "skip", "wide", "pnot", | |
"mvhd", "trak", "tkhd", "mdia", "mdhd", "hdlr", "minf", | |
"vmhd", "smhd", "dinf", "stbl", "stsd", "stts", "stsc", | |
"stsz", "stco", "co64", "edts", "elst", "udta", "meta" | |
}; | |
public class AtomInfo | |
{ | |
public string Type { get; set; } | |
public long Size { get; set; } | |
public long Offset { get; set; } | |
public bool IsComplete { get; set; } | |
} | |
public class FileCheckResult | |
{ | |
public string FilePath { get; set; } | |
public bool HasIssues { get; set; } | |
public List<string> Issues { get; set; } = new List<string>(); | |
public List<AtomInfo> Atoms { get; set; } = new List<AtomInfo>(); | |
public long FileSize { get; set; } | |
public long BytesValidated { get; set; } | |
} | |
private static FileCheckResult CheckFileIntegrity(string filePath) | |
{ | |
var result = new FileCheckResult | |
{ | |
FilePath = filePath, | |
HasIssues = false | |
}; | |
if (!File.Exists(filePath)) | |
{ | |
result.HasIssues = true; | |
result.Issues.Add("File does not exist"); | |
return result; | |
} | |
try | |
{ | |
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); | |
long fileLength = fs.Length; | |
result.FileSize = fileLength; | |
if (fileLength < 8) | |
{ | |
result.HasIssues = true; | |
result.Issues.Add("File too small to be a valid MOV (< 8 bytes)"); | |
return result; | |
} | |
// Parse all atoms in the file | |
bool hasStructuralErrors = false; | |
bool hasIncompleteAtoms = false; | |
long position = 0; | |
while (position < fileLength) | |
{ | |
fs.Position = position; | |
// Read atom size (4 bytes, big-endian) | |
byte[] sizeBytes = new byte[4]; | |
int bytesRead = fs.Read(sizeBytes, 0, 4); | |
if (bytesRead < 4) | |
{ | |
result.Issues.Add($"Incomplete atom header at offset {position:N0}"); | |
hasIncompleteAtoms = true; | |
break; | |
} | |
long atomSize = ReadBigEndianUInt32(sizeBytes); | |
// Read atom type (4 bytes) | |
byte[] typeBytes = new byte[4]; | |
bytesRead = fs.Read(typeBytes, 0, 4); | |
if (bytesRead < 4) | |
{ | |
result.Issues.Add($"Incomplete atom type at offset {position:N0}"); | |
hasIncompleteAtoms = true; | |
break; | |
} | |
string atomType = Encoding.ASCII.GetString(typeBytes); | |
// Handle extended size (size == 1) | |
long headerSize = 8; | |
if (atomSize == 1) | |
{ | |
byte[] extSizeBytes = new byte[8]; | |
bytesRead = fs.Read(extSizeBytes, 0, 8); | |
if (bytesRead < 8) | |
{ | |
result.Issues.Add($"Incomplete extended size for atom '{atomType}' at offset {position:N0}"); | |
hasIncompleteAtoms = true; | |
break; | |
} | |
atomSize = ReadBigEndianUInt64(extSizeBytes); | |
headerSize = 16; | |
} | |
// Handle size == 0 (atom extends to end of file) | |
else if (atomSize == 0) | |
{ | |
atomSize = fileLength - position; | |
} | |
// Validate atom size | |
if (atomSize < headerSize) | |
{ | |
result.Issues.Add($"Invalid atom size ({atomSize}) at offset {position:N0} for type '{atomType}'"); | |
hasStructuralErrors = true; | |
break; | |
} | |
// Check if atom extends beyond file | |
bool isComplete = (position + atomSize) <= fileLength; | |
var atom = new AtomInfo | |
{ | |
Type = atomType, | |
Size = atomSize, | |
Offset = position, | |
IsComplete = isComplete | |
}; | |
result.Atoms.Add(atom); | |
if (!isComplete) | |
{ | |
long available = fileLength - position; | |
long missing = atomSize - available; | |
result.Issues.Add($"Incomplete atom '{atomType}' at offset {position:N0}: Expected {atomSize:N0} bytes, available {available:N0} bytes, missing {missing:N0} bytes ({(missing * 100.0 / atomSize):F1}%)"); | |
hasIncompleteAtoms = true; | |
break; | |
} | |
// Warn about unknown atom types | |
if (!ValidAtomTypes.Contains(atomType) && !IsAsciiPrintable(atomType)) | |
{ | |
result.Issues.Add($"Unknown/invalid atom type '{atomType}' at offset {position:N0}"); | |
} | |
position += atomSize; | |
} | |
result.BytesValidated = position; | |
// Check for required atoms | |
bool hasFtyp = result.Atoms.Any(a => a.Type == "ftyp"); | |
bool hasMoov = result.Atoms.Any(a => a.Type == "moov"); | |
bool hasMdat = result.Atoms.Any(a => a.Type == "mdat"); | |
if (!hasFtyp && result.Atoms.Count > 0) | |
{ | |
result.Issues.Add("Missing 'ftyp' atom (file type header)"); | |
} | |
if (!hasMoov && result.Atoms.Count > 0) | |
{ | |
result.Issues.Add("Missing 'moov' atom (metadata)"); | |
} | |
if (!hasMdat && result.Atoms.Count > 0) | |
{ | |
result.Issues.Add("Missing 'mdat' atom (media data)"); | |
} | |
// Validate ftyp is first (if present) | |
if (hasFtyp && result.Atoms.Count > 0 && result.Atoms[0].Type != "ftyp") | |
{ | |
result.Issues.Add($"'ftyp' atom should be first, but found at offset {result.Atoms.First(a => a.Type == "ftyp").Offset:N0}"); | |
} | |
// Check last atom alignment | |
if (result.Atoms.Count > 0) | |
{ | |
var lastAtom = result.Atoms.Last(); | |
long declaredEnd = lastAtom.Offset + lastAtom.Size; | |
if (lastAtom.IsComplete && declaredEnd != fileLength) | |
{ | |
long gap = fileLength - declaredEnd; | |
result.Issues.Add($"Gap of {gap:N0} bytes after last atom '{lastAtom.Type}' at offset {declaredEnd:N0}"); | |
} | |
} | |
// Final verdict | |
if (hasStructuralErrors || hasIncompleteAtoms || result.Atoms.Count == 0) | |
{ | |
result.HasIssues = true; | |
} | |
} | |
catch (Exception ex) | |
{ | |
result.HasIssues = true; | |
result.Issues.Add($"Error reading file: {ex.Message}"); | |
} | |
return result; | |
} | |
private static void PrintDetailedResult(FileCheckResult result) | |
{ | |
Console.WriteLine($"\n{'='}{new string('=', 80)}"); | |
Console.WriteLine($"File: {Path.GetFileName(result.FilePath)}"); | |
Console.WriteLine(new string('=', 80)); | |
Console.WriteLine($"\n📊 Analysis Summary:"); | |
Console.WriteLine($" File Size: {result.FileSize:N0} bytes"); | |
Console.WriteLine($" Atoms Found: {result.Atoms.Count}"); | |
Console.WriteLine($" Bytes Validated: {result.BytesValidated:N0} / {result.FileSize:N0} ({(result.BytesValidated * 100.0 / Math.Max(1, result.FileSize)):F1}%)"); | |
if (result.Atoms.Count > 0) | |
{ | |
Console.WriteLine($"\n📦 Atom Structure:"); | |
foreach (var atom in result.Atoms) | |
{ | |
string status = atom.IsComplete ? "✅" : "❌"; | |
string knownType = ValidAtomTypes.Contains(atom.Type) ? "" : " (unknown)"; | |
Console.WriteLine($" {status} [{atom.Type}]{knownType} - Size: {atom.Size:N0} bytes, Offset: {atom.Offset:N0}"); | |
} | |
// Check for required atoms | |
bool hasFtyp = result.Atoms.Any(a => a.Type == "ftyp"); | |
bool hasMoov = result.Atoms.Any(a => a.Type == "moov"); | |
bool hasMdat = result.Atoms.Any(a => a.Type == "mdat"); | |
Console.WriteLine($"\n🔍 Key Atoms:"); | |
Console.WriteLine($" ftyp (file type): {(hasFtyp ? "✅ Found" : "❌ Missing")}"); | |
Console.WriteLine($" moov (metadata): {(hasMoov ? "✅ Found" : "❌ Missing")}"); | |
Console.WriteLine($" mdat (media data): {(hasMdat ? "✅ Found" : "❌ Missing")}"); | |
} | |
if (result.Issues.Count > 0) | |
{ | |
Console.WriteLine($"\n⚠️ Issues Found ({result.Issues.Count}):"); | |
foreach (var issue in result.Issues) | |
{ | |
WriteWarning($" • {issue}"); | |
} | |
} | |
if (result.HasIssues) | |
{ | |
WriteError("\n❌ File Status: CORRUPTED or INCOMPLETE"); | |
} | |
else | |
{ | |
WriteSuccess("\n✅ File Status: VALID and COMPLETE"); | |
} | |
} | |
private static uint ReadBigEndianUInt32(byte[] data) | |
{ | |
return ((uint)data[0] << 24) | ((uint)data[1] << 16) | ((uint)data[2] << 8) | data[3]; | |
} | |
private static long ReadBigEndianUInt64(byte[] data) | |
{ | |
return ((long)data[0] << 56) | ((long)data[1] << 48) | ((long)data[2] << 40) | ((long)data[3] << 32) | | |
((long)data[4] << 24) | ((long)data[5] << 16) | ((long)data[6] << 8) | data[7]; | |
} | |
private static bool IsAsciiPrintable(string str) | |
{ | |
return str.All(c => c >= 32 && c <= 126); | |
} | |
private static void WriteWarning(string message) | |
{ | |
var oldColor = Console.ForegroundColor; | |
Console.ForegroundColor = ConsoleColor.Yellow; | |
Console.WriteLine(message); | |
Console.ForegroundColor = oldColor; | |
} | |
private static void WriteSuccess(string message) | |
{ | |
var oldColor = Console.ForegroundColor; | |
Console.ForegroundColor = ConsoleColor.Green; | |
Console.WriteLine(message); | |
Console.ForegroundColor = oldColor; | |
} | |
private static void WriteError(string message) | |
{ | |
var oldColor = Console.ForegroundColor; | |
Console.ForegroundColor = ConsoleColor.Red; | |
Console.WriteLine(message); | |
Console.ForegroundColor = oldColor; | |
} | |
private static void WriteInfo(string message) | |
{ | |
var oldColor = Console.ForegroundColor; | |
Console.ForegroundColor = ConsoleColor.Cyan; | |
Console.WriteLine(message); | |
Console.ForegroundColor = oldColor; | |
} | |
public static void Main(string[] args) | |
{ | |
Console.WriteLine("=== MOV File Integrity Checker ===\n"); | |
if (args.Length == 0) | |
{ | |
Console.WriteLine("Usage:"); | |
Console.WriteLine(" Check single file: program.exe <path_to_mov_file>"); | |
Console.WriteLine(" Check folder: program.exe <path_to_folder>"); | |
Console.WriteLine("\nOptions:"); | |
Console.WriteLine(" -r, --recursive Check subfolders recursively"); | |
Console.WriteLine(" -s, --summary Show summary only (no detailed output)"); | |
Console.WriteLine("\nExamples:"); | |
Console.WriteLine(" program.exe video.mov"); | |
Console.WriteLine(" program.exe C:\\Videos"); | |
Console.WriteLine(" program.exe C:\\Videos -r"); | |
Console.WriteLine(" program.exe C:\\Videos --recursive --summary"); | |
return; | |
} | |
string path = args[0]; | |
bool recursive = args.Any(a => a == "-r" || a == "--recursive"); | |
bool summaryOnly = args.Any(a => a == "-s" || a == "--summary"); | |
var results = new List<FileCheckResult>(); | |
// Check if path is a file or directory | |
if (File.Exists(path)) | |
{ | |
// Single file | |
WriteInfo($"Checking file: {path}\n"); | |
var result = CheckFileIntegrity(path); | |
results.Add(result); | |
if (!summaryOnly) | |
{ | |
PrintDetailedResult(result); | |
} | |
} | |
else if (Directory.Exists(path)) | |
{ | |
// Directory | |
WriteInfo($"Checking folder: {path}"); | |
WriteInfo($"Recursive: {(recursive ? "Yes" : "No")}\n"); | |
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; | |
var extensions = new[] { "*.mov", "*.mp4", "*.m4v", "*.m4a" }; | |
var files = extensions | |
.SelectMany(ext => Directory.GetFiles(path, ext, searchOption)) | |
.OrderBy(f => f) | |
.ToList(); | |
if (files.Count == 0) | |
{ | |
WriteWarning("No MOV/MP4 files found in the specified folder."); | |
return; | |
} | |
WriteInfo($"Found {files.Count} file(s) to check...\n"); | |
int current = 0; | |
foreach (var file in files) | |
{ | |
current++; | |
if (summaryOnly) | |
{ | |
Console.Write($"\rProcessing: {current}/{files.Count} - {Path.GetFileName(file)}".PadRight(80)); | |
} | |
else | |
{ | |
WriteInfo($"[{current}/{files.Count}] Checking: {Path.GetFileName(file)}"); | |
} | |
var result = CheckFileIntegrity(file); | |
results.Add(result); | |
if (!summaryOnly) | |
{ | |
PrintDetailedResult(result); | |
} | |
} | |
if (summaryOnly) | |
{ | |
Console.WriteLine("\n"); | |
} | |
} | |
else | |
{ | |
WriteError($"Path not found: {path}"); | |
Environment.ExitCode = 1; | |
return; | |
} | |
// Print summary | |
Console.WriteLine($"\n{new string('=', 80)}"); | |
Console.WriteLine("SUMMARY"); | |
Console.WriteLine(new string('=', 80)); | |
int totalFiles = results.Count; | |
int validFiles = results.Count(r => !r.HasIssues); | |
int corruptedFiles = results.Count(r => r.HasIssues); | |
long totalSize = results.Sum(r => r.FileSize); | |
Console.WriteLine($"\nTotal Files Checked: {totalFiles}"); | |
WriteSuccess($"Valid Files: {validFiles} ({(validFiles * 100.0 / Math.Max(1, totalFiles)):F1}%)"); | |
WriteError($"Corrupted/Incomplete Files: {corruptedFiles} ({(corruptedFiles * 100.0 / Math.Max(1, totalFiles)):F1}%)"); | |
Console.WriteLine($"Total Size: {totalSize:N0} bytes ({totalSize / (1024.0 * 1024.0):F2} MB)"); | |
if (corruptedFiles > 0) | |
{ | |
Console.WriteLine($"\n❌ Corrupted/Incomplete Files:"); | |
foreach (var result in results.Where(r => r.HasIssues)) | |
{ | |
WriteError($" • {Path.GetFileName(result.FilePath)}"); | |
if (!summaryOnly && result.Issues.Count > 0) | |
{ | |
foreach (var issue in result.Issues.Take(3)) | |
{ | |
Console.WriteLine($" - {issue}"); | |
} | |
if (result.Issues.Count > 3) | |
{ | |
Console.WriteLine($" ... and {result.Issues.Count - 3} more issue(s)"); | |
} | |
} | |
} | |
} | |
Environment.ExitCode = corruptedFiles > 0 ? 1 : 0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment