Laden...

StreamWrapper zum Limitieren der Position und Length

Erstellt von sandreas vor einem Jahr Letzter Beitrag vor einem Jahr 953 Views
S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr
StreamWrapper zum Limitieren der Position und Length

Hallo zusammen,

ich habe folgendes Problem:

Ich möchte via Stream einen xxHash über eine Audio-Datei bilden, allerdings NUR über den Datenstrom der Audiodaten, nicht über die Metadaten...Ich habe zunächst mal einen FileStream von einer mp3-Datei, ganz klassisch von Position=0 bis Position=stream.Length - darin sind aber auch die Metadaten enthalten. Bilde ich nun einen Hash über den gesamten Stream, ändert sich der, sobald ich z.B. das Album ändere. Das möchte ich vermeiden.

Jetzt weiß ich, dass die Audio-Daten z.B. nur von Position=343 bis Position=8838492 (stream.Length - 889) gehen, die Datei also einen Metadaten-Header und Trailer hat, der mich für den Hash nicht interessiert.

Meiner xxHash-Funktion kann ich aber nur einen Stream geben, ohne Einschränkung von wo bis wo er den lesen können soll.


var s = file.OpenRead();
var hashBytes = BitConverter.GetBytes(xxHash64.ComputeHash(s));

Meine erste Idee ist natürlich einen MemoryStream zu nehmen und den relevanten Teil des FileStreams einfach komplett da rein zu kopieren. Allerdings können die Audio-Files seeehr groß werden > 1GB und wesentlich effizienter wäre es, einen StreamWrapper zu schreiben, der das Lesen auf byte 343 bis 8838492 einschränkt, ohne die komplette Datei in den Speicher zu kopieren:


var s = file.OpenRead();
var limited = new StreamLimiter(s, 343, 8838492);
var hashBytes = BitConverter.GetBytes(xxHash64.ComputeHash(limited));

Gibt es da schon was fertiges?
Falls nicht, wie würde ich das am besten machen?

T
2.221 Beiträge seit 2008
vor einem Jahr

Wenn du mit Stream arbeitest, kannst du auch die Position setzen.
Es wäre auch sinnvoll, wenn du die Daten blockweise liest und dann hasht, dann musst du nicht die ganze Datei lesen.
So macht man es auch mit den .NET Hash Klassen.

Doku zu Position aus Stream:
Stream.Position Eigenschaft (System.IO)

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Wenn du mit Stream arbeitest, kannst du auch die Position setzen.

Die Position ja, die Length leider nicht...

Es wäre auch sinnvoll, wenn du die Daten blockweise liest und dann hasht, dann musst du nicht die ganze Datei lesen.
So macht man es auch mit den .NET Hash Klassen.

Das passiert in der Hash-Funktion selbst schon über einen Buffer, ich gebe nur einen Stream rein, der wird blockweise gelesen und der Hash gebildet. Problem ist aber, dass der Hash über den GANZEN Stream gebildet wird, allerdings soll er nur über einen Teil gebildet werden.
Setze ich die Position auf 343, überspringt er den Header, allerdings stoppt er nicht beim Trailer und nimmt den mit rein...

Ich brauche also wirklich einen Wrapper, um die Library nutzen zu können.

T
2.221 Beiträge seit 2008
vor einem Jahr

Ich verstehe das Problem nicht ganz.
Du kannst doch einfach über Length - Position die Länge ausrechnen.
Damit kannst du dann die Leselänge einfach berechnen und einschränken.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Ich habe eine Library mit einer API zum Berechnen eines Hash-Werts, dass einen Stream entgegen nimmt, sonst nix (kein offset, kein limit).


var hash = xxHash64.ComputeHash(s)

Diese nimmt den Stream und berechnet für den gesamten Bytestrom den Hash. Ich habe aber nun einen Stream, wo ich nur für einen Teil des Bytestroms den Hash berechnet haben möchte. Leider kann ich bei der Library nicht sagen: Hör nach Length X auf. Die Position kann ich zwar setzen, aber dann nimmt er ab dieser Position den Rest des Bytestroms und errechnet den Hash, statt nach der Länge X aufzuhören.

Genauer:

  • Ich habe eine Datei mit 500 bytes
  • Ich möchte den Hash von byte 50 bis 450 errechnen, da die ersten 50 bytes und die letzten "veränderliche" daten enthalten, die nicht zum hash gehören sollen.
  • Setze ich beim Filestream die Position auf 50, rechnet er von 50 - 500 den Hash aus (was unerwünscht ist, da die letzten 50 bytes NICHT zum hash gehören sollen)

