[GUI] Implement MHDD log viewer functionality

This commit is contained in:
2025-11-18 17:14:48 +00:00
parent 9f0109af43
commit 4e06c565d8
10 changed files with 569 additions and 2 deletions

View File

@@ -79,4 +79,10 @@ public static class FilePickerFileTypes
AppleUniformTypeIdentifiers = ["public.json"],
MimeTypes = ["application/json"]
};
public static FilePickerFileType MhddLogFiles { get; } = new(UI.MHDD_Log_Files)
{
Patterns = ["*.bin"],
MimeTypes = ["application/octet-stream"]
};
}

View File

@@ -1,3 +1,35 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : MainWindowViewModel.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI view models.
//
// --[ Description ] ----------------------------------------------------------
//
// Main window view model.
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General public License for more details.
//
// You should have received a copy of the GNU General public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2025 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -81,6 +113,7 @@ public partial class MainWindowViewModel : ViewModelBase
CreateSidecarCommand = new RelayCommand(CreateSidecar);
ViewImageSectorsCommand = new RelayCommand(ViewImageSectors);
DecodeImageMediaTagsCommand = new RelayCommand(DecodeImageMediaTags);
OpenMhddLogCommand = new AsyncRelayCommand(OpenMhddLogAsync);
_genericHddIcon =
new Bitmap(AssetLoader.Open(new Uri("avares://Aaru.Gui/Assets/Icons/oxygen/32x32/drive-harddisk.png")));
@@ -132,6 +165,7 @@ public partial class MainWindowViewModel : ViewModelBase
public ICommand CreateSidecarCommand { get; }
public ICommand ViewImageSectorsCommand { get; }
public ICommand DecodeImageMediaTagsCommand { get; }
public ICommand OpenMhddLogCommand { get; }
public bool NativeMenuSupported
{
@@ -189,6 +223,25 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
async Task OpenMhddLogAsync()
{
IReadOnlyList<IStorageFile> result = await _view.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = UI.Dialog_Choose_image_to_open,
AllowMultiple = false,
FileTypeFilter = [FilePickerFileTypes.MhddLogFiles]
});
// Exit if user did not select exactly one file
if(result.Count != 1) return;
var mhddLogViewWindow = new MhddLogView();
mhddLogViewWindow.DataContext = new MhddLogViewModel(mhddLogViewWindow, result[0].Path.LocalPath);
mhddLogViewWindow.Show();
}
async Task OpenAsync()
{
// Open file picker dialog to allow user to select an image file

View File

@@ -0,0 +1,207 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : MhddLogViewModel.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI view models.
//
// --[ Description ] ----------------------------------------------------------
//
// View model for MHDD log viewer.
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General public License for more details.
//
// You should have received a copy of the GNU General public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2025 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Aaru.Gui.Views.Windows;
using Aaru.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using JetBrains.Annotations;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
using Sentry;
namespace Aaru.Gui.ViewModels.Windows;
public partial class MhddLogViewModel : ViewModelBase
{
readonly MhddLogView _window;
[ObservableProperty]
string _device;
[ObservableProperty]
string _filePath;
[ObservableProperty]
string _firmware;
[ObservableProperty]
string _mhddVersion;
[ObservableProperty]
string _scanBlockSize;
[ObservableProperty]
ObservableCollection<(ulong startingSector, double duration)> _sectorData;
[ObservableProperty]
string _sectorSize;
[ObservableProperty]
string _serialNumber;
[ObservableProperty]
string _totalSectors;
public MhddLogViewModel(MhddLogView window, [NotNull] string filePath)
{
_window = window;
FilePath = filePath;
_sectorData = [];
}
public void LoadData()
{
Stream stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
stream.Seek(0, SeekOrigin.Begin);
var buffer = new byte[4];
stream.ReadExactly(buffer, 0, 4);
var pointer = BitConverter.ToInt32(buffer, 0);
int d = stream.ReadByte();
int a = stream.ReadByte();
stream.ReadExactly(buffer, 0, 4);
string ver = Encoding.ASCII.GetString(buffer, 0, 4);
if(pointer > stream.Length || d != 0x0D || a != 0x0A || ver != "VER:")
{
stream.Close();
_ = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error,
UI.The_specified_file_is_not_a_correct_MHDD_log_file,
ButtonEnum.Ok,
Icon.Error)
.ShowWindowDialogAsync(_window);
_window.Close();
}
stream.Position = 4;
buffer = new byte[pointer - 4];
stream.ReadExactly(buffer, 0, buffer.Length);
string header = Encoding.ASCII.GetString(buffer, 0, buffer.Length);
try
{
// Parse VER field
Match versionMatch = VersionRegex().Match(header);
if(versionMatch.Success) MhddVersion = $"[green]{versionMatch.Groups[1].Value}[/]";
// Parse DEVICE field
Match deviceMatch = DeviceRegex().Match(header);
if(deviceMatch.Success) Device = $"[pink]{deviceMatch.Groups[1].Value.Trim()}[/]";
// Parse F/W field
Match firmwareMatch = FirmwareRegex().Match(header);
if(firmwareMatch.Success) Firmware = $"[rosybrown]{firmwareMatch.Groups[1].Value}[/]";
// Parse S/N field
Match serialMatch = SerialNumberRegex().Match(header);
if(serialMatch.Success) SerialNumber = $"[purple]{serialMatch.Groups[1].Value}[/]";
// Parse SECTORS field
Match sectorsMatch = SectorsRegex().Match(header);
if(sectorsMatch.Success)
TotalSectors = $"[teal]{ParseNumberWithSeparator(sectorsMatch.Groups[1].Value)}[/]";
// Parse SECTOR SIZE field
Match sectorSizeMatch = SectorSizeRegex().Match(header);
if(sectorSizeMatch.Success)
SectorSize = string.Format(UI._0_bytes_markup,
ParseNumberWithSeparator(sectorSizeMatch.Groups[1].Value));
// Parse SCAN BLOCK SIZE field
Match scanBlockMatch = ScanBlockSizeRegex().Match(header);
if(scanBlockMatch.Success)
ScanBlockSize = string.Format(UI._0_sectors_markup,
ParseNumberWithSeparator(scanBlockMatch.Groups[1].Value));
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
_ = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error,
string.Format(UI.Error_parsing_MHDD_log_header_0, ex.Message),
ButtonEnum.Ok,
Icon.Error)
.ShowWindowDialogAsync(_window);
_window.Close();
}
stream.Position = pointer;
buffer = new byte[8];
SectorData.Clear();
while(stream.Position < stream.Length)
{
stream.ReadExactly(buffer, 0, 8);
var sector = BitConverter.ToUInt64(buffer, 0);
stream.ReadExactly(buffer, 0, 8);
double duration = BitConverter.ToUInt64(buffer, 0) / 1000.0;
SectorData.Add((sector, duration));
}
stream.Close();
}
/// <summary>
/// Parses a number string that may contain thousands separators (en-US culture).
/// </summary>
/// <param name="value">The number string (e.g., "243,587" or "2,448")</param>
/// <returns>The parsed number without separators</returns>
static ulong ParseNumberWithSeparator(string value) => ulong.Parse(value.Replace(",", ""));
[GeneratedRegex(@"VER:\s*(\S+)")]
private static partial Regex VersionRegex();
[GeneratedRegex(@"DEVICE:\s*(.+?)(?=\n|$)")]
private static partial Regex DeviceRegex();
[GeneratedRegex(@"F/W:\s*(\S+)")]
private static partial Regex FirmwareRegex();
[GeneratedRegex(@"S/N:\s*(\S+)")]
private static partial Regex SerialNumberRegex();
[GeneratedRegex(@"SECTORS:\s*([\d,]+)")]
private static partial Regex SectorsRegex();
[GeneratedRegex(@"SECTOR SIZE:\s*([\d,]+)\s*bytes")]
private static partial Regex SectorSizeRegex();
[GeneratedRegex(@"SCAN BLOCK SIZE:\s*([\d,]+)\s*sectors")]
private static partial Regex ScanBlockSizeRegex();
}

