public void Delete_Symlink() { SetupTests(); ReparsePoint.Create(SymlinkPath, FirstTargetPath, true, ReparsePoint.LinkType.DirectoryLink); ReparsePoint.Delete(SymlinkPath); Assert.IsFalse(Directory.Exists(SymlinkPath)); }
public void Create_And_GetTarget_Symlink() { SetupTests(); ReparsePoint.Create(SymlinkPath, FirstTargetPath, true, ReparsePoint.LinkType.DirectoryLink); var measuredTargetPath = ReparsePoint.GetTarget(SymlinkPath); Assert.AreEqual(FirstTargetPath, measuredTargetPath, true); }
protected override void ChangeDirectory(string message = null, Action callback = null, bool refreshCache = false) { if (CurrentFolder.Type == ItemType.Link) { var reparsePoint = new ReparsePoint(CurrentFolder.Path); var path = reparsePoint.Target; if (path == null) { if (reparsePoint.LastError == 5) { try { var cmd = string.Format("/k dir \"{0}\" /AL", Path.Combine(CurrentFolder.Path, "..")); var p = Process.Start(new ProcessStartInfo("cmd.exe", cmd) { CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }); var r = new Regex(string.Format("{0} \\[(.*?)\\]", Regex.Escape(CurrentFolder.Name)), RegexOptions.IgnoreCase | RegexOptions.Singleline); string line; while ((line = p.StandardOutput.ReadLine()) != null) { var m = r.Match(line); if (!m.Success) { continue; } path = m.Groups[1].Value; break; } p.Close(); } catch { //do nothing if something goes wrong } } if (string.IsNullOrEmpty(path)) { WindowManager.ShowMessage(Resx.IOError, reparsePoint.LastError == 5 ? Resx.ReparsePointCannotBeAccessed : Resx.ReparsePointCannotBeResolved); return; } } var model = FileManager.GetItemInfo(path, ItemType.Directory); if (model == null) { WindowManager.ShowMessage(Resx.IOError, string.Format(Resx.ItemNotExistsOnPath, path)); return; } CurrentFolder = new FileSystemItemViewModel(model); } base.ChangeDirectory(message, callback, refreshCache); }
public void ShouldGetTargetPathForJunction() { var path = IOPath.Combine(Environment.GetEnvironmentVariable("TEMP"), IOPath.GetRandomFileName()); Directory.CreateDirectory(path); var junctionPath = string.Format("{0}+junction", path); JunctionPoint.Create(junctionPath, path, true); Assert.That(ReparsePoint.GetTarget(junctionPath), Is.EqualTo(path)); }
/// <summary> /// Determines whether the specified path is a symbolic link /// </summary> /// <param name="path">The path to check</param> public static bool IsSymbolicLink(string path) { if (File.Exists(path) || Directory.Exists(path)) { ReparsePoint p = new ReparsePoint(path); return(p.Tag == ReparsePoint.TagType.SymbolicLink); } else { throw new DirectoryNotFoundException(String.Format("The item at '{0}' could not be found", path)); } }
public void ReparsePoints_Empty() { NtfsFileSystem ntfs = new FileSystemSource().NtfsFileSystem(); ntfs.CreateDirectory("dir"); ntfs.SetReparsePoint("dir", new ReparsePoint(12345, new byte[0])); ReparsePoint rp = ntfs.GetReparsePoint("dir"); Assert.AreEqual(12345, rp.Tag); Assert.IsNotNull(rp.Content); Assert.AreEqual(0, rp.Content.Length); }
public void ReparsePoints_NonEmpty() { NtfsFileSystem ntfs = FileSystemSource.NtfsFileSystem(); ntfs.CreateDirectory("dir"); ntfs.SetReparsePoint("dir", new ReparsePoint(123, new byte[] { 4, 5, 6 })); ReparsePoint rp = ntfs.GetReparsePoint("dir"); Assert.Equal(123, rp.Tag); Assert.NotNull(rp.Content); Assert.Equal(3, rp.Content.Length); }
public void Copy_Files_From_Symlink() { SetupTests(); var originalFiles = Directory.GetFiles(FirstTargetPath).Length; ReparsePoint.Create(SymlinkPath, FirstTargetPath, true, ReparsePoint.LinkType.DirectoryLink); FileActions.CopyDirectory(SymlinkPath, SecondTargetPath); var measuredFiles = Directory.GetFiles(SecondTargetPath).Length; Assert.AreEqual(originalFiles, measuredFiles); }
public void TargetExistsTest() { Helper.DemandTestDriveAvailable(TestDrive); Assert.IsTrue(ReparsePoint.TargetExists(@"Y:\ReadTests\Junction")); Assert.IsTrue(ReparsePoint.TargetExists(@"Y:\ReadTests\SymbolicLink")); Assert.IsTrue(ReparsePoint.TargetExists(@"Y:\ReadTests\Volume")); Assert.IsTrue(ReparsePoint.TargetExists(@"Y:\ReadTests\SymbolicLink.txt")); Assert.IsFalse(ReparsePoint.TargetExists(@"Y:\ReadTests\Hardlink.txt")); Assert.IsFalse(ReparsePoint.TargetExists(@"Y:\ReadTests\Original")); Assert.IsFalse(ReparsePoint.TargetExists(@"Y:\ReadTests\Original.txt")); Assert.IsFalse(ReparsePoint.TargetExists(@"Y:\ReadTests\NotExists")); }
public void Create_And_Move_Symlink() { SetupTests(); var originalFiles = Directory.GetFiles(FirstTargetPath).Length + Directory.GetDirectories(FirstTargetPath).Length; var originalTargetFiles = Directory.GetFiles(SecondTargetPath).Length + Directory.GetDirectories(SecondTargetPath).Length; ReparsePoint.Create(SymlinkPath, FirstTargetPath, true, ReparsePoint.LinkType.DirectoryLink); var doubles = ReparsePoint.Move(SymlinkPath, SecondTargetPath, true, ReparsePoint.LinkType.DirectoryLink); var measuredTargetPath = ReparsePoint.GetTarget(SymlinkPath); var measuredFiles = Directory.GetFiles(SecondTargetPath).Length + Directory.GetDirectories(SecondTargetPath).Length; Assert.AreEqual(SecondTargetPath, measuredTargetPath, true); Assert.AreEqual(originalFiles, measuredFiles - originalTargetFiles + doubles); }
/// <summary> /// Sets the reparse point data on a file or directory. /// </summary> /// <param name="path">The file to set the reparse point on.</param> /// <param name="reparsePoint">The new reparse point.</param> public void SetReparsePoint(string path, ReparsePoint reparsePoint) { using (new NtfsTransaction()) { DirectoryEntry dirEntry = GetDirectoryEntry(path); if (dirEntry == null) { throw new FileNotFoundException("File not found", path); } else { File file = GetFile(dirEntry.Reference); NtfsStream stream = file.GetStream(AttributeType.ReparsePoint, null); if (stream != null) { // If there's an existing reparse point, unhook it. using (Stream contentStream = stream.Open(FileAccess.Read)) { byte[] oldRpBuffer = Utilities.ReadFully(contentStream, (int)contentStream.Length); ReparsePointRecord rp = new ReparsePointRecord(); rp.ReadFrom(oldRpBuffer, 0); _context.ReparsePoints.Remove(rp.Tag, dirEntry.Reference); } } else { stream = file.CreateStream(AttributeType.ReparsePoint, null); } // Set the new content ReparsePointRecord newRp = new ReparsePointRecord(); newRp.Tag = (uint)reparsePoint.Tag; newRp.Content = reparsePoint.Content; byte[] contentBuffer = new byte[newRp.Size]; newRp.WriteTo(contentBuffer, 0); using (Stream contentStream = stream.Open(FileAccess.ReadWrite)) { contentStream.Write(contentBuffer, 0, contentBuffer.Length); contentStream.SetLength(contentBuffer.Length); } // Update the standard information attribute - so it reflects the actual file state NtfsStream stdInfoStream = file.GetStream(AttributeType.StandardInformation, null); StandardInformation si = stdInfoStream.GetContent<StandardInformation>(); si.FileAttributes = si.FileAttributes | FileAttributeFlags.ReparsePoint; stdInfoStream.SetContent(si); // Update the directory entry used to open the file, so it's accurate dirEntry.Details.EASizeOrReparsePointTag = newRp.Tag; dirEntry.UpdateFrom(file); // Write attribute changes back to the Master File Table file.UpdateRecordInMft(); // Add the reparse point to the index _context.ReparsePoints.Add(newRp.Tag, dirEntry.Reference); } } }
/// <summary> /// Sets the reparse point data on a file or directory. /// </summary> /// <param name="path">The file to set the reparse point on.</param> /// <param name="reparsePoint">The new reparse point.</param> public void SetReparsePoint(string path, ReparsePoint reparsePoint) { throw new NotSupportedException(); }
public FileRecord(byte[] rawBytes, int offset) { Offset = offset; var sig = BitConverter.ToInt32(rawBytes, 0); if ((sig != _fileSig) && (sig != _baadSig) && (sig != 0x0)) { Logger.Fatal($"Invalid signature! 0x{sig:X}"); return; //throw new Exception("Invalid signature!"); } if (sig == _baadSig) { Logger.Warn($"Bad signature at offset 0x{offset:X}"); return; } Attributes = new List <Attribute>(); FixupOffset = BitConverter.ToInt16(rawBytes, 2); FixupEntryCount = BitConverter.ToInt16(rawBytes, 4); LogSequenceNumber = BitConverter.ToInt64(rawBytes, 0x8); SequenceNumber = BitConverter.ToInt16(rawBytes, 0x10); ReferenceCount = BitConverter.ToInt16(rawBytes, 0x12); FirstAttributeOffset = BitConverter.ToInt16(rawBytes, 0x14); EntryFlags = (EntryFlag)BitConverter.ToInt16(rawBytes, 0x16); Logger.Trace($"Entry flags: {EntryFlags}"); ActualRecordSize = BitConverter.ToInt32(rawBytes, 0x18); AllocatedRecordSize = BitConverter.ToInt32(rawBytes, 0x1c); var entryBytes = new byte[8]; Buffer.BlockCopy(rawBytes, 0x20, entryBytes, 0, 8); MFTRecordToBaseRecord = new MftEntryInfo(entryBytes); FirstAvailableAttribueId = BitConverter.ToInt16(rawBytes, 0x28); EntryNumber = BitConverter.ToInt32(rawBytes, 0x2c); var fixupExpectedBytes = new byte[2]; var fixupActual1 = new byte[2]; var fixupActual2 = new byte[2]; Buffer.BlockCopy(rawBytes, 0x30, fixupExpectedBytes, 0, 2); Buffer.BlockCopy(rawBytes, 0x32, fixupActual1, 0, 2); Buffer.BlockCopy(rawBytes, 0x34, fixupActual2, 0, 2); //verify this record looks ok based on fixup bytes //0x1FE and 0x3fe var expectedFixupVal = BitConverter.ToInt16(fixupExpectedBytes, 0); var x1FeValue = BitConverter.ToInt16(rawBytes, 0x1FE); var x3FeValue = BitConverter.ToInt16(rawBytes, 0x3FE); if ((x1FeValue != expectedFixupVal) && ((EntryFlags & EntryFlag.FileRecordSegmentInUse) == EntryFlag.FileRecordSegmentInUse)) { Logger.Warn( $"FILE record at offset 0x{offset:X}! Fixup values do not match at 0x1FE. Expected: {expectedFixupVal}, actual: {x1FeValue}, EntryFlags: {EntryFlags}"); } if ((x3FeValue != expectedFixupVal) && ((EntryFlags & EntryFlag.FileRecordSegmentInUse) == EntryFlag.FileRecordSegmentInUse)) { Logger.Warn( $"FILE record at offset 0x{offset:X}! Fixup values do not match at 0x3FE. Expected: {expectedFixupVal}, actual: {x3FeValue}, EntryFlags: {EntryFlags}"); } //header is done, replace fixup bytes with actual bytes //0x1fe and 0x3fe should contain fixup bytes Buffer.BlockCopy(fixupActual1, 0, rawBytes, 0x1fe, 2); Buffer.BlockCopy(fixupActual2, 0, rawBytes, 0x3fe, 2); //start attribute processing at FirstAttributeOffset var index = (int)FirstAttributeOffset; while (index < ActualRecordSize) { var attrType = BitConverter.ToInt32(rawBytes, index); var attrSize = BitConverter.ToInt32(rawBytes, index + 4); // Logger.Trace( // $"ActualRecordSize: {ActualRecordSize} attrType: 0x{attrType:X}, size: {attrSize}, index: {index}, offset: 0x{offset:x}, i+o: 0x{index + offset:X}"); if ((attrSize == 0) || (attrType == -1)) { index += 8; //skip -1 type and 0 size if (EntryFlags == 0) //this is a free record { break; } continue; } var rawAttr = new byte[attrSize]; Buffer.BlockCopy(rawBytes, index, rawAttr, 0, attrSize); switch ((AttributeType)attrType) { case AttributeType.StandardInformation: var si = new StandardInfo(rawAttr); Attributes.Add(si); SILastAccessedOn = si.LastAccessedOn; SICreatedOn = si.CreatedOn; SIRecordModifiedOn = si.RecordModifiedOn; SIContentModifiedOn = si.ContentModifiedOn; break; case AttributeType.FileName: var fi = new FileName(rawAttr); Attributes.Add(fi); if ((fi.FileInfo.NameType & NameTypes.Windows) == NameTypes.Windows) { FName = fi.FileInfo.FileName; } //if (fi.FileInfo.LastAccessedOn.UtcDateTime != SILastAccessedOn.UtcDateTime) //{ FNLastAccessedOn = fi.FileInfo.LastAccessedOn; //} //if (fi.FileInfo.CreatedOn.UtcDateTime != SICreatedOn.UtcDateTime) //{ FNCreatedOn = fi.FileInfo.CreatedOn; //} //if (fi.FileInfo.RecordModifiedOn.UtcDateTime != SIRecordModifiedOn.UtcDateTime) //{ FNRecordModifiedOn = fi.FileInfo.RecordModifiedOn; //} //if (fi.FileInfo.ContentModifiedOn.UtcDateTime != SIContentModifiedOn.UtcDateTime) //{ FNContentModifiedOn = fi.FileInfo.ContentModifiedOn; //} break; case AttributeType.Data: var data = new Data(rawAttr); Attributes.Add(data); break; case AttributeType.IndexAllocation: var ia = new IndexAllocation(rawAttr); Attributes.Add(ia); break; case AttributeType.IndexRoot: var ir = new IndexRoot(rawAttr); Attributes.Add(ir); break; case AttributeType.Bitmap: var bm = new Bitmap(rawAttr); Attributes.Add(bm); break; case AttributeType.VolumeVersionObjectId: var oi = new ObjectId(rawAttr); Attributes.Add(oi); break; case AttributeType.SecurityDescriptor: var sd = new SecurityDescriptor(rawAttr); Attributes.Add(sd); break; case AttributeType.VolumeName: var vn = new VolumeName(rawAttr); Attributes.Add(vn); break; case AttributeType.VolumeInformation: var vi = new VolumeInformation(rawAttr); Attributes.Add(vi); break; case AttributeType.LoggedUtilityStream: var lus = new LoggedUtilityStream(rawAttr); Attributes.Add(lus); break; case AttributeType.ReparsePoint: var rp = new ReparsePoint(rawAttr); Attributes.Add(rp); break; case AttributeType.AttributeList: var al = new AttributeList(rawAttr); Attributes.Add(al); break; case AttributeType.Ea: //TODO Finish this var ea = new ExtendedAttribute(rawAttr); Attributes.Add(ea); break; case AttributeType.EaInformation: var eai = new ExtendedAttributeInformation(rawAttr); Attributes.Add(eai); break; default: Logger.Warn($"Unhandled attribute type! Add me: {(AttributeType) attrType}"); throw new Exception($"Add me: {(AttributeType) attrType}"); break; } index += attrSize; } SlackStartOffset = index; //rest is slack. handle here? Logger.Trace($"Slack starts at {index} i+o: 0x{index + offset:X}"); }
/// <summary> /// Gets the target of a Symbloic Link or Junction Point /// </summary> /// <param name="path">The path of the Symbolic link or Junction Point</param> /// <returns>Returns the path the Reparse Point points to</returns> public static string GetLinkTarget(string path) { ReparsePoint p = new ReparsePoint(path); return(p.Target); }
/// <summary> /// Determines whether the specified path is a Junction Point /// </summary> /// <param name="path">The path to check</param> public static bool IsJunctionPoint(string path) { ReparsePoint p = new ReparsePoint(path); return(p.Tag == ReparsePoint.TagType.JunctionPoint); }
/// <summary> /// Validates the directories. /// </summary> /// <returns><c>true</c> if directories are valid; otherwise <c>false</c>.</returns> internal bool ValidateDirectories() { if (string.IsNullOrEmpty(this.ReferenceDirectory)) { this.Error = true; this.Status = Resources.ErrorNoReferenceDirectory; return(false); } if (string.IsNullOrEmpty(this.TargetDirectory)) { this.Error = true; this.Status = Resources.ErrorNoTargetDirectory; return(false); } // Resolve ReparsePoints try { this.ReferenceDirectory = ReparsePoint.GetTargetDirectory(new DirectoryInfo(this.ReferenceDirectory)); } catch (Exception) { } try { this.TargetDirectory = ReparsePoint.GetTargetDirectory(new DirectoryInfo(this.TargetDirectory)); } catch (Exception) { } // Normalize paths if (!this.ReferenceDirectory.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.OrdinalIgnoreCase)) { this.ReferenceDirectory += Path.DirectorySeparatorChar.ToString(); } if (!this.TargetDirectory.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.OrdinalIgnoreCase)) { this.TargetDirectory += Path.DirectorySeparatorChar.ToString(); } // Compare directories if (this.ReferenceDirectory.Equals(this.TargetDirectory, StringComparison.OrdinalIgnoreCase)) { this.Error = true; this.Status = Resources.ErrorReferenceDirectoryEqualsTargetDirectory; return(false); } // Create reference directory if (!Directory.Exists(this.ReferenceDirectory)) { if (this.SyncMode.CreateReferenceDirectory) { try { Directory.CreateDirectory(this.ReferenceDirectory); } catch (Exception) { this.Error = true; this.Status = Resources.ErrorReferenceDirectoryCouldNotBeCreated; return(false); } } else { this.Error = true; this.Status = Resources.ErrorReferenceDirectoryDoesNotExist; return(false); } } // Create target directory if (!Directory.Exists(this.TargetDirectory)) { if (this.SyncMode.CreateTargetDirectory) { try { Directory.CreateDirectory(this.TargetDirectory); } catch (Exception) { this.Error = true; this.Status = Resources.ErrorTargetDirectoryCouldNotBeCreated; return(false); } } else { this.Error = true; this.Status = Resources.ErrorTargetDirectoryDoesNotExist; return(false); } } return(true); }
public FileRecord(byte[] rawBytes, int offset) { Offset = offset; var sig = BitConverter.ToInt32(rawBytes, 0); switch (sig) { case FileSig: break; case BaadSig: _logger.Debug($"Bad signature at offset 0x{offset:X}"); IsBad = true; return; default: //not initialized _logger.Debug($"Uninitialized entry (no signature) at offset 0x{offset:X}"); IsUninitialized = true; return; } _logger.Debug($"Processing FILE record at offset 0x{offset:X}"); Attributes = new List <Attribute>(); FixupOffset = BitConverter.ToInt16(rawBytes, 0x4); FixupEntryCount = BitConverter.ToInt16(rawBytes, 0x6); //to build fixup info, take FixupEntryCount x 2 bytes as each are 2 bytes long var fixupTotalLength = FixupEntryCount * 2; var fixupBuffer = new byte[fixupTotalLength]; Buffer.BlockCopy(rawBytes, FixupOffset, fixupBuffer, 0, fixupTotalLength); //pull this early so we can check if its free in our fix up value messages EntryFlags = (EntryFlag)BitConverter.ToInt16(rawBytes, 0x16); FixupData = new FixupData(fixupBuffer); FixupOk = true; //fixup verification var counter = 512; foreach (var bytese in FixupData.FixupActual) { //adjust the offset to where we need to check var fixupOffset = counter - 2; var expected = BitConverter.ToInt16(rawBytes, fixupOffset); if (expected != FixupData.FixupExpected && EntryFlags != 0x0) { FixupOk = false; _logger.Warn( $"Offset: 0x{Offset:X} Entry/seq: 0x{EntryNumber:X}/0x{SequenceNumber:X} Fixup values do not match at 0x{fixupOffset:X}. Expected: 0x{FixupData.FixupExpected:X2}, actual: 0x{expected:X2}"); } //replace fixup expected with actual bytes. bytese has actual replacement values in it. Buffer.BlockCopy(bytese, 0, rawBytes, fixupOffset, 2); counter += 512; } LogSequenceNumber = BitConverter.ToInt64(rawBytes, 0x8); SequenceNumber = BitConverter.ToUInt16(rawBytes, 0x10); ReferenceCount = BitConverter.ToInt16(rawBytes, 0x12); FirstAttributeOffset = BitConverter.ToInt16(rawBytes, 0x14); ActualRecordSize = BitConverter.ToInt32(rawBytes, 0x18); AllocatedRecordSize = BitConverter.ToInt32(rawBytes, 0x1c); var entryBytes = new byte[8]; Buffer.BlockCopy(rawBytes, 0x20, entryBytes, 0, 8); MftRecordToBaseRecord = new MftEntryInfo(entryBytes); FirstAvailablAttribueId = BitConverter.ToInt16(rawBytes, 0x28); EntryNumber = BitConverter.ToUInt32(rawBytes, 0x2c); //start attribute processing at FirstAttributeOffset var index = (int)FirstAttributeOffset; while (index < ActualRecordSize) { var attrType = (AttributeType)BitConverter.ToInt32(rawBytes, index); var attrSize = BitConverter.ToInt32(rawBytes, index + 4); if (attrSize == 0 || attrType == AttributeType.EndOfAttributes) { index += 8; //skip -1 type and 0 size if (index != ActualRecordSize) { _logger.Warn($"Slack space found in entry/seq: 0x{EntryNumber:X}/0x{SequenceNumber:X}"); } //TODO process slack here? break; } _logger.Debug( $"Found Attribute Type {attrType.ToString()} at absolute offset: 0x{index + offset:X}"); _logger.Trace( $"ActualRecordSize: 0x{ActualRecordSize:X}, size: 0x{attrSize:X}, index: 0x{index:X}"); var rawAttr = new byte[attrSize]; Buffer.BlockCopy(rawBytes, index, rawAttr, 0, attrSize); switch (attrType) { case AttributeType.StandardInformation: var si = new StandardInfo(rawAttr); Attributes.Add(si); break; case AttributeType.FileName: var fi = new FileName(rawAttr); Attributes.Add(fi); break; case AttributeType.Data: var d = new Data(rawAttr); Attributes.Add(d); break; case AttributeType.IndexAllocation: var ia = new IndexAllocation(rawAttr); Attributes.Add(ia); break; case AttributeType.IndexRoot: var ir = new IndexRoot(rawAttr); Attributes.Add(ir); break; case AttributeType.Bitmap: var bm = new Bitmap(rawAttr); Attributes.Add(bm); break; case AttributeType.VolumeVersionObjectId: var oi = new ObjectId_(rawAttr); Attributes.Add(oi); break; case AttributeType.SecurityDescriptor: var sd = new SecurityDescriptor(rawAttr); Attributes.Add(sd); break; case AttributeType.VolumeName: var vn = new VolumeName(rawAttr); Attributes.Add(vn); break; case AttributeType.VolumeInformation: var vi = new VolumeInformation(rawAttr); Attributes.Add(vi); break; case AttributeType.LoggedUtilityStream: var lus = new LoggedUtilityStream(rawAttr); Attributes.Add(lus); break; case AttributeType.ReparsePoint: try { var rp = new ReparsePoint(rawAttr); Attributes.Add(rp); } catch (Exception) { var l = LogManager.GetLogger("ReparsePoint"); l.Error( $"There was an error parsing a ReparsePoint in FILE record at offset 0x{Offset:X}. Please extract via --dd and --do and send to [email protected]"); } break; case AttributeType.AttributeList: var al = new AttributeList(rawAttr); Attributes.Add(al); break; case AttributeType.Ea: var ea = new ExtendedAttribute(rawAttr); Attributes.Add(ea); break; case AttributeType.EaInformation: var eai = new ExtendedAttributeInformation(rawAttr); Attributes.Add(eai); break; default: throw new Exception($"Add me: {attrType} (0x{attrType:X})"); } index += attrSize; } //rest is slack. handle here? _logger.Trace($"Slack starts at 0x{index:X} Absolute offset: 0x{index + offset:X}"); }