Files
Flaro/server/tests/Halo.Core.Tests/SidecarTests.cs
T
ScarriffleandClaude Opus 5 f98b995050 Beiwerk im Medienordner, und eine Zuordnung lässt sich korrigieren
Plakate, Hintergründe und NFO-Dateien landen jetzt neben dem Film — und eine
falsche Zuordnung lässt sich richtigstellen, ohne dass der nächste Scan sie
wieder überschreibt.

    Der Untergang (2004)/
    ├── Der Untergang (2004).mkv
    ├── poster.jpg
    ├── fanart.jpg
    └── movie.nfo

Bei Serien liegt `tvshow.nfo` eine Ebene über dem Staffelordner, je Folge
kommen NFO und Standbild daneben. Liegen mehrere Filme in einem Ordner, heisst
das Beiwerk `Titel (Jahr)-poster.jpg` — sonst überschrieben sie sich
gegenseitig.

**Die Erlaubnisliste ist der ganze Schutz.** Damit das geht, muss die
Medieneinhängung ohne `:ro` auskommen, und damit könnte der Server dort alles
anstellen. Dass er es nicht tut, hängt an `SidecarNames`: sechs Arten von
Datei, nichts sonst. Ein Name, der nicht daraufsteht, wirft — laut, nicht
still, denn das wäre ein Fehler im Programm. Mediendateien werden nie
geändert, umbenannt oder gelöscht; es gibt keine Stelle, die das könnte.
Geprüft an echten Dateien, einschliesslich der Versuche mit `../`, mit
absoluten Pfaden und mit einer Mediendatei als Ziel.

Standardmässig **aus**. In fremde Medienordner zu schreiben ist nichts, was
ein Server ungefragt tun sollte.

**Gelesen wird auch ohne Schreiberlaubnis.** Eine vorhandene NFO ist eine
Zuordnung, die jemand getroffen hat — Halo nimmt sie und sucht gar nicht
erst. Dasselbe gilt für Ordnernamen der Form `[tmdbid-613]`. Halo liest sie
und benennt **nichts** um: Umbenennen ist der einzige Eingriff, der eine
Bibliothek wirklich zerlegen kann.

Nebenbei: eine NFO aus dem Medienordner hat Halo nicht geschrieben. DTDs sind
beim Lesen abgeschaltet — eine untergeschobene Datei mit externer Entität
könnte sonst den Server dazu bringen, `/config/halo.db` einzulesen und als
Titel auszuliefern. Mit Prüfung.

---

**Der Fehler, den erst der echte Lauf zeigte** — und er ist die zweite
Auflage eines schon einmal behobenen:

Nach der Korrektur stand im Ordner „Dark (2017)", in der Datenbank aber 2004.
Der nächste Scan suchte nach *(Dark, 2017)*, fand nichts, legte den Film neu
an und löschte den korrigierten als verwaist. Überlebt hat die Zuordnung nur,
weil sie in der NFO stand — das System hat sich selbst gerettet, aber der
Eintrag war ein anderer und das Jahr wieder falsch.

Dieselbe Klasse wie bei `ParsedTitle`: zugeordnet wurde über einen Wert, den
die Metadatenquelle verändert. Jetzt gibt es `ParsedYear`, und das ändert sich
nie. Mit Prüfung, die genau diesen Ablauf festhält.

---

Dazu von TMDB: die IMDb-Kennung wird nachgeschlagen, damit sie in der NFO
steht — wer später auf ein anderes Werkzeug wechselt, findet denselben Titel
wieder, auch wenn das TMDB gar nicht kennt.

308 Tests grün. Im Container nachgewiesen: Zuordnung falsch, korrigiert,
zweiter Scan — derselbe Eintrag, dieselbe Kennung, Jahr bleibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:01:26 +02:00

224 lines
8.2 KiB
C#