View File

@@ -19,6 +19,8 @@
<MenuItem Header="{x:Static localization:UI.Menu_File}">
<MenuItem Header="{x:Static localization:UI.Menu_Open}"
Command="{Binding OpenCommand, Mode=OneWay}" />
<MenuItem Header="{x:Static localization:UI.Menu_Open_MHDD_log}"
Command="{Binding OpenMhddLogCommand, Mode=OneWay}" />
<Separator />
<MenuItem Header="{x:Static localization:UI.Menu_Settings}"
IsVisible="{Binding !NativeMenuSupported, Mode=OneWay}"

View File

@@ -0,0 +1,97 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:windows="clr-namespace:Aaru.Gui.ViewModels.Windows"
xmlns:controls="clr-namespace:Aaru.Gui.Controls"
xmlns:localization="clr-namespace:Aaru.Localization;assembly=Aaru.Localization"
mc:Ignorable="d"
Width="640"
Height="480"
d:DesignWidth="640"
d:DesignHeight="400"
x:Class="Aaru.Gui.Views.Windows.MhddLogView"
x:DataType="windows:MhddLogViewModel"
Title="{x:Static localization:UI.Title_MHDD_log_viewer}">
<Design.DataContext>
<windows:MhddLogViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,*"
RowSpacing="8"
Margin="12">
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_File_path}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding FilePath, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_MHDD_Version}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding MhddVersion, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="2"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_Device}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding Device, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="3"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_Firmware}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding Firmware, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="4"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_Serial_number}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding SerialNumber, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="5"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_Total_sectors}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding TotalSectors, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="6"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_Sector_size}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding SectorSize, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="7"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<controls:SpectreTextBlock Grid.Column="0"
Text="{x:Static localization:UI.Title_Scan_block_size}" />
<controls:SpectreTextBlock Grid.Column="1"
Text="{Binding ScanBlockSize, Mode=OneWay}" />
</Grid>
<Border Grid.Row="8"
BorderThickness="1"
BorderBrush="SlateBlue">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<controls:BlockMap SectorData="{Binding SectorData, Mode=OneWay}"
VerticalAlignment="Top"
ScanBlockSize="{Binding ScanBlockSize, Mode=OneWay}" />
</ScrollViewer>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,57 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : MhddLogView.axaml.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI view models.
//
// --[ Description ] ----------------------------------------------------------
//
// Code behind for MHDD log viewer.
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General public License for more details.
//
// You should have received a copy of the GNU General public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2025 Natalia Portillo
// ****************************************************************************/
using System;
using Aaru.Gui.ViewModels.Windows;
using Avalonia;
using Avalonia.Controls;
namespace Aaru.Gui.Views.Windows;
public partial class MhddLogView : Window
{
public MhddLogView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
/// <inheritdoc />
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if(DataContext is MhddLogViewModel vm) vm?.LoadData();
}
}

