So let's say you have a protocol where packets are of arbitrary size, but they have some patterns by which they are indentifiable. For some identification techniques, see below, first the reading process.
Interrupt: It just loads bytes into a circular buffer, incrementing the write pointer (and pushing forth the read pointer when the buffer is full, so it drops bytes in a somewhat clean manner).
Main program: Polls the buffer. It starts at the current read pointer and seeks whether from that a packet can be identified. If nothing is found, it tries to seek from read pointer + 1, and so on until covering the buffer. The read pointer is left unchanged (this is important). If a packet could be identified, it processes it, and (only then) sets the read pointer at the packet's end, thus discarding its contents from the circular buffer.
Why leaving the read pointer unchanged is important when no packet could be identified? Since as further bytes trickle in, a larger packet might complete later.
When the buffer is full with junk, thus the interrupt is pushing forth the read pointer, you might experience hazards depending on how cautiously you designed the interaction between the IT and the main thread. Usually with any reasonably sane implementation, there shouldn't be too many problems here. The push forth behavior in the IT is more consistent than simply passing the read pointer by the write pointer (thus indirectly discarding the entire buffer).
Usually this approach works fine for just about anything. Once I had a situation where I had packet types which contained a smaller otherwise correct packet for the protocol. Now that was nasty to fix, to get the outer packet these cases! (The above algorithm will fetch the inner packet since it likely arrives sooner)
So what about identification?
If you have any identifiable header, a synchronization byte or whatever, it is easier to get the beginning (you can bail off fast if from the given start offset in the buffer it doesn't look like a proper header).
You may need to do a partial processing in the identification (in the main program of course) to figure out the length of the packet. The simplest if there is any direct length indication, but without it, if there is a packet type identification (which implies a length, this seems to be your case) it is nothing much harder.
If there is any checksum or CRC, it is also a good thing to detect the packet. Use it as part of the identification even if a complex header seems to be right: if it's there correct, then you definitely have a valid packet.
If there is no checksum or CRC at all, use whatever fixed bits the packet has, or whatever fixed ranges the protocol imposes on certain values within the packet to see whether they match a valid packet.
Using these principles I was able to get around quite wrecked protocols proper, I mean things like which assumed delays between packets with hardly any framing, the delays acting as separators, which ceased to exist after the data travelled through the Internet and / or was fetched by a PC running an operating system (in an operating system environment the interrupt and circular buffer is replaced by the OS' own buffering mechanisms, the principles to detect the packets are the same).