using Halo.Core.Library;
namespace Halo.Core.Tests;
/// <summary>Was Halo in einen Medienordner schreiben darf.</summary>
///
/// <remarks>
/// <b>Die wichtigste Prüfdatei des Projekts.</b> Damit Plakate neben dem Film
/// landen können, ist die Einhängung schreibbar — und damit hängt der Schutz
/// des gesamten Bestands an dieser einen Liste.
/// </remarks>
public class SidecarNamesTests
{
[TestCase("poster.jpg")]
[TestCase("fanart.jpg")]
[TestCase("logo.png")]
[TestCase("movie.nfo")]
[TestCase("tvshow.nfo")]
[TestCase("Dark - S01E01.nfo")]
[TestCase("Dark - S01E01-thumb.jpg")]
[TestCase("POSTER.JPG")]
public void Beiwerk_darf_geschrieben_werden(string name)
{
Assert.That(SidecarNames.MayWrite(name), Is.True);
}
/// <summary>Eine Mediendatei wird nie angefasst — auch nicht, wenn jemand es verlangt.</summary>
[TestCase("film.mkv")]
[TestCase("Der Untergang (2004).mp4")]
[TestCase("film.mkv.nfo.mkv")]
public void Mediendateien_niemals(string name)
{
Assert.That(SidecarNames.MayWrite(name), Is.False);
}
/// <summary>Kein Weg aus dem Ordner heraus.</summary>
///
/// <remarks>
/// Käme hier ein Pfadbestandteil durch, liesse sich über einen
/// erfundenen „Dateinamen" in jedes Verzeichnis des Containers
/// schreiben — auch in das, in dem die Datenbank liegt.
/// </remarks>
[TestCase("../poster.jpg")]
[TestCase("../../etc/passwd")]
[TestCase("/etc/poster.jpg")]
[TestCase("unterordner/poster.jpg")]
[TestCase("..\\poster.jpg")]
[TestCase("ordner\\movie.nfo")]
[TestCase("..movie.nfo")]
public void Kein_Pfad_kommt_durch(string versuch)
{
Assert.That(SidecarNames.MayWrite(versuch), Is.False);
}
[TestCase("")]
[TestCase(" ")]
[TestCase(".nfo")]
[TestCase("-thumb.jpg")]
[TestCase(".versteckt.nfo")]
[TestCase("poster.png")]
[TestCase("beliebig.txt")]
public void Alles_andere_wird_abgelehnt(string versuch)
{
Assert.That(SidecarNames.MayWrite(versuch), Is.False);
}
/// <summary>Zwei Filme im selben Ordner dürfen sich nicht überschreiben.</summary>
[Test]
public void Beiwerk_mit_Dateinamen_davor_ist_auch_erlaubt()
{
Assert.That(SidecarNames.Prefixed("Matrix (1999).mkv", SidecarNames.MoviePoster),
Is.EqualTo("Matrix (1999)-poster.jpg"));
foreach (var beiwerk in new[] { SidecarNames.MoviePoster, SidecarNames.MovieBackdrop, SidecarNames.MovieLogo })
Assert.That(SidecarNames.MayWrite(SidecarNames.Prefixed("Matrix (1999).mkv", beiwerk)), Is.True);
}
[Test]
public void Folgen_Beiwerk_heisst_wie_die_Folge()
{
Assert.That(SidecarNames.EpisodeNfo("Dark - S01E01.mkv"), Is.EqualTo("Dark - S01E01.nfo"));
Assert.That(SidecarNames.EpisodeThumb("Dark - S01E01.mkv"), Is.EqualTo("Dark - S01E01-thumb.jpg"));
}
/// <summary>Was erzeugt wird, muss auch erlaubt sein.</summary>
///
/// <remarks>
/// Die beiden Hälften könnten auseinanderlaufen — jemand ändert das Muster
/// für Folgen und vergisst die Liste. Dann schriebe Halo nichts mehr und
/// niemand wüsste, warum.
/// </remarks>
[TestCase("Dark - S01E01.mkv")]
[TestCase("Notruf.Hafenkante.S01E01.German.1080p.mkv")]
[TestCase("Serie 1x01.avi")]
public void Die_erzeugten_Namen_stehen_auf_der_Liste(string mediendatei)
{
Assert.That(SidecarNames.MayWrite(SidecarNames.EpisodeNfo(mediendatei)), Is.True);
Assert.That(SidecarNames.MayWrite(SidecarNames.EpisodeThumb(mediendatei)), Is.True);
}
}
public class ProviderIdParserTests
{
[Test]
public void Eine_TMDB_Kennung_im_Ordnernamen_wird_gelesen()
{
var kennungen = ProviderIdParser.FromPath("/media/Filme/Der Untergang (2004) [tmdbid-613]/film.mkv");
Assert.That(kennungen.Tmdb, Is.EqualTo(613));
}
[Test]
public void Eine_IMDb_Kennung_wird_gelesen_und_kleingeschrieben()
{
var kennungen = ProviderIdParser.FromPath("/media/Filme/Matrix (1999) [imdbid-TT0133093]/film.mkv");
Assert.That(kennungen.Imdb, Is.EqualTo("tt0133093"));
}
[Test]
public void Beide_nebeneinander_gehen_auch()
{
var kennungen = ProviderIdParser.FromPath("/m/Film (2004) [tmdbid-613] [imdbid-tt0363163]/f.mkv");
Assert.That((kennungen.Tmdb, kennungen.Imdb), Is.EqualTo((613, "tt0363163")));
}
[TestCase("/media/Filme/Der Untergang (2004)/film.mkv")]
[TestCase("/media/Filme/Film [1080p]/film.mkv")]
[TestCase("")]
[TestCase("/media/Film [tmdbid-]/f.mkv")]
[TestCase("/media/Film [imdbid-nichts]/f.mkv")]
public void Ohne_Kennung_kommt_nichts_zurueck(string pfad)
{
Assert.That(ProviderIdParser.FromPath(pfad).Any, Is.False);
}
}
public class NfoTests
{
[Test]
public void Ein_Film_wird_geschrieben_und_wieder_gelesen()
{
var xml = Nfo.Write(new MovieNfo(
"Der Untergang", 2004, "Berlin, April 1945.",
new ProviderIds(613, "tt0363163"), Locked: true));
Assert.That(xml, Does.StartWith("<?xml"));
Assert.That(xml, Does.Contain("<title>Der Untergang</title>"));
Assert.That(xml, Does.Contain("<year>2004</year>"));
var gelesen = Nfo.ReadIds(xml);
Assert.That((gelesen.Tmdb, gelesen.Imdb), Is.EqualTo((613, "tt0363163")));
Assert.That(Nfo.ReadLocked(xml), Is.True);
}
/// <summary>Kennungen stehen doppelt drin, für alte wie neue Werkzeuge.</summary>
[Test]
public void Die_Kennungen_stehen_in_beiden_Formen_da()
{
var xml = Nfo.Write(new MovieNfo("Egal", null, null, new ProviderIds(613, "tt0363163"), false));
Assert.That(xml, Does.Contain("""<uniqueid type="tmdb" default="true">613</uniqueid>"""));
Assert.That(xml, Does.Contain("<tmdbid>613</tmdbid>"));
Assert.That(xml, Does.Contain("""<uniqueid type="imdb">tt0363163</uniqueid>"""));
Assert.That(xml, Does.Contain("<imdbid>tt0363163</imdbid>"));
}
[Test]
public void Die_aeltere_Form_allein_wird_auch_gelesen()
{
var gelesen = Nfo.ReadIds("<movie><tmdbid>613</tmdbid><imdbid>tt0363163</imdbid></movie>");
Assert.That((gelesen.Tmdb, gelesen.Imdb), Is.EqualTo((613, "tt0363163")));
}
[Test]
public void Sonderzeichen_im_Titel_werden_richtig_geschrieben()
{
var xml = Nfo.Write(new MovieNfo("Tom & Jerry <der Film>", null, null, ProviderIds.None, false));
Assert.That(xml, Does.Contain("Tom &amp; Jerry &lt;der Film&gt;"));
Assert.That(Nfo.ReadIds(xml).Any, Is.False, "Und die Datei bleibt lesbar");
}
/// <summary>Eine von Hand geschriebene NFO ist oft kaputt — das darf nichts abbrechen.</summary>
[TestCase("")]
[TestCase("kein xml")]
[TestCase("<movie><title>offen")]
[TestCase("<movie><tmdbid>keine zahl</tmdbid></movie>")]
public void Unlesbares_ergibt_nichts_statt_eines_Absturzes(string kaputt)
{
Assert.That(Nfo.ReadIds(kaputt).Any, Is.False);
Assert.That(Nfo.ReadLocked(kaputt), Is.False);
}
/// <summary>Eine NFO aus dem Medienordner hat Halo nicht geschrieben.</summary>
///
/// <remarks>
/// <b>Ohne abgeschaltete DTDs wäre das ein Weg, fremde Dateien zu lesen.</b>
/// Eine untergeschobene NFO mit einer externen Entität könnte den Server
/// dazu bringen, <c>/config/halo.db</c> einzulesen und den Inhalt als
/// Titel auszuliefern.
/// </remarks>
[Test]
public void Eine_NFO_mit_externer_Entitaet_wird_nicht_ausgewertet()
{
var angriff = """
<?xml version="1.0"?>
<!DOCTYPE movie [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<movie><title>&xxe;</title><tmdbid>613</tmdbid></movie>
""";
Assert.That(Nfo.ReadIds(angriff).Any, Is.False, "Die Datei wird gar nicht erst verarbeitet");
Assert.That(Nfo.ReadLocked(angriff), Is.False);
}
[Test]
public void Eine_Folge_bekommt_Staffel_und_Nummer()
{
var xml = Nfo.Write(new EpisodeNfo("Geheimnisse", 1, 1, "Ein Junge verschwindet."));
Assert.That(xml, Does.Contain("<season>1</season>"));
Assert.That(xml, Does.Contain("<episode>1</episode>"));
Assert.That(xml, Does.Contain("<title>Geheimnisse</title>"));
}
}