Skip to content

Instantly share code, notes, and snippets.

@AldeRoberge
Created October 15, 2025 13:12
Show Gist options
  • Save AldeRoberge/124684ecb7e7811cb463853a70283969 to your computer and use it in GitHub Desktop.
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.
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