Leider gibt das Interface der Library das nicht her. Sprich:

  • Variante 1: Entweder kopiere ich den Teil, den ich hashen möchte, in einen neuen Stream, z.B. MemoryStream (Speicherintensiv, da große Dateien möglich sind)
  • Variante 2 (bevorzugt): Oder ich Wrappe den Stream, so das er beim Lesen (Read) nicht erst bei 500 aufhört, sondern schon bei 450 (das wäre meine Bevorzugte variante)

Variante 1:


var partialStream = new MemoryStream();
copyStream(s, partialStream, 50, 450); // eigene Methode, die einen Teil des Streams in einen neuen kopiert

// das hier geht schon, er errechnet den hash nur von 50 - 450, ist aber bei großen Dateien sehr speicherintensiv, da fast die gesamte Datei in den speicher kopiert werden muss
var hash = xxHash64.ComputeHash(partialStream);

Variante 2:


// diese Klasse StreamLimiter gibt es noch nicht, sondern die müsste ich entwickeln (oder eine Library nutzen). StreamLimiter extended dabei Stream, schreibt aber die Position, Length und Read Methodik so um, dass der FileStream immernoch 500 bytes hat, dieser aber nur von 50 - 450 gelesen werden kann. 
// Ich simuliere quasi eine Datei, die keinen Header und Trailer hat OHNE die Datei komplett in den Speicher zu laden.
var wrappedStream = new StreamLimiter(s, 50, 450);
var hash = xxHash64.ComputeHash(wrappedStream);

Das hier macht glaube ich sowas, allerdings nur für MemoryMappedFiles, nicht für Streams oder echte Dateien.
MemoryMappedFile Klasse (System.IO.MemoryMappedFiles)

T
2.221 Beiträge seit 2008
vor einem Jahr

Klingt dann eher nach einer Ableitung von Stream als einen Wrapper.
Diese müsste dann intern bei Read ab der festen Position anfangen und bei der berechneten Länge abbrechen.
Dadurch müsstest du auch nicht deinen Code anpassen.

Anbei habe ich mal den Code überflogen.
Aktuell sind alle Klassen und Methoden static.
Deine static readonly Kosntanten solltest du mit const ausweisen, dafür ist es bei Kosntanten da.

Es gibt gerade durch static in deinem Code keine Interface oder abstrakte Klassen.
Dadurch müsstest du jede allgemeine Änderung auch händisch wieder mit statischen Methoden in alle Hash Klassen einbauen.
Hier solltest du dir per abstrakter Klasse und sogar per Interface eine gemeinsame Grundlage aufbauen.
Die Konkreten Implementierungen müssen nicht mal static sein.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Ich glaube wir haben uns gewaltig missverstanden... die Library, die ich nutzen möchte ist NICHT von mir geschrieben. Daher kann ich an dem Interface der Hash-Funktion auch nichts ändern. Ist also nicht mein Code...

Den Filestream habe ich bereits - durch die Library System.Abstractions.IO, daran kann ich also auch erstmal nix ändern. Natürlich könnte ich die FileSystem-Abstraktionen weglassen und einen eigenen Filestream schreiben, aber ein StreamWrapper schien mir die beste und vor allem am Wenigsten aufwändige Option. Ein Beispiel:


public class StreamLimiter: Stream
{
    private readonly Stream _innerStream;
    private readonly long _offset;
    private readonly long _limit;

    public StreamLimiter(Stream innerStream, long offset, long limit=long.MaxValue)
    {
        _offset = offset;
        _limit = limit;
        innerStream.Position = offset;
        innerStream.SetLength(offset + limit);
        _innerStream = innerStream;
    }

    public override void Flush() => _innerStream.Flush();

    public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);

    public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
    public override void SetLength(long value) => _innerStream.SetLength(Math.Min(value, _limit));

    public override void Write(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }

    public override bool CanRead { get; }
    public override bool CanSeek { get; }
    public override bool CanWrite { get; }
    public override long Length { get; }
    public override long Position { get; set; }
}

Ich versuche es aber mal mit der


stream.SetLength(450)

Methode. Ich hatte nicht gesehen, das es die gibt und gedacht, Length wäre readonly. Wenns damit geht, ist mein Problem gelöst.

