I just spent some time putting together a simple protocol and implementation for packetizing byte streams (on GitHub here). The project is aimed at embedded systems that need a very lightweight way to pass discrete messages across serial lines. Since I've never had any of my C reviewed before, would you mind taking a look at it and seeing if there's anything that could be improved in the style, design, or best practices?
s3p.h:
#ifndef S3P_H
#define S3P_H
#include <stdint.h>
#define S3P_OVERHEAD 3
typedef enum {
S3P_SUCCESS = 0,
S3P_BUF_TOO_SMALL = 1,
S3P_PAYLOAD_TOO_LARGE = 2,
S3P_CHECKSUM_ERR = 3,
S3P_PARSE_FAILURE = 4,
} S3P_ERR;
/**
Build a new packet from the bytes in "data" into "out".
S3P packets are limited to 256 bytes of data; attempting to packetize more
will result in a S3P_PAYLOAD_TOO_LARGE error. "out" must be at least
S3P_OVERHEAD bytes longer than the data that needs to be packetized; and this
can increase to twofold depending on the number of bytes that need
escaping. In general, unless memory usage is a concern, allocate twice as
much space as the data you wish to packetize.
Params:
data: a byte array of data to be packetized.
dsize: the length of "data".
out: a byte array which will hold the built packet.
osize: the length of "out".
psize: a pointer to an int which will contain the length of the
built packet.
*/
S3P_ERR s3p_build(uint8_t const *data, int dsize, uint8_t *out, int osize,
int *psize);
/**
Read a packet from "in", and place unescaped data into "data".
Breaking streams into packets is the responsibility of the
caller. This function will only read the first packet in the
"in" buffer; all other data will be ignored. This function guarantees that
the length of the data retrieved from the packet will be strictly less than
the size of the input packet.
Params:
in: byte array of raw, packetized data.
isize: the length of "in".
data: byte array which will contain the retrieved data.
dsize: the length of "data".
psize: pointer to an int which will contain the length of the
retrieved data.
*/
S3P_ERR s3p_read(uint8_t const *in, int isize, uint8_t *data, int dsize,
int *psize);
#endif
s3p.c:
#include <s3p.h>
enum S3P_CONTROL_CHARS {
S3P_START = 0x56, // Marks the start of a packet
S3P_ESCAPE = 0x25, // Marks escaped bytes
S3P_MASK = 0x20, // Mask used for escaping chars. echar = char ^ S3P_MASK
};
S3P_ERR s3p_build(uint8_t const *data, int dsize, uint8_t *out, int osize,
int *psize){
if((dsize + S3P_OVERHEAD) > osize){
return S3P_BUF_TOO_SMALL;
}
if(255 < dsize){
return S3P_PAYLOAD_TOO_LARGE;
}
/*
S3P packets follow this structure:
START | LENGTH (N) | DATA 1 | DATA 2 | ... | DATA N | CHECKSUM
*/
out[0] = S3P_START;
int dnext = 2; // Next data byte position in output buffer
/*
If the length of the data (dsize, in this case) is S3P_START or S3P_ESCAPE
we need to escape it.
*/
if(S3P_START == dsize || S3P_ESCAPE == dsize){
out[1] = S3P_ESCAPE;
out[2] = ((uint8_t) dsize) ^ S3P_MASK;
dnext = 3;
} else {
out[1] = (uint8_t) dsize;
}
uint8_t check = 0; // Checksum
int i;
for(i=0; i<dsize; i++){
// Check the size restrictions: dnext should contain the number of bytes
// written so far less one (as the indices start at zero), and we need to
// add a checksum byte after all is said and done.
if(dnext + 2 > osize){
return S3P_BUF_TOO_SMALL;
}
uint8_t dbyte = data[i];
check += dbyte;
if(S3P_START == dbyte || S3P_ESCAPE == dbyte){
out[dnext] = S3P_ESCAPE;
dnext++;
out[dnext] = dbyte ^ S3P_MASK;
} else {
out[dnext] = dbyte;
}
dnext++;
}
out[dnext] = check;
dnext++;
*psize = dnext;
return S3P_SUCCESS;
}
S3P_ERR s3p_read(uint8_t const *in, int isize, uint8_t *data, int dsize,
int *psize){
if(dsize < 1){
return S3P_BUF_TOO_SMALL;
}
if(isize < S3P_OVERHEAD){
return S3P_PARSE_FAILURE;
}
// The first byte should be a start byte. If not, this is not an S3P packet
// and we should not try to parse it.
if(S3P_START != in[0]){
return S3P_PARSE_FAILURE;
}
int dnext = 2;
int length;
if(S3P_ESCAPE == in[1]){
length = in[2] ^ S3P_MASK;
dnext++;
} else {
length = in[1];
}
int dread = 0; // Number of data bytes read
uint8_t check = 0; // Checksum
while(dread < length){
if(dread >= dsize){
return S3P_BUF_TOO_SMALL;
}
if(dnext >= isize){
return S3P_PARSE_FAILURE;
}
uint8_t dbyte = in[dnext];
if(S3P_START == dbyte){
return S3P_PARSE_FAILURE;
}
if(S3P_ESCAPE == dbyte){
dnext++;
dbyte = in[dnext];
if(S3P_START == dbyte || S3P_ESCAPE == dbyte){
return S3P_PARSE_FAILURE;
}
dbyte = dbyte ^ S3P_MASK;
}
data[dread] = dbyte;
check += dbyte;
dnext++;
dread++;
}
*psize = dread;
uint8_t pcheck;
if(S3P_ESCAPE == in[dnext]){
dnext++;
pcheck = in[dnext] ^ S3P_MASK;
} else {
pcheck = in[dnext];
}
if(pcheck != check){
return S3P_CHECKSUM_ERR;
}
return S3P_SUCCESS;
}
Update: I've incorporated some changes suggested by William Morris, such as adding end markers and changing size parameters to unsigned values (size_t). This simplified the code a bit. The latest version is up on GitHub now.