View File

@@ -6279,5 +6279,77 @@ namespace Aaru.Localization {
return ResourceManager.GetString("Nothing_opened", resourceCulture);
}
}
public static string MHDD_Log_Files {
get {
return ResourceManager.GetString("MHDD_Log_Files", resourceCulture);
}
}
public static string Title_File_path {
get {
return ResourceManager.GetString("Title_File_path", resourceCulture);
}
}
public static string Title_MHDD_log_viewer {
get {
return ResourceManager.GetString("Title_MHDD_log_viewer", resourceCulture);
}
}
public static string Title_MHDD_Version {
get {
return ResourceManager.GetString("Title_MHDD_Version", resourceCulture);
}
}
public static string Title_Firmware {
get {
return ResourceManager.GetString("Title_Firmware", resourceCulture);
}
}
public static string Title_Total_sectors {
get {
return ResourceManager.GetString("Title_Total_sectors", resourceCulture);
}
}
public static string Title_Scan_block_size {
get {
return ResourceManager.GetString("Title_Scan_block_size", resourceCulture);
}
}
public static string Menu_Open_MHDD_log {
get {
return ResourceManager.GetString("Menu_Open_MHDD_log", resourceCulture);
}
}
public static string The_specified_file_is_not_a_correct_MHDD_log_file {
get {
return ResourceManager.GetString("The_specified_file_is_not_a_correct_MHDD_log_file", resourceCulture);
}
}
public static string _0_bytes_markup {
get {
return ResourceManager.GetString("_0_bytes_markup", resourceCulture);
}
}
public static string _0_sectors_markup {
get {
return ResourceManager.GetString("_0_sectors_markup", resourceCulture);
}
}
public static string Error_parsing_MHDD_log_header_0 {
get {
return ResourceManager.GetString("Error_parsing_MHDD_log_header_0", resourceCulture);
}
}
}
}

View File

