/*
* 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;
}
}
}
}