using System; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Collections.Generic; namespace ReScene { struct RarBlock { public byte Type; public ushort Flags; public byte[] Data; public long FilePos; } struct HeaderBlock // 0x69 { public static ushort SupportedFlagMask = 0x1; public string AppName; public HeaderBlock(RarBlock block) { BinaryReader br = new BinaryReader(new MemoryStream(block.Data), Encoding.ASCII); // skip 7-byte block header br.BaseStream.Position += 7; // if flag 0x1 is set, header contains 2 bytes for app name length, then the name if ((block.Flags & 0x1) != 0) AppName = new string(br.ReadChars(br.ReadUInt16())); else AppName = "Unknown"; } } struct StoreBlock // 0x6A { public static ushort SupportedFlagMask = 0x8000; public string FileName; public ushort FileOffset; public uint FileLength; public StoreBlock(RarBlock block) { BinaryReader br = new BinaryReader(new MemoryStream(block.Data), Encoding.ASCII); // skip 7-byte block header br.BaseStream.Position += 7; // 4 bytes for file length FileLength = br.ReadUInt32(); // 2 bytes for name length, then the name FileName = new string(br.ReadChars(br.ReadUInt16())); FileOffset = (ushort)br.BaseStream.Position; } } struct SrrBlock // 0x71 { public static ushort SupportedFlagMask = 0x1; public string FileName; public SrrBlock(RarBlock block) { BinaryReader br = new BinaryReader(new MemoryStream(block.Data), Encoding.ASCII); // skip 7-byte block header br.BaseStream.Position += 7; // 2 bytes for name length, then the name FileName = new string(br.ReadChars(br.ReadUInt16())); } } struct FileBlock // 0x74 or 0x7A (FILE and NEWSUB share the same structure) { public byte CompressionMethod; public ulong PackedSize; public ulong UnpackedSize; public uint FileCrc; public string FileName; public uint RecoverySectors; public ulong DataSectors; public FileBlock(RarBlock block) { BinaryReader br = new BinaryReader(new MemoryStream(block.Data), Encoding.ASCII); // skip 7-byte block header br.BaseStream.Position += 7; // 4 bytes for packed size, 4 for unpacked PackedSize = br.ReadUInt32(); UnpackedSize = br.ReadUInt32(); // skip 1 byte for OS br.BaseStream.Position++; // 4 bytes for crc FileCrc = br.ReadUInt32(); // skip 4 bytes for file date/time, 1 for required RAR version br.BaseStream.Position += 5; // 1 byte for compression method, then 2 for filename length CompressionMethod = br.ReadByte(); ushort nameLength = br.ReadUInt16(); // skip 4 bytes for file attributes br.BaseStream.Position += 4; // if large file flag is set, next are 4 bytes each for high order bits of file sizes if ((block.Flags & 0x0100) == 0x0100) { PackedSize += br.ReadUInt32() * 0x100000000ul; UnpackedSize += br.ReadUInt32() * 0x100000000ul; } // and finally, the file name FileName = new string(br.ReadChars(nameLength)); // if block type is 0x7A and file name is RR, this is a recovery record. sizes follow if (block.Type == 0x7A && FileName == "RR") { // skip 8 bytes for 'Protect+' br.BaseStream.Position += 8; // 4 bytes for recovery sector count, 8 bytes for data sector count RecoverySectors = br.ReadUInt32(); DataSectors = br.ReadUInt64(); } else { RecoverySectors = 0; DataSectors = 0; } } } class Program { const string appName = "ReScene .NET Beta 3"; static bool Verbose = false; static uint[] CrcTab = new uint[256]; static void InitCrcTab() { for (uint i = 0; i < 256; i++) { uint c = i; for (int j = 0; j < 8; j++) c = (c & 1) == 1 ? (c >> 1) ^ 0xedb88320u : (c >> 1); CrcTab[i] = c; } } static uint UpdateCrc(uint startCrc, byte[] data, int offset, int length) { if (CrcTab[1] == 0) InitCrcTab(); for (int i = offset; i < length + offset; i++) startCrc = CrcTab[(byte)(startCrc ^ data[i])] ^ (startCrc >> 8); return startCrc; } static void ReportError(string msg) { ConsoleColor normalColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(msg); Console.ForegroundColor = normalColor; } static void DisplayUsage() { Console.WriteLine("\nUsage:"); Console.WriteLine("\tTo create a reconstruction file (SRR), use the release SFV file.\n\tAll files referenced by the SFV must be in the same folder as the SFV.\n\t\tex: srr example.sfv"); Console.WriteLine("\tTo reconstruct a release, use the SRR file created from the release.\n\tAll files to be archived must be in the same folder as the SRR file.\n\t\tex: srr example.srr"); Console.WriteLine("\nAvailable switches:"); Console.WriteLine("\t-s : Store additional files in the SRR (wildcards supported)"); Console.WriteLine("\t-d: Use directory name as basis for generated .srr file name."); Console.WriteLine("\t-v: Enable verbose (technical) output."); } static void ReportUnsupportedFlag() { ReportError("Warning: Unsupported flag value encountered in SRR file. This file may use features not supported in this version of the application"); } static bool CheckOverwrite(string filePath) { if (File.Exists(filePath)) { Console.WriteLine("Warning: File {0} already exists. Do you wish to continue? (Y/N)", filePath); char res = Console.ReadKey(true).KeyChar; if (res != 'y' && res != 'Y') return false; } return true; } static IList GetSfvFileList(byte[] sfvData) { // get list of files referenced by the SFV. Make sure RAR files are sorted so we get the data in the right place SortedList files = new SortedList(); StreamReader sr = new StreamReader(new MemoryStream(sfvData)); Regex newNameFormat = new Regex(@"\.part(\d+)\.rar$", RegexOptions.IgnoreCase); Regex oldNameFormat = new Regex(@"\.([rs])(ar|\d{2})$", RegexOptions.IgnoreCase); string line; while ((line = sr.ReadLine()) != null) { if (line.Trim().Length < 10 || line.StartsWith(";")) continue; // last 8 characters is crc32 value. rest should be file name line = line.Substring(0, line.Length - 9).Trim(); if (newNameFormat.IsMatch(line)) { files.Add(int.Parse(newNameFormat.Match(line).Groups[1].Value), line); } else if (oldNameFormat.IsMatch(line)) { Match match = oldNameFormat.Match(line); files.Add((match.Groups[1].Value.ToLower() == "s" ? 100 : 0) + (match.Groups[2].Value.ToLower() != "ar" ? int.Parse(match.Groups[2].Value) : -1), line); } else { ReportError(string.Format("Warning: Non-RAR file referenced in SFV. This file cannot be recreated:\n\t{0}", line)); } } return files.Values; } static IList GetRarBlocks(string fileName, bool srrMode) { List blocks = new List(); using (FileStream fs = new FileStream(fileName, FileMode.Open)) { BinaryReader br = new BinaryReader(fs, Encoding.ASCII); bool includeRecovery = false; while (br.BaseStream.Position <= br.BaseStream.Length - 7) { long blockStartPos = br.BaseStream.Position; // block header is always 7 bytes. 2 for crc, 1 for block type, 2 for flags, and 2 for block length ushort crc = br.ReadUInt16(); byte blockType = br.ReadByte(); ushort flags = br.ReadUInt16(); ushort length = br.ReadUInt16(); int addlLength = 0; // if ADD_SIZE flag is set, next 4 bytes are additional data size if ((flags & 0x8000) == 0x8000) addlLength = br.ReadInt32(); // reset position to the beginning we can read the entire block br.BaseStream.Position = blockStartPos; byte[] buff = new byte[length]; br.Read(buff, 0, length); // check for srr block and see if we should include recovery record data // required for backward compatibility with beta1 and earlier that stored recovery records if (blockType == 0x71) includeRecovery = (flags & 0x1) == 0; // read the additional data, but only if this is not a file block or recovery record // if we are building the reconstruction file, we want to skip the those parts of the RAR // if we are in the process of reconstruction, the data isn't in the file, so don't try to read or skip it bool skipAddData = blockType == 0x74 || // file block or (blockType == 0x7A && length >= 34 && !includeRecovery && // newsub block that is also a recovery record buff[26] == 0x02 && buff[27] == 0x00 && buff[32] == 0x52 && buff[33] == 0x52); // peek ahead to double check for RR if (!skipAddData && addlLength > 0) { byte[] oldbuff = buff; buff = new byte[length + addlLength]; oldbuff.CopyTo(buff, 0); br.BaseStream.Read(buff, length, addlLength); } else if (!srrMode && addlLength > 0) { br.BaseStream.Seek(addlLength, SeekOrigin.Current); } blocks.Add(new RarBlock() { Type = blockType, Flags = flags, Data = buff, FilePos = blockStartPos }); } } return blocks; } static int CreateReconstructionFile(FileInfo sfvFileInfo, List storeFiles, bool useDirName) { string srrName; if (useDirName) srrName = Path.Combine(sfvFileInfo.DirectoryName, sfvFileInfo.Directory.Name + ".srr"); else srrName = Path.Combine(sfvFileInfo.DirectoryName, Path.GetFileNameWithoutExtension(sfvFileInfo.Name) + ".srr"); using (FileStream srrfs = new FileStream(srrName, FileMode.Create)) { BinaryWriter bw = new BinaryWriter(srrfs, Encoding.ASCII); // SRR blocks are based on RAR block format. Header block type is 0x69. We don't use crc for blocks (as of now), so crc value is set to 0x6969 // Flag 0x1 indicates the header contains appName. Length of the block is 7 (header length) + 2 bytes for appName length + the length of the appName. // See http://datacompression.info/ArchiveFormats/RAR202.txt for more details on RAR file format bw.Write(new byte[] { 0x69, 0x69, 0x69, 0x01, 0x00 }); bw.Write((ushort)(7 + 2 + appName.Length)); bw.Write((ushort)appName.Length); bw.Write(appName.ToCharArray()); // we store copies of any files included in the storeFiles list in the .srr using a "store block". the SFV file is always included. // since the sfv is always last, we'll leave its contents in the buffer when we move on the the next step. byte[] storeBuff = null; foreach (string fileName in storeFiles) { string searchName = fileName; if (!Path.IsPathRooted(searchName)) searchName = Path.Combine(sfvFileInfo.DirectoryName, fileName); foreach (FileInfo storeFile in new DirectoryInfo(Path.GetDirectoryName(searchName)).GetFiles(Path.GetFileName(searchName))) { Console.WriteLine("Storing file: {0}", storeFile.Name); using (FileStream storefs = storeFile.Open(FileMode.Open, FileAccess.Read)) { storeBuff = new byte[storefs.Length]; storefs.Read(storeBuff, 0, storeBuff.Length); } // store block (type 0x6A) has the 0x8000 flag set to indicate there is additional data following the block. // format is 7 byte header followed by 4 byte file size, 2 byte file name length, and file name bw.Write(new byte[] { 0x6A, 0x6A, 0x6A, 0x00, 0x80 }); bw.Write((ushort)(7 + 4 + 2 + sfvFileInfo.Name.Length)); bw.Write((uint)storeBuff.Length); bw.Write((ushort)storeFile.Name.Length); bw.Write(storeFile.Name.ToCharArray()); // then the file data bw.Write(storeBuff); } } // sfv data should be left in the buffer, so let's use it from there foreach (string file in GetSfvFileList(storeBuff)) { Console.WriteLine("Processing file: {0}", file); string fileName = Path.Combine(sfvFileInfo.DirectoryName, file); if (!File.Exists(fileName)) { ReportError(string.Format("Referenced file not found: {0}", fileName)); srrfs.Close(); File.Delete(srrName); return 2; } // we create one SRR block (type 0x71) for each RAR file. // it has 7 byte header, 2 bytes for file name length, then file name // flag 0x1 means recovery records have been removed if present bw.Write(new byte[] { 0x71, 0x71, 0x71, 0x01, 0x00 }); bw.Write((ushort)(7 + 2 + file.Length)); bw.Write((ushort)file.Length); bw.Write(file.ToCharArray()); foreach (RarBlock block in GetRarBlocks(fileName, false)) { if (Verbose) { Console.WriteLine("\tBlock Type: 0x{0:x2}", block.Type); Console.WriteLine("\tBlock Size: {0}", block.Data.Length); } if (block.Type == 0x74) // file block { FileBlock fileData = new FileBlock(block); if (Verbose) { Console.WriteLine("\t\tCompression Type: 0x{0:x2}", fileData.CompressionMethod); Console.WriteLine("\t\tPacked Data Size: {0:n0}", fileData.PackedSize); Console.WriteLine("\t\tFile Size: {0:n0}", fileData.UnpackedSize); Console.WriteLine("\t\tFile Name: {0}", fileData.FileName); } if (fileData.CompressionMethod != 0x30) { ReportError(string.Format("Archive uses unsupported compression method: {0}", fileName)); srrfs.Close(); File.Delete(srrName); return 3; } } else if (block.Type == 0x7A) // newsub block { FileBlock subData = new FileBlock(block); if (Verbose & subData.RecoverySectors > 0) { Console.WriteLine("\t\tRecovery Record Size: {0:n0}", subData.PackedSize); Console.WriteLine("\t\tRecovery Sectors: {0:n0}", subData.RecoverySectors); Console.WriteLine("\t\tProtected Sectors: {0:n0}", subData.DataSectors); } } // store the raw data for any blocks found bw.Write(block.Data); } } } Console.WriteLine("\nReconstruction file successfully created: {0}", srrName); return 0; } static int Reconstruct(FileInfo srrFileInfo) { string rarName = null, srcName = null; FileStream rarfs = null, srcfs = null; bool rebuildRecovery = false; byte[] copyBuff = new byte[0x10000]; foreach (RarBlock block in GetRarBlocks(srrFileInfo.FullName, true)) { if (Verbose) { Console.WriteLine("\tBlock Type: 0x{0:x2}", block.Type); Console.WriteLine("\tBlock Size: {0}", block.Data.Length); } if (block.Type == 0x69) // header block { // file header block. the only thing here so far is the name of the app that created the SRR file if ((block.Flags & ~HeaderBlock.SupportedFlagMask) != 0) ReportUnsupportedFlag(); HeaderBlock headBlock = new HeaderBlock(block); } else if (block.Type == 0x6A) // store block { // There is a file stored within the .srr. extract it. if ((block.Flags & ~StoreBlock.SupportedFlagMask) != 0) ReportUnsupportedFlag(); StoreBlock sb = new StoreBlock(block); string fileName = Path.Combine(srrFileInfo.DirectoryName, sb.FileName); if (CheckOverwrite(fileName)) { Console.WriteLine("Re-creating stored file: {0}", sb.FileName); using (FileStream sffs = new FileStream(fileName, FileMode.Create)) { sffs.Write(block.Data, sb.FileOffset, (int)sb.FileLength); } } else { ReportError("Operation aborted."); return -1; } } /* else if (block.Type >= 0x6B && block.Type <= 70) { // reserved for future use } */ else if (block.Type == 0x71) // srr block { // for each SRR block, we need to create a RAR file. get the stored name and create it. if ((block.Flags & ~SrrBlock.SupportedFlagMask) != 0) ReportUnsupportedFlag(); SrrBlock srrBlock = new SrrBlock(block); if (rarName != srrBlock.FileName) { // we use flag 0x1 to mark files that have recovery records removed. all other flags are currently undefined. rebuildRecovery = (block.Flags & 0x1) != 0; rarName = srrBlock.FileName; if (rarfs != null) rarfs.Close(); if (CheckOverwrite(Path.Combine(srrFileInfo.DirectoryName, rarName))) { rarfs = new FileStream(Path.Combine(srrFileInfo.DirectoryName, rarName), FileMode.Create, FileAccess.ReadWrite); Console.WriteLine("Re-creating RAR file: {0}", srrBlock.FileName); } else { ReportError("Operation aborted."); return -1; } } } else if (block.Type == 0x74) // file data { // this is the main RAR block we treat differently. We removed the data when storing it, so we need to get the data back from the extracted file FileBlock fileData = new FileBlock(block); if (Verbose) { Console.WriteLine("\t\tPacked Data Size: {0:n0}", fileData.PackedSize); Console.WriteLine("\t\tFile Name: {0}", fileData.FileName); } if (srcName != fileData.FileName) { srcName = fileData.FileName; if (srcfs != null) srcfs.Close(); FileInfo srcInfo = new FileInfo(Path.Combine(srrFileInfo.DirectoryName, srcName)); if (!srcInfo.Exists) { ReportError(string.Format("Could not locate data file: {0}", srcInfo.FullName)); return 4; } if ((ulong)srcInfo.Length != fileData.UnpackedSize) { ReportError(string.Format("Data file is not the correct size: {0}\n\tFound: {1:n0}\n\tExpected: {2:n0}", srcInfo.FullName, srcInfo.Length, fileData.UnpackedSize)); return 5; } srcfs = new FileStream(srcInfo.FullName, FileMode.Open); } // write the block contents from the .srr file rarfs.Write(block.Data, 0, block.Data.Length); // then grab the correct amount of data from the extracted file int bytesCopied = 0; while (bytesCopied < (int)fileData.PackedSize) { int bytesToCopy = (int)fileData.PackedSize - bytesCopied; if (bytesToCopy > copyBuff.Length) bytesToCopy = copyBuff.Length; srcfs.Read(copyBuff, 0, bytesToCopy); rarfs.Write(copyBuff, 0, bytesToCopy); bytesCopied += bytesToCopy; } } else if (block.Type == 0x7A) // newsub block { // the multi-purpose newsub block is used for recovery record data. it consists of two parts: crc's and recovery sectors // all file data preceding the recovery record block is protected by the recovery record. that data is broken into sectors of 512 bytes. // the crc portion of the recovery block is the 2 low-order bytes of the crc32 value for each sector (2 bytes * protected sector count) // the recovery sectors are created by breaking the data into slices based on the recovery sector count. (512 bytes * recovery sector count) // each slice will get one parity sector created by xor-ing the corresponding bytes from all other sectors in the slice. FileBlock subData = new FileBlock(block); if (subData.RecoverySectors > 0 && rebuildRecovery) { if (Verbose) { Console.WriteLine("\t\tCRC entries to rebuild: {0:n0}", subData.DataSectors); Console.WriteLine("\t\tRecovery sectors to rebuild: {0:n0}", subData.RecoverySectors); } byte[] crc = new byte[subData.DataSectors * 2]; byte[][] rr = new byte[subData.RecoverySectors][]; for (int i = 0; i < subData.RecoverySectors; i++) rr[i] = new byte[512]; int rrSlice = 0; long currentSector = 0; long rarPos = rarfs.Position; byte[] sector = new byte[512]; rarfs.Position = 0; while (rarfs.Position < rarPos) { // read data 1 sector at a time. pad the last sector with 0's if (rarPos - rarfs.Position >= 512) rarfs.Read(sector, 0, 512); else { long pos = rarfs.Position; rarfs.Read(sector, 0, (int)(rarPos - pos)); for (int i = (int)(rarPos - pos); i < 512; i++) sector[i] = 0; } // calculate the crc32 for the sector and store the 2 low-order bytes ushort sectorCrc = (ushort)(UpdateCrc(0xffffffff, sector, 0, sector.Length) & 0xffff); crc[currentSector * 2] = (byte)(sectorCrc & 0xff); crc[currentSector * 2 + 1] = (byte)((sectorCrc >> 8) & 0xff); currentSector++; // update the recovery sector parity data for this slice for (int i = 0; i < 512; i++) rr[rrSlice][i] ^= sector[i]; if (++rrSlice % subData.RecoverySectors == 0) rrSlice = 0; } // write the backed-up block header, crc data, and recovery sectors rarfs.Write(block.Data, 0, block.Data.Length); rarfs.Write(crc, 0, crc.Length); foreach (byte[] ba in rr) rarfs.Write(ba, 0, ba.Length); } else { // block is from a previous ReScene version or is not a recovery record. just copy it rarfs.Write(block.Data, 0, block.Data.Length); } } else if (block.Type >= 0x72) // rar block { // copy any other rar blocks to the destination unmodified rarfs.Write(block.Data, 0, block.Data.Length); } else { ReportError(string.Format("Warning: Unknown block type ({0:X2}) encountered in SRR file, consisting of {1:n0} bytes. This block will be skipped.", block.Type, block.Data.Length)); } } if (rarfs != null) rarfs.Close(); Console.WriteLine("\nRelease successfully reconstructed. Please re-check files against the SFV to verify before using."); return 0; } static Dictionary > GetArgsDictionary(string[] args) { Dictionary> dict = new Dictionary >(); string cmdSwitch = null; List switchParams = null; for (int i = 1; i < args.Length; i++) { if (args[i].StartsWith("-") || args[i].StartsWith("/")) { if (cmdSwitch != null) dict.Add(cmdSwitch, switchParams); cmdSwitch = args[i].Substring(1).ToLower(); switchParams = new List(); } else { if (switchParams != null) switchParams.Add(args[i]); } } if (cmdSwitch != null) dict.Add(cmdSwitch, switchParams); return dict; } static int Main(string[] args) { try { Dictionary > argDict = GetArgsDictionary(args); if (args.Length < 1 || argDict.ContainsKey("?")) { DisplayUsage(); return -1; } Verbose = argDict.ContainsKey("v"); FileInfo inputFileInfo = new FileInfo(args[0]); if (!inputFileInfo.Exists) { ReportError(string.Format("Input file not found: {0}", inputFileInfo.FullName)); DisplayUsage(); return 1; } else if (inputFileInfo.Extension.ToLower() == ".sfv") { List storeFiles = argDict.ContainsKey("s") ? argDict["s"] : new List(); storeFiles.Add(inputFileInfo.Name); return CreateReconstructionFile(inputFileInfo, storeFiles, argDict.ContainsKey("d")); } else if (inputFileInfo.Extension.ToLower() == ".srr") { return Reconstruct(inputFileInfo); } else { ReportError(string.Format("Input file type not recognized: {0}", inputFileInfo.Extension)); DisplayUsage(); return -1; } } catch (Exception ex) { System.Windows.Forms.MessageBox.Show(ex.ToString(), "Unexpected Error", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error); return 99; } } } }