S
248 Beiträge seit 2008
vor einem Jahr

Hallo sandreas,

die Wrapper Klasse wäre vermutlich der einfachste Weg.
Jedoch solltest du nicht SetLength auf dem gewrappten (File)Stream aufrufen. Dadurch verkleinerst deine gewrappte Datei (schneidest Daten am Ende ab), was vermutlich nicht gewollt ist.

Grüße
spooky

6.911 Beiträge seit 2009
vor einem Jahr

Hallo sandreas,

ganz verstehe ich das Problem nicht bzw. warum soviel drumherum diskutiert wird.
Schreib einen Stream-Wrapper, dessen Ctor Offset und Länge auch übergeben wird, und das Problem ist gelöst.

Per Definition der Stream.Read-APIs wird maximal count Bytes gelesen, d.h. es kann weniger sein. Falls nichts mehr zu lesen ist, so wird 0 zurückgegeben.
Damit lässt sich das Problem fast trivial lösen.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

die Wrapper Klasse wäre vermutlich der einfachste Weg.
Jedoch solltest du nicht SetLength auf dem gewrappten (File)Stream aufrufen. Dadurch verkleinerst deine gewrappte Datei (schneidest Daten am Ende ab), was vermutlich nicht gewollt ist.

Ja, das mit dem "verkleinern" ist mir klar - spielt in meinem Fall aber keine Rolle, da der Stream "readonly" ist und nur innerhalb einer Methode genutzt wird - ist also gekapselt. Zur not könnte ich den StreamLimiter.Dispose überschreiben, dass er die Länge des "innerStream" wieder zurück auf den Originalwert setzt.

ganz verstehe ich das Problem nicht bzw. warum soviel drumherum diskutiert wird.
Schreib einen Stream-Wrapper, dessen Ctor Offset und Länge auch übergeben wird, und das Problem ist gelöst.

Per Definition der Stream.Read-APIs wird maximal count Bytes gelesen, d.h. es kann weniger sein. Falls nichts mehr zu lesen ist, so wird 0 zurückgegeben.
Damit lässt sich das Problem fast trivial lösen.

Ja, ich dachte, das gibt es schon fertig bei nuget (irgendeine StreamUtils lib). Man kann da bestimmt wieder einiges falsch machen, wenn man die interna von Stream nicht genau kennt.

Ich denke, ich habs jetzt gelöst. Ausgiebige Tests stehen noch aus. Vielen Dank für die ganzen Kommentare.

6.911 Beiträge seit 2009
vor einem Jahr

Hallo sandreas,

Man kann da bestimmt wieder einiges falsch machen, wenn man die interna von Stream nicht genau kennt.

Warum? Es ist hier doch nur einfache Addition (wieviel schon gelesen wurde) und ein Vergleich mit der Soll-Länge?! KISS -- vermutlich hast du dich gedanklich in einem zu komplizierten Weg verrannt.

Die Interna von Stream (der Basisklasse) sind nicht sehr schwierig, v.a. bei Read. Man übergibt einen Buffer (optional wenns ein byte[] ist auch Offset und Count) und teilt somit mit, wieviel max. gelesen werden soll. Als Rückgabewert kommen die Anzahl der gelesenen Bytes. Mehr ist es nicht -- das macht das Konzept vom Stream so universal einsetzbar (sowas finde ich ganz einfach super).

Ob der tatsächliche Stream ein FileStream, NetworkStream, etc. ist spielt dabei keine Rolle. Für die Verwendung vom Stream braucht man auch nicht* wissen wie NetworkStream unter der Haube funktioniert, da in .NET das Liskovsches Substitutionsprinzip befolgt wird.

* es schadet aber nicht 😉

gibt es schon fertig bei nuget

Kann sein. Aber bei "trivial Aufgaben" ist der Aufwand bei NuGet oft höhere, da* Suchen ob es etwas gibt

  • wenns mehrere gibt evaluieren was am besten passt
  • mind. ReadMe lesen / Doku lesen

Zusätzlich hier noch das Thema im Forum -- in dieser Zeit wäre die Lösung inkl. Tests wohl schon fertig 😉

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

So, für alle, die es mal brauchen - hier ist ein netter Beitrag auf StackOverflow: how-to-set-length-in-stream-without-truncated

Ich war so frei und habe selbst eine "verbesserte" Version davon geschrieben - muss diese aber noch auf Herz und Nieren testen. Bisher wirkt es ganz gut und funktioniert, allerdings könnten da durchaus noch off-by-one errors drin sein.


