Best way to find position in the Stream where given byte sequence starts

15,383

Solution 1

I've reached this solution.

I did some benchmarks with an ASCII file that was 3.050 KB and 38803 lines. With a search byte array of 22 bytes in the last line of the file I've got the result in about 2.28 seconds (in a slow/old machine).

public static long FindPosition(Stream stream, byte[] byteSequence)
{
    if (byteSequence.Length > stream.Length)
        return -1;

    byte[] buffer = new byte[byteSequence.Length];

    using (BufferedStream bufStream = new BufferedStream(stream, byteSequence.Length))
    {
        int i;
        while ((i = bufStream.Read(buffer, 0, byteSequence.Length)) == byteSequence.Length)
        {
            if (byteSequence.SequenceEqual(buffer))
                return bufStream.Position - byteSequence.Length;
            else
                bufStream.Position -= byteSequence.Length - PadLeftSequence(buffer, byteSequence);
        }
    }

    return -1;
}

private static int PadLeftSequence(byte[] bytes, byte[] seqBytes)
{
    int i = 1;
    while (i < bytes.Length)
    {
        int n = bytes.Length - i;
        byte[] aux1 = new byte[n];
        byte[] aux2 = new byte[n];
        Array.Copy(bytes, i, aux1, 0, n);
        Array.Copy(seqBytes, aux2, n);
        if (aux1.SequenceEqual(aux2))
            return i;
        i++;
    }
    return i;
}

Solution 2

If you treat the stream like another sequence of bytes, you can just search it like you were doing a string search. Wikipedia has a great article on that. Boyer-Moore is a good and simple algorithm for this.

Here's a quick hack I put together in Java. It works and it's pretty close if not Boyer-Moore. Hope it helps ;)

public static final int BUFFER_SIZE = 32;

public static int [] buildShiftArray(byte [] byteSequence){
    int [] shifts = new int[byteSequence.length];
    int [] ret;
    int shiftCount = 0;
    byte end = byteSequence[byteSequence.length-1];
    int index = byteSequence.length-1;
    int shift = 1;

    while(--index >= 0){
        if(byteSequence[index] == end){
            shifts[shiftCount++] = shift;
            shift = 1;
        } else {
            shift++;
        }
    }
    ret = new int[shiftCount];
    for(int i = 0;i < shiftCount;i++){
        ret[i] = shifts[i];
    }
    return ret;
}

public static byte [] flushBuffer(byte [] buffer, int keepSize){
    byte [] newBuffer = new byte[buffer.length];
    for(int i = 0;i < keepSize;i++){
        newBuffer[i] = buffer[buffer.length - keepSize + i];
    }
    return newBuffer;
}

public static int findBytes(byte [] haystack, int haystackSize, byte [] needle, int [] shiftArray){
    int index = needle.length;
    int searchIndex, needleIndex, currentShiftIndex = 0, shift;
    boolean shiftFlag = false;

    index = needle.length;
    while(true){
        needleIndex = needle.length-1;
        while(true){
            if(index >= haystackSize)
                return -1;
            if(haystack[index] == needle[needleIndex])
                break;
            index++;
        }
        searchIndex = index;
        needleIndex = needle.length-1;
        while(needleIndex >= 0 && haystack[searchIndex] == needle[needleIndex]){
            searchIndex--;
            needleIndex--;
        }
        if(needleIndex < 0)
            return index-needle.length+1;
        if(shiftFlag){
            shiftFlag = false;
            index += shiftArray[0];
            currentShiftIndex = 1;
        } else if(currentShiftIndex >= shiftArray.length){
            shiftFlag = true;
            index++;
        } else{
            index += shiftArray[currentShiftIndex++];
        }           
    }
}

