Skip to content

Commit

Permalink
Replace legacy IO implementation with new file formats
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-lerch committed Apr 23, 2024
1 parent d7ed505 commit 851f003
Show file tree
Hide file tree
Showing 23 changed files with 305 additions and 778 deletions.
6 changes: 3 additions & 3 deletions docs/fileformats.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Vocup uses different proprietary formats:
|-----------|---------------------------------|------------------|---------|
| .vhf | **V**okabel**h**eft **f**ile | Vocabulary data | |
| .vhr | **V**okabel**h**eft **r**esults | Practice results | Not used for vhf2 files anymore |
| .vdp | | Backup file | Backup creation removed in Vocup 1.8.0<br>Backup restore removed in Vocup 2.0.0 |
| .vdp | | Backup file | Backup creation removed in Vocup 1.8.0<br>Backup restore removed in Vocup 1.9.0 |

## .vhf v2

Expand Down Expand Up @@ -130,9 +130,9 @@ D:\Schule\Englisch\Vocabulary\Year 11.vhf

## .vdp

> ⚠️ This file format is deprecated. Since Vocup 1.8.0 it is not possible to create new backups anymore. Since Vocup 2.0.0 is not possible to restore backups anymore.
> ⚠️ This file format is deprecated. Since Vocup 1.8.0 it is not possible to create new backups anymore. Since Vocup 1.9.0 is also not possible to restore backups anymore.
This format is used for Vocup Backups. It is basically a zip file using `Deflate` compression:
This format was used for Vocup Backups. It is basically a zip file using `Deflate` compression:
```
*.vdp/
chars/
Expand Down
14 changes: 7 additions & 7 deletions src/Vocup/Forms/MergeFiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ private void BtnAdd_Click(object sender, EventArgs e)
{
foreach (string file in addFile.FileNames)
{
VocabularyBook book = new VocabularyBook();
if (!VocabularyFile.ReadVhfFile(file, book))
VocabularyBook book = new();
if (!BookFileFormat.TryDetectAndRead(file, book, Program.Settings.VhrPath))
continue;
VocabularyFile.ReadVhrFile(book);

VocabularyBook conflict = books.Where(x => x.FilePath.Equals(book.FilePath, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
if (conflict != null)
{
Expand Down Expand Up @@ -124,18 +124,20 @@ private void TextBox_Enter(object sender, EventArgs e)

private void BtnSave_Click(object sender, EventArgs e)
{
BookFileFormat format;
string path;

using (SaveFileDialog save = new SaveFileDialog
{
Title = Words.SaveVocabularyBook,
FileName = TbMotherTongue.Text + " - " + TbForeignLang.Text,
InitialDirectory = Program.Settings.VhfPath,
Filter = Words.VocupVocabularyBookFile + " (*.vhf)|*.vhf"
Filter = $"{Words.VocupVocabularyBookFile} (*.vhf)|*.vhf|{Words.VocupVocabularyBookLegacy} (*.vhf)|*.vhf"
})
{
if (save.ShowDialog() == DialogResult.OK)
{
format = save.FilterIndex == 2 ? BookFileFormat.Vhf1 : BookFileFormat.Vhf2;
path = save.FileName;
}
else
Expand Down Expand Up @@ -164,10 +166,8 @@ private void BtnSave_Click(object sender, EventArgs e)

result.GenerateVhrCode();

if (!VocabularyFile.WriteVhfFile(path, result) ||
!VocabularyFile.WriteVhrFile(result))
if (!format.TryWrite(path, result, Program.Settings.VhrPath))
{
MessageBox.Show(Messages.VocupFileWriteError, Messages.VocupFileWriteErrorT, MessageBoxButtons.OK, MessageBoxIcon.Error);
DialogResult = DialogResult.Abort;
}
else
Expand Down
96 changes: 82 additions & 14 deletions src/Vocup/IO/BookFileFormat.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using Vocup.IO.Vhf1;
using Vocup.IO.Vhf2;
using Vocup.Models;
using Vocup.Util;
using Vocup.Properties;

namespace Vocup.IO;

Expand All @@ -13,31 +13,99 @@ public abstract class BookFileFormat
public static Vhf1Format Vhf1 { get; } = Vhf1Format.Instance;
public static Vhf2Format Vhf2 { get; } = Vhf2Format.Instance;

public static async ValueTask DetectAndRead(string path, VocabularyBook book, string vhrPath)
public static bool TryDetectAndRead(string path, VocabularyBook book, string vhrPath)
{
await default(HopToThreadPoolAwaitable); // Perform IO operations on a separate thread
try
{
DetectAndRead(path, book, vhrPath);
return true;
}
catch (VhfFormatException ex)
{
(string message, string title) = ex.ErrorCode switch
{
VhfError.InvalidVersion => (Messages.VhfInvalidVersion, Messages.VhfCorruptFileT),
VhfError.InvalidVhrCode => (Messages.VhfInvalidVhrCode, Messages.VhfCorruptFileT),
VhfError.InvalidLanguages => (Messages.VhfInvalidLanguages, Messages.VhfCorruptFileT),
VhfError.InvalidRow => (Messages.VhfInvalidRow, Messages.VhfCorruptFileT),
VhfError.UpdateRequired => (Messages.VhfMustUpdate, Messages.VhfMustUpdateT),
_ => (Messages.VhfCorruptFile, Messages.VhfCorruptFileT),
};
MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
catch (Exception ex)
{
MessageBox.Show(string.Format(Messages.VocupFileReadError, ex.Message), Messages.VocupFileReadErrorT, MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
}

public static void DetectAndRead(string path, VocabularyBook book, string vhrPath)
{
//await default(HopToThreadPoolAwaitable); // Perform IO operations on a separate thread

using FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read);

if (await StartsWithZipHeader(stream).ConfigureAwait(false))
await Vhf2Format.Instance.Read(stream, book).ConfigureAwait(false);
if (StartsWithZipHeader(stream))
Vhf2Format.Instance.Read(stream, book);
else
await Vhf1Format.Instance.Read(stream, book, vhrPath).ConfigureAwait(false);
Vhf1Format.Instance.Read(stream, book, vhrPath);
}

private static async ValueTask<bool> StartsWithZipHeader(Stream stream)
private static bool StartsWithZipHeader(Stream stream)
{
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException("Stream must be readable and seekable.", nameof(stream));

Memory<byte> buffer = new byte[4];
bool zipHeader = await stream.ReadAsync(buffer).ConfigureAwait(false) == 4
&& buffer.Span[0] == 0x50
&& buffer.Span[1] == 0x4B
&& buffer.Span[2] == 0x03
&& buffer.Span[3] == 0x04;
Span<byte> buffer = stackalloc byte[4];
bool zipHeader = stream.Read(buffer) == 4
&& buffer[0] == 0x50
&& buffer[1] == 0x4B
&& buffer[2] == 0x03
&& buffer[3] == 0x04;

stream.Seek(0, SeekOrigin.Begin);
return zipHeader;
}

public bool TryWrite(string path, VocabularyBook book, string vhrPath)
{
try
{
//await default(HopToThreadPoolAwaitable); // Perform IO operations on a separate thread

using FileStream stream = new(path, FileMode.Create, FileAccess.Write, FileShare.None);
Write(stream, book, vhrPath);
return true;
}
catch (Exception ex)
{
MessageBox.Show(string.Format(Messages.VocupFileWriteError, ex.Message), Messages.VocupFileWriteErrorT, MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
}

public abstract void Write(FileStream stream, VocabularyBook book, string vhrPath);

protected static bool TryDeleteVhrFile(string? vhrCode, string vhrPath)
{
try
{
if (vhrCode != null)
{
string path = Path.Combine(vhrPath, vhrCode + ".vhr");

if (File.Exists(path))
{
File.Delete(path);
return true;
}
}
}
// Cleaning up practice results is just nice to have.
catch { }

return false;
}
}
8 changes: 4 additions & 4 deletions src/Vocup/IO/CsvFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
using Vocup.Models;
using Vocup.Properties;

namespace Vocup.IO.Internal;
namespace Vocup.IO;

internal class CsvFile
public static class CsvFile
{
public bool Import(string path, VocabularyBook book, bool importSettings)
public static bool Import(string path, VocabularyBook book, bool importSettings)
{
try
{
Expand Down Expand Up @@ -105,7 +105,7 @@ public bool Import(string path, VocabularyBook book, bool importSettings)
return false;
}

public bool Export(string path, VocabularyBook book)
public static bool Export(string path, VocabularyBook book)
{
try
{
Expand Down
46 changes: 29 additions & 17 deletions src/Vocup/IO/Vhf1/Vhf1Format.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Vocup.Models;

namespace Vocup.IO.Vhf1;
Expand All @@ -15,9 +14,9 @@ public class Vhf1Format : BookFileFormat

private Vhf1Format() { }

public async ValueTask Read(FileStream stream, VocabularyBook book, string vhrPath)
public void Read(FileStream stream, VocabularyBook book, string vhrPath)
{
string decrypted = await ReadAndDecryptAsync(stream).ConfigureAwait(false);
string decrypted = ReadAndDecrypt(stream);
using StringReader reader = new(decrypted);

string? version = reader.ReadLine();
Expand Down Expand Up @@ -66,11 +65,11 @@ public async ValueTask Read(FileStream stream, VocabularyBook book, string vhrPa
if (!string.IsNullOrEmpty(vhrCode))
{
// Read results from .vhr file
await ReadResults(book, stream.Name, vhrCode, vhrPath).ConfigureAwait(false);
ReadResults(book, stream.Name, vhrCode, vhrPath);
}
}

public async ValueTask Write(FileStream stream, VocabularyBook book, string vhrPath)
public override void Write(FileStream stream, VocabularyBook book, string vhrPath)
{
StringBuilder content = new();
content.AppendLine("1.0");
Expand All @@ -87,13 +86,15 @@ public async ValueTask Write(FileStream stream, VocabularyBook book, string vhrP
content.AppendLine(word.ForeignLangSynonym);
}

await EncryptAndWriteAsync(stream, content.ToString()).ConfigureAwait(false);
EncryptAndWrite(stream, content.ToString());

if (!string.IsNullOrEmpty(book.VhrCode))
{
// Write results to .vhr file
await WriteResults(book, stream.Name, book.VhrCode, vhrPath).ConfigureAwait(false);
WriteResults(book, stream.Name, book.VhrCode, vhrPath);
}

book.FilePath = stream.Name;
}

public string GenerateVhrCode()
Expand All @@ -114,12 +115,12 @@ public string GenerateVhrCode()
return new string(code);
}

private async ValueTask ReadResults(VocabularyBook book, string fileName, string vhrCode, string vhrPath)
private void ReadResults(VocabularyBook book, string fileName, string vhrCode, string vhrPath)
{
try
{
using FileStream file = new(Path.Combine(vhrPath, vhrCode + ".vhr"), FileMode.Open, FileAccess.Read, FileShare.Read);
string decrypted = await ReadAndDecryptAsync(file).ConfigureAwait(false);
string decrypted = ReadAndDecrypt(file);
using StringReader reader = new(decrypted);

string? path = reader.ReadLine();
Expand All @@ -128,7 +129,9 @@ private async ValueTask ReadResults(VocabularyBook book, string fileName, string
if (string.IsNullOrWhiteSpace(path) ||
string.IsNullOrWhiteSpace(mode) || !int.TryParse(mode, out int imode) || !((PracticeMode)imode).IsValid())
{
return; // Ignore files with invalid header
// Delete .vhr files with corrupt header
TryDeleteVhrFile(vhrCode, vhrPath);
return;
}

var results = new List<(int stateNumber, DateTime date)>();
Expand All @@ -140,19 +143,27 @@ private async ValueTask ReadResults(VocabularyBook book, string fileName, string
string[] columns = line.Split('#');
if (columns.Length != 2 || !int.TryParse(columns[0], out int state) || state < 0)
{
// Delete .vhr files with corrupt entries
TryDeleteVhrFile(vhrCode, vhrPath);
return;
}
DateTime time = default;
if (!string.IsNullOrWhiteSpace(columns[1])
&& !DateTime.TryParseExact(columns[1], "dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out time))
{
// Delete .vhr files with corrupt entries
TryDeleteVhrFile(vhrCode, vhrPath);
return;
}
results.Add((state, time));
}

if (book.Words.Count != results.Count)
{
// Delete .vhr files that are not in sync anymore.
// This can only happen when using a cloud storage for .vhf files but not .vhr files.
// When using save as or by copying a file manually, a new .vhr file will be created.
TryDeleteVhrFile(vhrCode, vhrPath);
return;
}

Expand All @@ -176,11 +187,12 @@ private async ValueTask ReadResults(VocabularyBook book, string fileName, string
}
catch (Exception ex) when (ex is IOException || ex is VhfFormatException)
{
// Practice results are useful but not necessary so we ignore file not found and all other exceptions
// Delete corrupt .vhr files
TryDeleteVhrFile(vhrCode, vhrPath);
}
}

private async ValueTask WriteResults(VocabularyBook book, string fileName, string vhrCode, string vhrPath)
private void WriteResults(VocabularyBook book, string fileName, string vhrCode, string vhrPath)
{
string results;

Expand All @@ -204,16 +216,16 @@ private async ValueTask WriteResults(VocabularyBook book, string fileName, strin

string absoluteFileName = Path.Combine(vhrPath, vhrCode + ".vhr");
using FileStream file = new(absoluteFileName, FileMode.Create, FileAccess.Write, FileShare.None);
await EncryptAndWriteAsync(file, results).ConfigureAwait(false);
EncryptAndWrite(file, results);
}

private static async ValueTask<string> ReadAndDecryptAsync(Stream stream)
private static string ReadAndDecrypt(Stream stream)
{
try
{
byte[] ciphertext;
using (StreamReader reader = new(stream, Encoding.UTF8))
ciphertext = Convert.FromBase64String(await reader.ReadToEndAsync().ConfigureAwait(false));
ciphertext = Convert.FromBase64String(reader.ReadToEnd());

using MemoryStream plainstream = new();
using DES csp = DES.Create();
Expand All @@ -231,7 +243,7 @@ private static async ValueTask<string> ReadAndDecryptAsync(Stream stream)
}
}

private static async ValueTask EncryptAndWriteAsync(Stream stream, string content)
private static void EncryptAndWrite(Stream stream, string content)
{
byte[] buffer = Encoding.UTF8.GetBytes(content);
using StreamWriter writer = new(stream, Encoding.UTF8);
Expand All @@ -243,6 +255,6 @@ private static async ValueTask EncryptAndWriteAsync(Stream stream, string conten
using CryptoStream plainstream = new(cipherstream, transform, CryptoStreamMode.Write);
plainstream.Write(buffer, 0, buffer.Length);
plainstream.FlushFinalBlock();
await writer.WriteAsync(Convert.ToBase64String(cipherstream.ToArray())).ConfigureAwait(false);
writer.Write(Convert.ToBase64String(cipherstream.ToArray()));
}
}
Loading

0 comments on commit 851f003

Please sign in to comment.