Skip to content

Commit

Permalink
Vhf2 file format (#57)
Browse files Browse the repository at this point in the history
* New vhf1 file format implementation

* New vhf2 file format for the established data model

* Replace legacy IO implementation with new file formats

* Clarify available file format in save file dialog

* Fix vhf1 format for PracticeMode.AskForBothMixed

* Make Vhf2 header JSON and add support for non-breaking changes in new file versions

* Improve dutch translation

Co-authored-by: jdelange22 <[email protected]>

* Fix normal save feature

* Option to share books without practice results

* Improve dutch translation

Co-authored-by: jdelange22 <[email protected]>

---------

Co-authored-by: jdelange22 <[email protected]>
  • Loading branch information
daniel-lerch and jdelange22 authored May 12, 2024
1 parent 6924ac2 commit 1bc8f41
Show file tree
Hide file tree
Showing 41 changed files with 1,884 additions and 1,294 deletions.
72 changes: 59 additions & 13 deletions docs/fileformats.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,55 @@

Vocup uses different proprietary formats:

| Extension | Name | Usage |
|-----------|---------------------------------|------------------|
| .vhf | **V**okabel**h**eft **f**ile | Vocabulary data |
| .vhr | **V**okabel**h**eft **r**esults | Practice results |
| .vdp | | Backup file |
| Extension | Name | Usage | Remarks |
|-----------|---------------------------------|------------------|---------|
| .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 1.9.0 |

## .vhf
## .vhf v2

Since Vocup v2, _Vokabelheft files_ are ZIP archives and not encrypted anymore.
Vocup uses the ZIP header to determine whether a .vhf file is vhf1 or vhf2.

The header file makes it easier to identify a ZIP archive as a Vocup file.
Future file formats may maintain compatibility with Vocup v2 by keeping a `book.2.json` file but also adding a `book.3.json` with breaking changes.

### Example

`VOCUP VOCABULARY BOOK`
```json
{
"fileVersion": "2.0",
}
```

`book.2.json`
```json
{
"motherTongue": "Deutsch",
"foreignLanguage": "Englisch",
"practiceMode": "AskForForeignLanguage",
"words": [
{
"motherTongue": "Diskriminierung",
"foreignLang": "discrimination",
"foreignLangSynonym": null,
"practiceStateNumber": 0,
"practiceDate": ""
},
{
"motherTongue": "eingehend untersuchen (AE/BE)",
"foreignLang": "to scrutinize",
"foreignLangSynonym": "to scrutinise",
"practiceStateNumber": 1,
"practiceDate": "2018-04-06T23:04:21"
}
]
}
```

## .vhf v1

*Vokabelheft files* contain the base64 encoded ciphertext of the DES encrypted inner file.
The encryption is done with a hard-coded key and offers no security but complicates reverse engineering of the file format.
Expand All @@ -35,14 +77,14 @@ Diskriminierung#discrimination#
eingehend untersuchen (AE/BE)#to scrutinize#to scrutinise
Entfremdung, Distanzierung#alienation#
entstellen#to warp#
enttäuscht#disappointed#crestfallen
entwöhnen#to wean off#
enttäuscht#disappointed#crestfallen
entwähnen#to wean off#
Freiheit#freedom#liberty
Gefängnisstrafe#imprisonment#jail sentence
gehäuft, gestapelt#piled#
Gefängnisstrafe#imprisonment#jail sentence
gehäuft, gestapelt#piled#
Gewaltenteilung#system of checks and balances#
gipfeln (in)#to culminate (in)#
Gründungsväter#Founding Fathers#
Gründungsväter#Founding Fathers#
halbieren#to halve#
Haltung, Einstellung#attitude#
Handgelenk#wrist#
Expand All @@ -52,6 +94,8 @@ Haufen, Stapel#pile#

## .vhr

> ⚠️ This file format is deprecated. Since vhf2 results are saved in the vocabulary book.
Like *Vokabelheft files* *Vokabelheft result* files are DES encrypted and saved as a base64 string.

The inner file contains 2 lines of metadata.
Expand Down Expand Up @@ -91,7 +135,9 @@ D:\Schule\Englisch\Vocabulary\Year 11.vhf

## .vdp

This format is used for Vocup Backups. It is basically a zip file using `Deflate` compression:
> ⚠️ 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 was used for Vocup Backups. It is basically a zip file using `Deflate` compression:
```
*.vdp/
chars/
Expand Down Expand Up @@ -131,4 +177,4 @@ Like the other logfiles it included one line per item:
#### vhr
The `vhr` folder works pretty much like the `chars` folder.
All `.vhr` files are stored in this folder with their original name.
In the `vhr.log` file all these files are simply listed.
In the `vhr.log` file all these files are simply listed.
16 changes: 8 additions & 8 deletions src/Vocup/Forms/MergeFiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ private void BtnAdd_Click(object sender, EventArgs e)
{
Title = Words.AddVocabularyBooks,
InitialDirectory = Program.Settings.VhfPath,
Filter = Words.VocupVocabularyBookFile + " (*.vhf)|*.vhf",
Filter = Words.FileFormatVhf + " (*.vhf)|*.vhf",
Multiselect = true
};

if (addFile.ShowDialog() == DialogResult.OK)
{
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.FileFormatVhf2} (*.vhf)|*.vhf|{Words.FileFormatVhf1} (*.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
115 changes: 115 additions & 0 deletions src/Vocup/IO/BookFileFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.IO;
using System.Windows;
using Vocup.Models;
using Vocup.Properties;

namespace Vocup.IO;

public abstract class BookFileFormat
{
public static Vhf1Format Vhf1 { get; } = Vhf1Format.Instance;
public static Vhf2Format Vhf2 { get; } = Vhf2Format.Instance;

public static bool TryDetectAndRead(string path, VocabularyBook book, string vhrPath)
{
try
{
if (!DetectAndRead(path, book, vhrPath))
MessageBox.Show(Messages.VhfCompatMode, Messages.VhfCompatModeT, MessageBoxButton.OK, MessageBoxImage.Information);
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 bool 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 (StartsWithZipHeader(stream))
{
return Vhf2Format.Instance.Read(stream, book);
}
else
{
Vhf1Format.Instance.Read(stream, book, vhrPath);
return true;
}
}

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

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, bool includeResults = true)
{
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, includeResults);
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, bool includeResults);

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
Loading

0 comments on commit 1bc8f41

Please sign in to comment.