public static int findBytes(InputStream stream, byte [] needle){
    byte [] buffer = new byte[BUFFER_SIZE];
    int [] shiftArray = buildShiftArray(needle);
    int bufferSize, initBufferSize;
    int offset = 0, init = needle.length;
    int val;

    try{
        while(true){
            bufferSize = stream.read(buffer, needle.length-init, buffer.length-needle.length+init);
            if(bufferSize == -1)
                return -1;
            if((val = findBytes(buffer, bufferSize+needle.length-init, needle, shiftArray)) != -1)
                return val+offset;
            buffer = flushBuffer(buffer, needle.length);
            offset += bufferSize-init;
            init = 0;
        }
    } catch (IOException e){
        e.printStackTrace();
    }
    return -1;
}

Solution 3

I needed to do this myself, had already started, and didn't like the solutions above. I specifically needed to find where the search-byte-sequence ends. In my situation, I need to fast-forward the stream until after that byte sequence. But you can use my solution for this question too:

var afterSequence = stream.ScanUntilFound(byteSequence);
var beforeSequence = afterSequence - byteSequence.Length;

Here is StreamExtensions.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace System
{

    static class StreamExtensions
    {
        /// <summary>
        /// Advances the supplied stream until the given searchBytes are found, without advancing too far (consuming any bytes from the stream after the searchBytes are found).
        /// Regarding efficiency, if the stream is network or file, then MEMORY/CPU optimisations will be of little consequence here.
        /// </summary>
        /// <param name="stream">The stream to search in</param>
        /// <param name="searchBytes">The byte sequence to search for</param>
        /// <returns></returns>
        public static int ScanUntilFound(this Stream stream, byte[] searchBytes)
        {
            // For this class code comments, a common example is assumed:
            // searchBytes are {1,2,3,4} or 1234 for short
            // # means value that is outside of search byte sequence

            byte[] streamBuffer = new byte[searchBytes.Length];
            int nextRead = searchBytes.Length;
            int totalScannedBytes = 0;

            while (true)
            {
                FillBuffer(stream, streamBuffer, nextRead);
                totalScannedBytes += nextRead; //this is only used for final reporting of where it was found in the stream

                if (ArraysMatch(searchBytes, streamBuffer, 0))
                    return totalScannedBytes; //found it

                nextRead = FindPartialMatch(searchBytes, streamBuffer);
            }
        }

        /// <summary>
        /// Check all offsets, for partial match. 
        /// </summary>
        /// <param name="searchBytes"></param>
        /// <param name="streamBuffer"></param>
        /// <returns>The amount of bytes which need to be read in, next round</returns>
        static int FindPartialMatch(byte[] searchBytes, byte[] streamBuffer)
        {
            // 1234 = 0 - found it. this special case is already catered directly in ScanUntilFound            
            // #123 = 1 - partially matched, only missing 1 value
            // ##12 = 2 - partially matched, only missing 2 values
            // ###1 = 3 - partially matched, only missing 3 values
            // #### = 4 - not matched at all

            for (int i = 1; i < searchBytes.Length; i++)
            {
                if (ArraysMatch(searchBytes, streamBuffer, i))
                {
                    // EG. Searching for 1234, have #123 in the streamBuffer, and [i] is 1
                    // Output: 123#, where # will be read using FillBuffer next. 
                    Array.Copy(streamBuffer, i, streamBuffer, 0, searchBytes.Length - i);
                    return i; //if an offset of [i], makes a match then only [i] bytes need to be read from the stream to check if there's a match
                }
            }

            return 4;
        }

        /// <summary>
        /// Reads bytes from the stream, making sure the requested amount of bytes are read (streams don't always fulfill the full request first time)
        /// </summary>
        /// <param name="stream">The stream to read from</param>
        /// <param name="streamBuffer">The buffer to read into</param>
        /// <param name="bytesNeeded">How many bytes are needed. If less than the full size of the buffer, it fills the tail end of the streamBuffer</param>
        static void FillBuffer(Stream stream, byte[] streamBuffer, int bytesNeeded)
        {
            // EG1. [123#] - bytesNeeded is 1, when the streamBuffer contains first three matching values, but now we need to read in the next value at the end 
            // EG2. [####] - bytesNeeded is 4

            var bytesAlreadyRead = streamBuffer.Length - bytesNeeded; //invert
            while (bytesAlreadyRead < streamBuffer.Length)
            {
                bytesAlreadyRead += stream.Read(streamBuffer, bytesAlreadyRead, streamBuffer.Length - bytesAlreadyRead);
            }
        }

        /// <summary>
        /// Checks if arrays match exactly, or with offset. 
        /// </summary>
        /// <param name="searchBytes">Bytes to search for. Eg. [1234]</param>
        /// <param name="streamBuffer">Buffer to match in. Eg. [#123] </param>
        /// <param name="startAt">When this is zero, all bytes are checked. Eg. If this value 1, and it matches, this means the next byte in the stream to read may mean a match</param>
        /// <returns></returns>
        static bool ArraysMatch(byte[] searchBytes, byte[] streamBuffer, int startAt)
        {
            for (int i = 0; i < searchBytes.Length - startAt; i++)
            {
                if (searchBytes[i] != streamBuffer[i + startAt])
                    return false;
            }
            return true;
        }
    }
}