// inspired by https://stackoverflow.com/questions/33354822/how-to-set-length-in-stream-without-truncated
public class StreamLimiter: Stream
{
    private readonly Stream _innerStream;
    private readonly long _limit;
    public StreamLimiter(Stream input, long offset, long length)
    {
        _innerStream = input;
        _innerStream.Position = ClampPosition(_innerStream, offset);
        _limit = ClampPosition(_innerStream, _innerStream.Position + length);
    }

    private static long ClampPosition(Stream input, long offset) => Math.Min(Math.Max(offset, 0), input.Length);

    public override bool CanRead => _innerStream.CanRead;
    public override bool CanSeek => _innerStream.CanSeek;
    public override bool CanWrite => false;
    public override void Flush() => _innerStream.Flush();
    public override long Length => _limit;

    public override long Position
    {
        get => _innerStream.Position;
        set => _innerStream.Position = value; // todo: clamp position to limited length?
    }

    public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, ClampCount(count));

    public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);

    public override void SetLength(long value) => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

    public override bool CanTimeout => _innerStream.CanTimeout;

    public override int ReadTimeout
    {
        get => _innerStream.ReadTimeout;
        set => _innerStream.ReadTimeout = value;
    }

    public override int WriteTimeout
    {
        get => _innerStream.ReadTimeout;
        set => _innerStream.ReadTimeout = value;
    }

    public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => _innerStream.BeginRead(buffer, offset, ClampCount(count), callback, state);

    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw new NotSupportedException();

    // do not close the inner stream
    public override void Close() { }

    public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);

    public override int EndRead(IAsyncResult asyncResult) => _innerStream.EndRead(asyncResult);

    public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.ReadAsync(buffer, offset, ClampCount(count), cancellationToken);

    public override int ReadByte() => ClampCount(1) == 0 ? -1 : _innerStream.ReadByte();

    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException();

    public override void WriteByte(byte value) => throw new NotSupportedException();

    private int ClampCount(int count) => (int)Math.Min(count, ClampPosition(_innerStream, _limit - _innerStream.Position));
}

6.911 Beiträge seit 2009
vor einem Jahr

Hallo sandreas,

ich finde die StreamLimiter Klasse ist weder Teig noch Mehl. Irgendwie wird der _innerStream beschränkt, dann aber doch nicht so ganz konsequent.
Z.B. Position gibt diese vom _innerStream an.

So einen Typen würde ich eher SubReadStream nennen, der eine "View" über den _innerStream legt, dabei zu Beginn von Außen Offset := 0 und Länge := das Limit ist.
Intern kann das ja auf den _innerStream umgerechnet werden. Z.B. beim Setzen der Position halt den Offset berücksichtigen.

Beim Lesen, egal ob die byte[] od. Span<byte>-Überladung, ist count bzw. der buffer (bei Span-Überladung) entsprechend zu verkleinern, falls über das Limit hinaus gelesen würde.

Für den Verwender sollte dieser Stream jedenfalls nur den nutzbaren Teil (~ "View") widerspiegeln.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

16.825 Beiträge seit 2008
vor einem Jahr

Die Anforderung

Jetzt weiß ich, dass die Audio-Daten z.B. nur von Position=343 bis Position=8838492 (stream.Length - 889) gehen, die Datei also einen Metadaten-Header und Trailer hat, der mich für den Hash nicht interessiert.

ist für mich eigentlich ein klarer Ausdruck von Abstraktion, sodass ein Wrapper das eigentliche Mittel wäre und keine Vererbung.
Ansonsten sehe ich das wie Gü, so ist das weder Teig noch Mehl.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

... ich finde die StreamLimiter Klasse ist weder Teig noch Mehl. Irgendwie wird der _innerStream beschränkt, dann aber doch nicht so ganz konsequent.
Z.B. Position gibt diese vom _innerStream an.

So einen Typen würde ich eher SubReadStream nennen, der eine "View" über den _innerStream legt, dabei zu Beginn von Außen Offset := 0 und Länge := das Limit ist.
Intern kann das ja auf den _innerStream umgerechnet werden. Z.B. beim Setzen der Position halt den Offset berücksichtigen.

Beim Lesen, egal ob die byte[] od. Span<byte>-Überladung, ist count bzw. der buffer (bei Span-Überladung) entsprechend zu verkleinern, falls über das Limit hinaus gelesen würde.

