From 4e06c565d81e953ffb4d634e76c46d3f3fdd4f2f Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Tue, 18 Nov 2025 17:14:48 +0000 Subject: [PATCH] [GUI] Implement MHDD log viewer functionality --- Aaru.Gui/FilePickerFileTypes.cs | 6 + .../ViewModels/Windows/MainWindowViewModel.cs | 53 +++++ .../ViewModels/Windows/MhddLogViewModel.cs | 207 ++++++++++++++++++ Aaru.Gui/Views/Windows/MainWindow.axaml | 2 + Aaru.Gui/Views/Windows/MhddLogView.axaml | 97 ++++++++ Aaru.Gui/Views/Windows/MhddLogView.axaml.cs | 57 +++++ Aaru.Localization/UI.Designer.cs | 72 ++++++ Aaru.Localization/UI.es.resx | 38 +++- Aaru.Localization/UI.resx | 38 +++- Aaru.sln.DotSettings | 1 + 10 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 Aaru.Gui/ViewModels/Windows/MhddLogViewModel.cs create mode 100644 Aaru.Gui/Views/Windows/MhddLogView.axaml create mode 100644 Aaru.Gui/Views/Windows/MhddLogView.axaml.cs diff --git a/Aaru.Gui/FilePickerFileTypes.cs b/Aaru.Gui/FilePickerFileTypes.cs index 19b82e8ec..7ff52475f 100644 --- a/Aaru.Gui/FilePickerFileTypes.cs +++ b/Aaru.Gui/FilePickerFileTypes.cs @@ -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"] + }; } \ No newline at end of file diff --git a/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs b/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs index 88bdeef18..2866906f9 100644 --- a/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs +++ b/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs @@ -1,3 +1,35 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : MainWindowViewModel.cs +// Author(s) : Natalia Portillo +// +// 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 . +// +// ---------------------------------------------------------------------------- +// 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 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 diff --git a/Aaru.Gui/ViewModels/Windows/MhddLogViewModel.cs b/Aaru.Gui/ViewModels/Windows/MhddLogViewModel.cs new file mode 100644 index 000000000..cdefbb2eb --- /dev/null +++ b/Aaru.Gui/ViewModels/Windows/MhddLogViewModel.cs @@ -0,0 +1,207 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : MhddLogViewModel.cs +// Author(s) : Natalia Portillo +// +// 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 . +// +// ---------------------------------------------------------------------------- +// 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(); + } + + /// + /// Parses a number string that may contain thousands separators (en-US culture). + /// + /// The number string (e.g., "243,587" or "2,448") + /// The parsed number without separators + 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(); +} \ No newline at end of file diff --git a/Aaru.Gui/Views/Windows/MainWindow.axaml b/Aaru.Gui/Views/Windows/MainWindow.axaml index 540a72550..895807a24 100644 --- a/Aaru.Gui/Views/Windows/MainWindow.axaml +++ b/Aaru.Gui/Views/Windows/MainWindow.axaml @@ -19,6 +19,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Aaru.Gui/Views/Windows/MhddLogView.axaml.cs b/Aaru.Gui/Views/Windows/MhddLogView.axaml.cs new file mode 100644 index 000000000..0a7f714cf --- /dev/null +++ b/Aaru.Gui/Views/Windows/MhddLogView.axaml.cs @@ -0,0 +1,57 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : MhddLogView.axaml.cs +// Author(s) : Natalia Portillo +// +// 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 . +// +// ---------------------------------------------------------------------------- +// 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 + } + + /// + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + if(DataContext is MhddLogViewModel vm) vm?.LoadData(); + } +} \ No newline at end of file diff --git a/Aaru.Localization/UI.Designer.cs b/Aaru.Localization/UI.Designer.cs index 4b685a46a..d15814469 100644 --- a/Aaru.Localization/UI.Designer.cs +++ b/Aaru.Localization/UI.Designer.cs @@ -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); + } + } } } diff --git a/Aaru.Localization/UI.es.resx b/Aaru.Localization/UI.es.resx index 16cc49d35..1a0dbecc1 100644 --- a/Aaru.Localization/UI.es.resx +++ b/Aaru.Localization/UI.es.resx @@ -2601,7 +2601,7 @@ Probadores: [slateblue1]Sectores[/] - [slateblue1]Tamaño de sector[/] + [bold][slateblue1]Tamaño de sector[/][/] Sector de Seguridad @@ -3140,4 +3140,40 @@ Probadores: Nada abierto. + + Archivos de registro MHDD + + + [bold][slateblue1]Archivo[/][/] + + + Visor de registros MHDD + + + [bold][slateblue1]Versión de MHDD[/][/] + + + [bold][slateblue1]Firmware[/][/] + + + [bold][slateblue1]Sectores totales[/][/] + + + [bold][slateblue1]Tamaño del bloque de escaneo[/][/] + + + Abrir registro _MHDD + + + El archivo especificado no es un registro MHDD correcto. + + + [aqua]{0}[/] [slateblue1]bytes[/] + + + [violet]{0}[/] [slateblue1]sectors[/] + + + Error interpretando cabecera del registro MHDD: {0} + \ No newline at end of file diff --git a/Aaru.Localization/UI.resx b/Aaru.Localization/UI.resx index 2b4bc8a11..8a4af9b64 100644 --- a/Aaru.Localization/UI.resx +++ b/Aaru.Localization/UI.resx @@ -770,7 +770,7 @@ In you are unsure, please press N to not continue. [slateblue1]Sectors[/] - [slateblue1]Sector size[/] + [bold][slateblue1]Sector size[/][/] [slateblue1]Creation time[/] @@ -3216,4 +3216,40 @@ Do you want to continue? Nothing opened. + + MHDD Log Files + + + [bold][slateblue1]File path[/][/] + + + MHDD log viewer + + + [bold][slateblue1]MHDD Version[/][/] + + + [bold][slateblue1]Firmware[/][/] + + + [bold][slateblue1]Total sectors[/][/] + + + [bold][slateblue1]Scan block size[/][/] + + + Open _MHDD log + + + The specified file is not a correct MHDD log file. + + + [aqua]{0}[/] [slateblue1]bytes[/] + + + [violet]{0}[/] [slateblue1]sectors[/] + + + Error parsing MHDD log header: {0} + \ No newline at end of file diff --git a/Aaru.sln.DotSettings b/Aaru.sln.DotSettings index c97abab70..01f87d323 100644 --- a/Aaru.sln.DotSettings +++ b/Aaru.sln.DotSettings @@ -1363,6 +1363,7 @@ True True True + True True True True