@@ -2601,7 +2601,7 @@ Probadores:
<value>[slateblue1]Sectores[/]</value>
</data>
<data name="Title_Sector_size" xml:space="preserve">
<value>[slateblue1]Tamaño de sector[/]</value>
<value>[bold][slateblue1]Tamaño de sector[/][/]</value>
</data>
<data name="Title_Security_Sector" xml:space="preserve">
<value>Sector de Seguridad</value>
@@ -3140,4 +3140,40 @@ Probadores:
<data name="Nothing_opened" xml:space="preserve">
<value>Nada abierto.</value>
</data>
<data name="MHDD_Log_Files" xml:space="preserve">
<value>Archivos de registro MHDD</value>
</data>
<data name="Title_File_path" xml:space="preserve">
<value>[bold][slateblue1]Archivo[/][/]</value>
</data>
<data name="Title_MHDD_log_viewer" xml:space="preserve">
<value>Visor de registros MHDD</value>
</data>
<data name="Title_MHDD_Version" xml:space="preserve">
<value>[bold][slateblue1]Versión de MHDD[/][/]</value>
</data>
<data name="Title_Firmware" xml:space="preserve">
<value>[bold][slateblue1]Firmware[/][/]</value>
</data>
<data name="Title_Total_sectors" xml:space="preserve">
<value>[bold][slateblue1]Sectores totales[/][/]</value>
</data>
<data name="Title_Scan_block_size" xml:space="preserve">
<value>[bold][slateblue1]Tamaño del bloque de escaneo[/][/]</value>
</data>
<data name="Menu_Open_MHDD_log" xml:space="preserve">
<value>Abrir registro _MHDD</value>
</data>
<data name="The_specified_file_is_not_a_correct_MHDD_log_file" xml:space="preserve">
<value>El archivo especificado no es un registro MHDD correcto.</value>
</data>
<data name="_0_bytes_markup" xml:space="preserve">
<value>[aqua]{0}[/] [slateblue1]bytes[/]</value>
</data>
<data name="_0_sectors_markup" xml:space="preserve">
<value>[violet]{0}[/] [slateblue1]sectors[/]</value>
</data>
<data name="Error_parsing_MHDD_log_header_0" xml:space="preserve">
<value>Error interpretando cabecera del registro MHDD: {0}</value>
</data>
</root>

View File

@@ -770,7 +770,7 @@ In you are unsure, please press N to not continue.</value>
<value>[slateblue1]Sectors[/]</value>
</data>
<data name="Title_Sector_size" xml:space="preserve">
<value>[slateblue1]Sector size[/]</value>
<value>[bold][slateblue1]Sector size[/][/]</value>
</data>
<data name="Title_Creation_time" xml:space="preserve">
<value>[slateblue1]Creation time[/]</value>
@@ -3216,4 +3216,40 @@ Do you want to continue?</value>
<data name="Nothing_opened" xml:space="preserve">
<value>Nothing opened.</value>
</data>
<data name="MHDD_Log_Files" xml:space="preserve">
<value>MHDD Log Files</value>
</data>
<data name="Title_File_path" xml:space="preserve">
<value>[bold][slateblue1]File path[/][/]</value>
</data>
<data name="Title_MHDD_log_viewer" xml:space="preserve">
<value>MHDD log viewer</value>
</data>
<data name="Title_MHDD_Version" xml:space="preserve">
<value>[bold][slateblue1]MHDD Version[/][/]</value>
</data>
<data name="Title_Firmware" xml:space="preserve">
<value>[bold][slateblue1]Firmware[/][/]</value>
</data>
<data name="Title_Total_sectors" xml:space="preserve">
<value>[bold][slateblue1]Total sectors[/][/]</value>
</data>
<data name="Title_Scan_block_size" xml:space="preserve">
<value>[bold][slateblue1]Scan block size[/][/]</value>
</data>
<data name="Menu_Open_MHDD_log" xml:space="preserve">
<value>Open _MHDD log</value>
</data>
<data name="The_specified_file_is_not_a_correct_MHDD_log_file" xml:space="preserve">
<value>The specified file is not a correct MHDD log file.</value>
</data>
<data name="_0_bytes_markup" xml:space="preserve">
<value>[aqua]{0}[/] [slateblue1]bytes[/]</value>
</data>
<data name="_0_sectors_markup" xml:space="preserve">
<value>[violet]{0}[/] [slateblue1]sectors[/]</value>
</data>
<data name="Error_parsing_MHDD_log_header_0" xml:space="preserve">
<value>Error parsing MHDD log header: {0}</value>
</data>
</root>

View File

@@ -1363,6 +1363,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=mfgsz/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mgui/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mhdd/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mhdd/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mhddlog/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Microdrive/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Micropolis/@EntryIndexedValue">True</s:Boolean>