Für den Verwender sollte dieser Stream jedenfalls nur den nutzbaren Teil (~ "View") widerspiegeln...

Vielen Dank für dein Feedback. Sehe ich tatsächlich ähnlich. Aber interessant, wie man anhand eines Code-Beispiels wieder an den Kern der ursprünglichen Frage zurück kommt.
Natürlich ist das wrappen des Streams hier nicht optimal gelöst, ich denke, ich muss noch ALLE methoden, die den Ursprungs-Stream benutzen und über das festgelegte offset und limit hinausgehen, "clampen", also die gewünschte Position auf den Bereich limitieren.

...sodass ein Wrapper das eigentliche Mittel wäre und keine Vererbung...

Absolut richtig. Leider ist in C# Stream kein Interface, sondern eine Klasse. Die Library verlangt ausdrücklich einen Stream, also kann der Wrapper nur implementiert werden, in dem er auch vom Stream erbt. Schön ist das nicht, aber meines Wissens nach die einzige Möglichkeit, um die Library wie sie ist zu nutzen.

Ich wollte noch kurz Bescheid geben, dass die Klasse bisher tut, was sie soll (ob der Schwächen, die sie hat) und effizient genug ist, um auch große Dateien zu Hashen. Folglich ist mein Problem erstmal gelöst, vielen Dank.

Falls noch jemand einen saubereren Implementierungsvorschlag hat, bin ich gespannt auf die Code-Änderung bzw. die jeweilige Implementierung.

Vielen Dank für eure Hilfe.

S
248 Beiträge seit 2008
vor einem Jahr

Natürlich ist das wrappen des Streams hier nicht optimal gelöst, ich denke, ich muss noch ALLE methoden, die den Ursprungs-Stream benutzen und über das festgelegte offset und limit hinausgehen, "clampen", also die gewünschte Position auf den Bereich limitieren.

Ich denke du schießt gewaltig über das Ziel mit der Implementierung hinaus.
Das Einzige was du überschreiben musst sind die abstrakten Members von Stream - alle anderen kannst du genau so lassen wie sie sind. Ganz besonders macht es keinen Sinn Member zu überschreiben und dann den Basis-Member wieder eins zu eins aufzurufen...

.. der eine "View" über den _innerStream legt, dabei zu Beginn von Außen Offset := 0 und Länge := das Limit ist.
Intern kann das ja auf den _innerStream umgerechnet werden. Z.B. beim Setzen der Position halt den Offset berücksichtigen.

Wie schon gesagt wurde ... du muss lediglich Position, Length, Read und Seek so anpassen dass nur der gewünschte Bereich von außen sicht- und zugreifbar ist und der Rest verhält sich dann wie gewünscht.
Hier ein grober Ansatz, den du ausbauen kannst (Parameterprüfung von ctor, Position etc.):


public class StreamLimiter : Stream
{
    private readonly Stream _stream;
    private readonly long _offset;
    private readonly long _length;

    public StreamLimiter(Stream stream, long offset, long length)
    {
        _stream = stream;
        _offset = offset;
        _length = length;

        Position = 0;
    }

    public override bool CanRead => true;
    public override bool CanSeek => true;
    public override bool CanWrite => false;
    public override long Length => _length;

    public override long Position
    {
        get { return _stream.Position - _offset; }
        set { _stream.Position = _offset + value; }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        long remaining = Length - Position;
        if (remaining == 0)
            return 0;

        count = (int)Math.Min(count, remaining);
        return _stream.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        long position = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => Position + offset,
            SeekOrigin.End => Length + offset,
            _ => throw new ArgumentException(nameof(origin))
        };

        if (position < 0 || position > Length)
            throw new ArgumentException(nameof(offset));

        Position = position;
        return position;
    }

    public override void Flush() => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

Grüße
spooky

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Ich denke du schießt gewaltig über das Ziel mit der Implementierung hinaus.
Das Einzige was du überschreiben musst sind die abstrakten Members von Stream - alle anderen kannst du genau so lassen wie sie sind. Ganz besonders macht es keinen Sinn Member zu überschreiben und dann den Basis-Member wieder eins zu eins aufzurufen...

Cool danke für das Beispiel. Ich werde da jedenfalls noch mal etwas dran verschlanken und alles wegwerfen, was ich nicht brauche. Sehr schön sauberer Code.