/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; using System.Windows; using System.Windows.Media; using Microsoft.Win32; using Asm65; namespace SourceGen.Tools.WpfGui { /// /// File slicer tool. /// public partial class FileSlicer : Window, INotifyPropertyChanged { /// /// Path to file to slice. /// private string mPathName; /// /// Text formatter. /// private Formatter mFormatter; /// /// Length of file to slice. /// private long mFileLength; /// /// Open file. /// private FileStream mFileStream; private bool mIsSaveEnabled; public bool IsSaveEnabled { get { return mIsSaveEnabled; } set { mIsSaveEnabled = value; OnPropertyChanged(); } } public string FileLengthStr { get { return FormatDecAndHex(mFileLength); } } // Start/length entry fields and dec+hex display. private string mSliceStart; public string SliceStart { get { return mSliceStart; } set { mSliceStart = value; OnPropertyChanged(); UpdateControls(); } } private string mSliceStartDesc; public string SliceStartDesc { get { return mSliceStartDesc; } set { mSliceStartDesc = value; OnPropertyChanged(); } } private string mSliceLength; public string SliceLength { get { return mSliceLength; } set { mSliceLength = value; OnPropertyChanged(); UpdateControls(); } } private string mSliceLengthDesc; public string SliceLengthDesc { get { return mSliceLengthDesc; } set { mSliceLengthDesc = value; OnPropertyChanged(); } } private string mStartHexDump; public string StartHexDump { get { return mStartHexDump; } set { mStartHexDump = value; OnPropertyChanged(); } } private string mEndHexDump; public string EndHexDump { get { return mEndHexDump; } set { mEndHexDump = value; OnPropertyChanged(); } } // Text turns red on error. private Brush mSliceStartBrush; public Brush SliceStartBrush { get { return mSliceStartBrush; } set { mSliceStartBrush = value; OnPropertyChanged(); } } private Brush mSliceLengthBrush; public Brush SliceLengthBrush { get { return mSliceLengthBrush; } set { mSliceLengthBrush = value; OnPropertyChanged(); } } private Brush mDefaultLabelColor = SystemColors.WindowTextBrush; private Brush mErrorLabelColor = Brushes.Red; // INotifyPropertyChanged implementation public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } /// /// Constructor. /// public FileSlicer(Window owner, string pathName, Formatter formatter) { InitializeComponent(); Owner = owner; DataContext = this; mPathName = pathName; mFormatter = formatter; mFileLength = new FileInfo(pathName).Length; mFileStream = new FileStream(pathName, FileMode.Open, FileAccess.Read); mSliceStart = mSliceLength = string.Empty; UpdateControls(); } private void Window_Closing(object sender, CancelEventArgs e) { mFileStream.Close(); } /// /// Formats a value in decimal and hex. /// /// Value to format. /// Formatted string. private string FormatDecAndHex(long val) { StringBuilder sb = new StringBuilder(); sb.Append(val.ToString()); sb.Append(" ("); sb.Append(mFormatter.FormatHexValue((int)val, 4)); sb.Append(")"); return sb.ToString(); } /// /// Updates the state of the controls after something changed. /// private void UpdateControls() { ParseStartLength(out bool isStartValid, out long sliceStart, out bool isLengthValid, out long sliceLength); SliceStartDesc = FormatDecAndHex(sliceStart); SliceLengthDesc = FormatDecAndHex(sliceLength) + (string)FindResource("str_LastByteAt") + mFormatter.FormatOffset24((int)(sliceStart + sliceLength - 1)); if (isStartValid && isLengthValid) { // anchor is first byte in slice StartHexDump = CreateSplitHexDump(0, sliceStart, sliceStart + sliceLength - 1); // anchor is first byte after slice (may be off end) EndHexDump = CreateSplitHexDump(sliceStart, sliceStart + sliceLength, mFileLength - 1); } else { StartHexDump = EndHexDump = string.Empty; } SliceStartBrush = isStartValid ? mDefaultLabelColor : mErrorLabelColor; SliceLengthBrush = isLengthValid ? mDefaultLabelColor : mErrorLabelColor; IsSaveEnabled = isStartValid && isLengthValid; } private void ParseStartLength(out bool startOk, out long sliceStart, out bool lengthOk, out long sliceLength) { startOk = lengthOk = true; if (string.IsNullOrEmpty(SliceStart)) { sliceStart = 0; } else if (Number.TryParseLong(SliceStart.Trim(), out sliceStart, out int unused1)) { if (sliceStart < 0 || sliceStart >= mFileLength) { startOk = false; } } else { startOk = false; } if (string.IsNullOrEmpty(SliceLength)) { sliceLength = mFileLength - sliceStart; if (sliceLength < 0) { sliceLength = 0; } } else if (Number.TryParseLong(SliceLength.Trim(), out sliceLength, out int unused2)) { if (sliceLength <= 0 || sliceLength > mFileLength || sliceStart + sliceLength > mFileLength) { lengthOk = false; } } else { lengthOk = false; } } /// /// Creates a hex dump with up to 5 lines. Two lines before the anchor point, then /// a gap, then two lines that start with the anchor. /// /// Earliest position we're allowed to include. /// Anchor point. /// Last position we're allowed to show (inclusive end). /// Multi-line formatted string. private string CreateSplitHexDump(long minFirst, long anchorPos, long maxLast) { const long AND_16_MASK = ~0x0f; Debug.Assert(minFirst <= anchorPos && anchorPos <= maxLast + 1 && minFirst <= maxLast); Debug.Assert(minFirst >= 0); Debug.Assert(maxLast < mFileLength); StringBuilder sb = new StringBuilder(5 * 64); byte[] dataBuf = new byte[32]; // We show two lines of hex dump before the anchor, so we need up to 32 bytes. long firstPos = Math.Max(anchorPos - 32, minFirst); firstPos = (firstPos + 15) & AND_16_MASK; long chunkLen = anchorPos - firstPos; mFileStream.Seek(firstPos, SeekOrigin.Begin); int actual = mFileStream.Read(dataBuf, 0, dataBuf.Length); Debug.Assert(chunkLen <= actual); if (chunkLen <= 0) { // no pre-anchor data sb.AppendLine(string.Empty); sb.AppendLine(string.Empty); } else { long pos = firstPos; long lineLen = 16 - (pos & 0x0f); if (lineLen >= chunkLen) { // top part fits on a single line; do it on the next one lineLen = chunkLen; sb.AppendLine(string.Empty); } else { mFormatter.FormatHexDump(dataBuf, (int)(pos - firstPos), (int)pos, (int)lineLen, sb); sb.Append("\r\n"); //sb.AppendLine(pos.ToString("x4") + ": " + lineLen); pos += lineLen; lineLen = chunkLen - lineLen; } mFormatter.FormatHexDump(dataBuf, (int)(pos - firstPos), (int)pos, (int)lineLen, sb); sb.Append("\r\n"); //sb.AppendLine(pos.ToString("x4") + ": " + lineLen); } sb.AppendLine("------"); // We show two lines of hex dump with the anchor, so we need up to 31 bytes // following it. // // NOTE: anchorPos is inclusive and represents the first byte contained in the // range. If we're doing the hex dump for the end of the file, the value can be // one greater than maxLast if the pre-anchor range runs to EOF. long lastPos = ((anchorPos + 32) & AND_16_MASK) - 1; // inclusive end lastPos = Math.Min(lastPos, maxLast); chunkLen = lastPos - anchorPos + 1; mFileStream.Seek(anchorPos, SeekOrigin.Begin); actual = mFileStream.Read(dataBuf, 0, dataBuf.Length); Debug.Assert(chunkLen <= actual); if (chunkLen <= 0) { // anchor not visible sb.AppendLine(string.Empty); sb.AppendLine(string.Empty); } else { long pos = anchorPos; long lineLen = 16 - (pos & 0x0f); if (lineLen > chunkLen) { lineLen = chunkLen; } mFormatter.FormatHexDump(dataBuf, (int)(pos - anchorPos), (int)pos, (int)lineLen, sb); sb.Append("\r\n"); //sb.AppendLine(pos.ToString("x4") + ": " + lineLen); pos += lineLen; lineLen = chunkLen - lineLen; if (lineLen <= 0) { sb.AppendLine(string.Empty); } else { mFormatter.FormatHexDump(dataBuf, (int)(pos - anchorPos), (int)pos, (int)lineLen, sb); sb.Append("\r\n"); //sb.AppendLine(pos.ToString("x4") + ": " + lineLen); } } return sb.ToString(); } private void SaveButton_Click(object sender, RoutedEventArgs e) { SaveFileDialog fileDlg = new SaveFileDialog() { Filter = Res.Strings.FILE_FILTER_ALL, FilterIndex = 0, ValidateNames = true, FileName = "slice.bin" }; if (fileDlg.ShowDialog() != true) { return; } string pathName = Path.GetFullPath(fileDlg.FileName); Debug.WriteLine("OUTPUT TO " + pathName); try { ParseStartLength(out bool startOk, out long sliceStart, out bool lengthOk, out long sliceLength); if (!(startOk && lengthOk)) { throw new Exception("Internal error: start/length invalid"); } using (FileStream outStream = new FileStream(pathName, FileMode.Create)) { mFileStream.Seek(sliceStart, SeekOrigin.Begin); CopyStreamToStream(mFileStream, outStream, sliceLength); } } catch (Exception ex) { string ecaption = (string)FindResource("str_FileAccessFailedCaption"); string efmt = (string)FindResource("str_FileAccessFailedFmt"); MessageBox.Show(string.Format(efmt, ex.Message), ecaption, MessageBoxButton.OK, MessageBoxImage.Error); return; } string caption = (string)FindResource("str_SuccessCaption"); string msg = (string)FindResource("str_SuccessMsg"); MessageBox.Show(msg, caption, MessageBoxButton.OK, MessageBoxImage.None); } private static void CopyStreamToStream(Stream inStream, Stream outStream, long length) { byte[] buffer = new byte[256 * 1024]; while (length > 0) { int getLen = (int)Math.Min(length, buffer.Length); int actual = inStream.Read(buffer, 0, getLen); if (actual != getLen) { throw new IOException("Read failed: requested " + getLen + ", got " + actual); } outStream.Write(buffer, 0, getLen); length -= getLen; } } } }