Solution 4

You'll basically need to keep a buffer the same size as byteSequence so that once you've found that the "next byte" in the stream matches, you can check the rest but then still go back to the "next but one" byte if it's not an actual match.

It's likely to be a bit fiddly whatever you do, to be honest :(

Share:
15,383

Related videos on Youtube

sh0gged
Author by

sh0gged

Updated on June 04, 2021

Comments

  • sh0gged
    sh0gged almost 3 years

    How do you think what is the best way to find position in the System.Stream where given byte sequence starts (first occurence):

    public static long FindPosition(Stream stream, byte[] byteSequence)
    {
        long position = -1;
    
        /// ???
        return position;
    }
    

    P.S. The simpliest yet fastest solution is preffered. :)

    • dharga
      dharga over 14 years
      your question is confusing...what are you looking for? that specific sequence of bytes in the stream?
    • Joseph Shih
      Joseph Shih over 14 years
      I think the question's title should be updated. Stream is misspelled as Steam, which makes it seem like a question that should be tagged Valve.
    • Powerlord
      Powerlord over 14 years
      @chollida: Actually, I came to this question just to fix that.
    • sh0gged
      sh0gged over 14 years
      Actually i'm looking for guid in the stream.
    • dharga
      dharga over 14 years
      is memory an issue? or can you read the whole stream into an array of bytes?
    • bruno conde
      bruno conde over 14 years
      Please check if my solution fits your needs.
  • dharga
    dharga over 14 years
    it might not be simplest, but it's pretty fast. it think given the constraints of reading from a stream doesn't allow for simple if you want speed. but I hope my code can alleviate some of your troubles, or be of help to someone in the future.
  • Tullo_x86
    Tullo_x86 almost 12 years
    For future reference, PadLeftSequence is searching for the first non-matching byte which caused SequenceEqual to return false. It seems like a micro-optimisation to me, since one would expect SequenceEqual to early-return on a non-match anyway. Disclaimer: I have not done any measurements, this is only opinion.
  • imaximchuk
    imaximchuk almost 11 years
    your code is incorrect. consider haystack = [ 2,1,2,1,1 ], needle = [ 2,1,1 ]. Your code returns -1, but correct answer is 2
  • YoniXw
    YoniXw over 7 years
    doesnt it only work if the sequence is at index of a length multipication? I mean, 6 bytes seq at index 4 will not be found ?
  • Borislav Ivanov
    Borislav Ivanov over 5 years
    It seems initBufferSize variable in findBytes is not used.
  • alamoot
    alamoot almost 3 years
    Welcome to Stack Overflow. Try to avoid code only answer, and give some explanation of your code.
  • Jonas Kohl
    Jonas Kohl about 2 years
    Heads up: This solution seems to be in Java, whereas OP asked for C#