diff --git a/ais_tools/aivdm.py b/ais_tools/aivdm.py index 62b2346..169131a 100644 --- a/ais_tools/aivdm.py +++ b/ais_tools/aivdm.py @@ -8,7 +8,7 @@ from ais_tools.ais import AISMessageTranscoder from ais_tools.nmea import split_multipart from ais_tools.nmea import expand_nmea -from ais_tools.checksum import checksumstr +from ais_tools.core import checksum_str from ais_tools.message import Message @@ -166,7 +166,7 @@ def safe_encode(self, message): def encode(self, message): body, pad = self.encode_payload(message) sentence = "AIVDM,1,1,,A,{},{}".format(body, pad) - return Message("!{}*{}".format(sentence, checksumstr(sentence))) + return Message("!{}*{}".format(sentence, checksum_str(sentence))) def encode_payload(self, message): return self.encoder.encode_payload(message) diff --git a/ais_tools/checksum.c b/ais_tools/checksum.c deleted file mode 100644 index f707296..0000000 --- a/ais_tools/checksum.c +++ /dev/null @@ -1,103 +0,0 @@ -// checksum module - -#define PY_SSIZE_T_CLEAN /* Make "s#" use Py_ssize_t rather than int. */ -#include -#include -#include - -int _checksum(const char *s) -{ - int c = 0; - while (*s) - c = c ^ *s++; - return c; -} - -char* _checksum_str(const char * s, char* checksum) -{ - int c = _checksum(s); - sprintf(checksum, "%02X", c); - return checksum; -} - -bool _is_checksum_valid(char* s) -{ - const char * skip_chars = "!?\\"; - const char * separator = "*"; - - char* body = s; - char* checksum = NULL; - char computed_checksum[3]; - char* lasts = NULL; - - if (*body && strchr(skip_chars, body[0])) - body++; - - body = strtok_r(body, separator, &lasts); - checksum = strtok_r(NULL, separator, &lasts); - if (checksum == NULL || strlen(checksum) != 2) - return false; - - _checksum_str(body, computed_checksum); - return strcasecmp(checksum, computed_checksum) == 0; -} - -static PyObject * -checksum_compute_checksum(PyObject *module, PyObject *args) -{ - const char *str; - int c; - - if (!PyArg_ParseTuple(args, "s", &str)) - return NULL; - c = _checksum(str); - return PyLong_FromLong(c); -} - -static PyObject * -checksum_compute_checksumstr(PyObject *module, PyObject *args) -{ - const char *str; - char c_str[3]; - - if (!PyArg_ParseTuple(args, "s", &str)) - return NULL; - _checksum_str(str, c_str); - return PyUnicode_FromString(c_str); -} - -static PyObject * -checksum_is_checksum_valid(PyObject *module, PyObject *args) -{ - char *str; - - if (!PyArg_ParseTuple(args, "s", &str)) - return NULL; - - return _is_checksum_valid(str) ? Py_True: Py_False; -} - - -static PyMethodDef checksum_methods[] = { - {"checksum", (PyCFunction)(void(*)(void))checksum_compute_checksum, METH_VARARGS, - "Compute checksum of a string. returns an integer value"}, - {"checksumstr", (PyCFunction)(void(*)(void))checksum_compute_checksumstr, METH_VARARGS, - "Compute checksum of a string. returns a 2-character hex string"}, - {"is_checksum_valid", (PyCFunction)(void(*)(void))checksum_is_checksum_valid, METH_VARARGS, - "Returns True if the given string is terminated with a valid checksum, else False"}, - {NULL, NULL, 0, NULL} /* sentinel */ -}; - -static struct PyModuleDef checksum_module = { - PyModuleDef_HEAD_INIT, - "checksum", - NULL, - -1, - checksum_methods -}; - -PyMODINIT_FUNC -PyInit_checksum(void) -{ - return PyModule_Create(&checksum_module); -} \ No newline at end of file diff --git a/ais_tools/core/checksum.c b/ais_tools/core/checksum.c new file mode 100644 index 0000000..25686cf --- /dev/null +++ b/ais_tools/core/checksum.c @@ -0,0 +1,83 @@ +// checksum module + +#include +#include +#include +#include "core.h" +#include "checksum.h" + +/* + * Compute the checksum value of a string. This is + * computed by xor-ing the integer value of each character in the string + * sequentially. + */ +int checksum(const char *s) +{ + int c = 0; + while (*s) + c = c ^ *s++; + return c; +} + +/* + * Get the checksum value of a string as a 2-character hex string + * This is always uppercase. The destination buffer must be at least 3 chars + * (one for the terminating null character) + * + * Returns a pointer to the destination buffer + * Returns null if the dest buffer is too small + */ +char* checksum_str(char * __restrict dst, const char* __restrict src, size_t dsize) +{ + if (dsize < 3) + return NULL; + + int c = checksum(src); + sprintf(dst, "%02X", c); + return dst; +} + + +/* + * Compute the checksum value of the given string and compare it to the checksum + * that appears at the end of the string. + * the checksum should be a 2 character hex value at the end separated by a '*' + * + * For example: + * c:1000,s:old*5A + * + * If the string starts with any of these characrters ?!\ then the first character is ignored + * for purposes of computing the checksum + * + * If no checksum is found at at the end of the string then the return is false + * + * Returns true if the checksum at the end of the string matches the computed checksum, else false + * + * NOTE: The given string will be modified to separate it into the body portion and the checksum portion + */ +bool is_checksum_valid(char* s) +{ + const char * skip_chars = "!?\\"; + const char separator = '*'; + + char* body = s; + char* c_str = NULL; + char computed_checksum[3]; + + if (*body && strchr(skip_chars, body[0])) + body++; + + char* ptr = body; + while (*ptr != '\0' && *ptr != separator) + ptr++; + + if (*ptr == '*') + *ptr++ = '\0'; + c_str = ptr; + + if (c_str == NULL || strlen(c_str) != 2) + return false; + + checksum_str(computed_checksum, body, ARRAY_LENGTH(computed_checksum)); + return strcasecmp(c_str, computed_checksum) == 0; +} diff --git a/ais_tools/core/checksum.h b/ais_tools/core/checksum.h new file mode 100644 index 0000000..00bbbac --- /dev/null +++ b/ais_tools/core/checksum.h @@ -0,0 +1,5 @@ +/* AIS Tools checksum functions */ + +int checksum(const char *s); +char* checksum_str(char * __restrict dst, const char* __restrict src, size_t dsize); +bool is_checksum_valid(char* s); diff --git a/ais_tools/core/core.h b/ais_tools/core/core.h new file mode 100644 index 0000000..25685e7 --- /dev/null +++ b/ais_tools/core/core.h @@ -0,0 +1,37 @@ +/* AIS Tools core functions implemented in C */ + +#define PY_SSIZE_T_CLEAN /* Make "s#" use Py_ssize_t rather than int. */ + +#define SUCCESS 0 +#define FAIL -1 +#define FAIL_STRING_TOO_LONG -101 +#define FAIL_TOO_MANY_FIELDS -102 + + +#define ARRAY_LENGTH(array) (sizeof((array))/sizeof((array)[0])) + +#define ERR_TAGBLOCK_DECODE "Unable to decode tagblock string" +#define ERR_TAGBLOCK_TOO_LONG "Tagblock string too long" +#define ERR_TOO_MANY_FIELDS "Too many fields" +#define ERR_NMEA_TOO_LONG "NMEA string too long" +#define ERR_UNKNOWN "Unknown error" + +#define MAX_TAGBLOCK_FIELDS 8 // max number of fields allowed in a tagblock +#define MAX_TAGBLOCK_STR_LEN 1024 // max length of a tagblock string +#define MAX_KEY_LEN 32 // max length of a single key in a tagblock +#define MAX_VALUE_LEN 256 // max length of a single value in a tagblock +#define MAX_SENTENCE_LENGTH 1024 // max length of a single nmea sentence (tagblock + AIVDM) + +#define TAGBLOCK_SEPARATOR "\\" +#define CHECKSUM_SEPARATOR "*" +#define FIELD_SEPARATOR "," +#define KEY_VALUE_SEPARATOR ":" +#define GROUP_SEPARATOR "-" +#define AIVDM_START "!" +#define EMPTY_STRING "" + + +// string copy utils +char * unsafe_strcpy(char * __restrict dest, const char * __restrict est_end, const char * __restrict src); +size_t safe_strcpy(char * __restrict dst, const char * __restrict src, size_t dsize); + diff --git a/ais_tools/core/methods.c b/ais_tools/core/methods.c new file mode 100644 index 0000000..01829bd --- /dev/null +++ b/ais_tools/core/methods.c @@ -0,0 +1,175 @@ +#include +#include +#include +#include "core.h" +#include "checksum.h" +#include "tagblock.h" + +PyObject * +method_compute_checksum(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + const char *str; + + if (nargs != 1) + return PyErr_Format(PyExc_TypeError, "compute_checksum expects 1 argument"); + + str = PyUnicode_AsUTF8(PyObject_Str(args[0])); + return PyLong_FromLong(checksum(str)); +} + +PyObject * +method_compute_checksum_str(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + const char *str; + char c_str[3]; + + if (nargs != 1) + return PyErr_Format(PyExc_TypeError, "checksum_str expects 1 argument"); + + str = PyUnicode_AsUTF8(PyObject_Str(args[0])); + checksum_str(c_str, str, ARRAY_LENGTH(c_str)); + return PyUnicode_FromString(c_str); +} + +PyObject * +method_is_checksum_valid(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + const char *str; + char buffer[MAX_SENTENCE_LENGTH]; + + if (nargs != 1) + return PyErr_Format(PyExc_TypeError, "checksum_str expects 1 argument"); + + str = PyUnicode_AsUTF8(PyObject_Str(args[0])); + + if (safe_strcpy(buffer, str, ARRAY_LENGTH(buffer)) >= ARRAY_LENGTH(buffer)) + return PyErr_Format(PyExc_ValueError, "String too long"); + + return is_checksum_valid(buffer) ? Py_True: Py_False; +} + +PyObject * +method_join_tagblock(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + char buffer[MAX_SENTENCE_LENGTH]; + const char* tagblock_str; + const char* nmea_str; + + if (nargs != 2) + return PyErr_Format(PyExc_TypeError, "join expects 2 arguments"); + + tagblock_str = PyUnicode_AsUTF8(PyObject_Str(args[0])); + nmea_str = PyUnicode_AsUTF8(PyObject_Str(args[1])); + + if (FAIL == join_tagblock(buffer, ARRAY_LENGTH(buffer), tagblock_str, nmea_str)) + return PyErr_Format(PyExc_ValueError, ERR_NMEA_TOO_LONG); + + return PyUnicode_FromString(buffer); +} + +PyObject * +method_split_tagblock(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + const char* str; + char buffer[MAX_SENTENCE_LENGTH]; + const char* tagblock_str; + const char* nmea_str; + + if (nargs != 1) + return PyErr_Format(PyExc_TypeError, "split expects only 1 argument"); + + str = PyUnicode_AsUTF8(PyObject_Str(args[0])); + if (safe_strcpy(buffer, str, ARRAY_LENGTH(buffer)) >= ARRAY_LENGTH(buffer)) + return PyErr_Format(PyExc_ValueError, ERR_NMEA_TOO_LONG); + + split_tagblock(buffer, &tagblock_str, &nmea_str); + + return PyTuple_Pack(2, PyUnicode_FromString(tagblock_str), PyUnicode_FromString(nmea_str)); +} + +PyObject * +method_decode_tagblock(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + const char *str; + char tagblock_str[MAX_TAGBLOCK_STR_LEN]; + + if (nargs != 1) + return PyErr_Format(PyExc_TypeError, "decode expects only 1 argument"); + + str = PyUnicode_AsUTF8(PyObject_Str(args[0])); + + if (safe_strcpy(tagblock_str, str, ARRAY_LENGTH(tagblock_str)) >= ARRAY_LENGTH(tagblock_str)) + return PyErr_Format(PyExc_ValueError, ERR_TAGBLOCK_TOO_LONG); + + PyObject* dict = decode_tagblock(tagblock_str); + + if (!dict) + return PyErr_Format(PyExc_ValueError, ERR_TAGBLOCK_DECODE); + + return dict; +} + + +PyObject * +method_encode_tagblock(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + + PyObject *dict; + int result; + char tagblock_str[MAX_TAGBLOCK_STR_LEN]; + + if (nargs != 1) + return PyErr_Format(PyExc_TypeError, "encode expects only 1 argument"); + + dict = args[0]; + if (!PyDict_Check(dict)) + return PyErr_Format(PyExc_ValueError, "encode requires a dict object as the argument"); + + result = encode_tagblock(tagblock_str, dict, ARRAY_LENGTH(tagblock_str)); + + if (result < 0) + switch(result) + { + case FAIL_TOO_MANY_FIELDS: + return PyErr_Format(PyExc_ValueError, "encode failed: too many fields"); + case FAIL_STRING_TOO_LONG: + return PyErr_Format(PyExc_ValueError, "encode failed: encoded string is too long"); + } + + return PyUnicode_FromString(tagblock_str); +} + +PyObject * +method_update_tagblock(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + const char* str; + PyObject* dict; + char message[MAX_SENTENCE_LENGTH]; + char updated_message[MAX_SENTENCE_LENGTH]; + + if (nargs != 2) + return PyErr_Format(PyExc_TypeError, "update expects 2 arguments"); + + str = PyUnicode_AsUTF8(PyObject_Str(args[0])); + dict = args[1]; + if (!PyDict_Check(dict)) + return PyErr_Format(PyExc_ValueError, "The second argument to update must be a dict"); + + if (safe_strcpy(message, str, ARRAY_LENGTH(message)) >= ARRAY_LENGTH(message)) + return PyErr_Format(PyExc_ValueError, ERR_NMEA_TOO_LONG); + + int message_len = update_tagblock(updated_message, ARRAY_LENGTH(updated_message), message, dict); + + if (message_len < 0) + switch(message_len) + { + case FAIL_STRING_TOO_LONG: + return PyErr_Format(PyExc_ValueError, ERR_TAGBLOCK_TOO_LONG); + case FAIL_TOO_MANY_FIELDS: + return PyErr_Format(PyExc_ValueError, ERR_TOO_MANY_FIELDS); + default: + return PyErr_Format(PyExc_ValueError, ERR_UNKNOWN); + } + + return PyUnicode_FromString(updated_message); +} diff --git a/ais_tools/core/methods.h b/ais_tools/core/methods.h new file mode 100644 index 0000000..982c447 --- /dev/null +++ b/ais_tools/core/methods.h @@ -0,0 +1,10 @@ +// Module methods + +PyObject * method_compute_checksum (PyObject *module, PyObject *const *args, Py_ssize_t nargs); +PyObject * method_compute_checksum_str(PyObject *module, PyObject *const *args, Py_ssize_t nargs); +PyObject * method_is_checksum_valid (PyObject *module, PyObject *const *args, Py_ssize_t nargs); +PyObject * method_join_tagblock (PyObject *module, PyObject *const *args, Py_ssize_t nargs); +PyObject * method_split_tagblock (PyObject *module, PyObject *const *args, Py_ssize_t nargs); +PyObject * method_decode_tagblock (PyObject *module, PyObject *const *args, Py_ssize_t nargs); +PyObject * method_encode_tagblock (PyObject *module, PyObject *const *args, Py_ssize_t nargs); +PyObject * method_update_tagblock (PyObject *module, PyObject *const *args, Py_ssize_t nargs); \ No newline at end of file diff --git a/ais_tools/core/module.c b/ais_tools/core/module.c new file mode 100644 index 0000000..54d19f7 --- /dev/null +++ b/ais_tools/core/module.c @@ -0,0 +1,71 @@ +#include +#include +#include "core.h" +#include "methods.h" + +static PyMethodDef core_methods[] = { + { + "checksum", + (PyCFunction)(void(*)(void))method_compute_checksum, + METH_FASTCALL, + PyDoc_STR("Compute checksum of a string. Returns an integer value. The checksum for an empty string is 0") + }, + { + "checksum_str", + (PyCFunction)(void(*)(void))method_compute_checksum_str, + METH_FASTCALL, + PyDoc_STR("Compute checksum of a string. Returns a 2-character hex string") + }, + { + "is_checksum_valid", + (PyCFunction)(void(*)(void))method_is_checksum_valid, + METH_FASTCALL, + PyDoc_STR("Returns True if the given string is terminated with a valid checksum, else False") + }, + { + "decode_tagblock", + (PyCFunction)(void(*)(void))method_decode_tagblock, + METH_FASTCALL, + PyDoc_STR("Decode a tagblock string. Returns a dict") + }, + { + "encode_tagblock", + (PyCFunction)(void(*)(void))method_encode_tagblock, + METH_FASTCALL, + PyDoc_STR("Encode a tagblock string from a dict. Takes a dict and returns a string") + }, + { + "update_tagblock", + (PyCFunction)(void(*)(void))method_update_tagblock, + METH_FASTCALL, + PyDoc_STR("Update a tagblock string from a dict. Takes a string and a dict and returns a string") + }, + { + "split_tagblock", + (PyCFunction)(void(*)(void))method_split_tagblock, + METH_FASTCALL, + PyDoc_STR("Split off the tagblock portion of a longer nmea string. Returns a tuple containing two strings " + "(tagblock, nmea)") + }, + { + "join_tagblock", + (PyCFunction)(void(*)(void))method_join_tagblock, + METH_FASTCALL, + PyDoc_STR("Join a tagblock to an AIVDM message. Takes two strings and returns a string.") + }, + {NULL, NULL, 0, NULL} /* sentinel */ +}; + +static struct PyModuleDef core_module = { + PyModuleDef_HEAD_INIT, + "core", + PyDoc_STR("AIS Tools core methods implemented in C. Supports tagblock manipulation and computing checksums"), + -1, + core_methods +}; + +PyMODINIT_FUNC +PyInit_core(void) +{ + return PyModule_Create(&core_module); +} \ No newline at end of file diff --git a/ais_tools/core/strcpy.c b/ais_tools/core/strcpy.c new file mode 100644 index 0000000..a1645b1 --- /dev/null +++ b/ais_tools/core/strcpy.c @@ -0,0 +1,50 @@ +#include +#include + +/* + * Copy a source str to a dest str + * + * Does not write a null terminator + * dest_end should point to the last position in the dest string buffer. This method will + * stop at the character immediately before this position + * + * returns a pointer the the position immediately after the last position written in dest + * you can use the return pointer for a subsequent copy, or if you are finished, write a + * null char to that position to terminate the string + */ +char * unsafe_strcpy(char * __restrict dest, const char * __restrict dest_end, const char * __restrict src) +{ + while (dest < dest_end && *src) + *dest++ = *src++; + return dest; +} + + +/* + * Copy string src to buffer dst of size dsize. At most dsize-1 + * chars will be copied. Always NUL terminates (unless dsize == 0). + * Returns strlen(src); if retval >= dsize, truncation occurred. + */ +size_t safe_strcpy(char * __restrict dst, const char * __restrict src, size_t dsize) +{ + const char *osrc = src; + size_t nleft = dsize; + + /* Copy as many bytes as will fit. */ + if (nleft != 0) { + while (--nleft != 0) { + if ((*dst++ = *src++) == '\0') + break; + } + } + + /* Not enough room in dst, add NUL and traverse rest of src. */ + if (nleft == 0) { + if (dsize != 0) + *dst = '\0'; /* NUL-terminate dst */ + while (*src++) + ; + } + + return(src - osrc - 1); /* count does not include NUL */ +} \ No newline at end of file diff --git a/ais_tools/core/tagblock.h b/ais_tools/core/tagblock.h new file mode 100644 index 0000000..ce4f186 --- /dev/null +++ b/ais_tools/core/tagblock.h @@ -0,0 +1,52 @@ +/* tagblock definitions */ + +#define TAGBLOCK_TIMESTAMP "tagblock_timestamp" +#define TAGBLOCK_DESTINATION "tagblock_destination" +#define TAGBLOCK_LINE_COUNT "tagblock_line_count" +#define TAGBLOCK_RELATIVE_TIME "tagblock_relative_time" +#define TAGBLOCK_STATION "tagblock_station" +#define TAGBLOCK_TEXT "tagblock_text" +#define TAGBLOCK_SENTENCE "tagblock_sentence" +#define TAGBLOCK_GROUPSIZE "tagblock_groupsize" +#define TAGBLOCK_ID "tagblock_id" +#define CUSTOM_FIELD_PREFIX "tagblock_" +#define TAGBLOCK_GROUP "g" + + +/* tagblock_fields */ + +struct TAGBLOCK_FIELD +{ + char key[2]; + const char* value; +}; + +extern const char* group_field_keys[3]; + +const char* lookup_long_key(const char *short_key); +const char* lookup_short_key(const char* long_key); +int lookup_group_field_key(const char* long_key); +void extract_custom_short_key(char* buffer, size_t buf_size, const char* long_key); +void init_fields(struct TAGBLOCK_FIELD* fields, size_t num_fields); + +int split_fields(struct TAGBLOCK_FIELD* fields, char* tagblock_str, int max_fields); +int join_fields(char* tagblock_str, size_t buf_size, const struct TAGBLOCK_FIELD* fields, size_t num_fields); +int merge_fields( struct TAGBLOCK_FIELD* fields, size_t num_fields, size_t max_fields, + struct TAGBLOCK_FIELD* update_fields, size_t num_update_fields); + +/* tagblock_join */ +int join_tagblock(char* buffer, size_t buf_size, const char* tagblock_str, const char* nmea_str); + +/* tagblock_split */ +int split_tagblock(char* message, const char** tagblock, const char** nmea); + +/* tagblock_encode */ +int encode_fields(struct TAGBLOCK_FIELD* fields, size_t max_fields, PyObject* dict, char* buffer, size_t buf_size); +int encode_tagblock(char * dest, PyObject *dict, size_t dest_buf_size); + +/* tagblock_decode */ +PyObject * decode_tagblock(char * tagblock_str); + +/* tagblock_update */ +int update_tagblock(char * dest, size_t dest_size, char* message, PyObject * dict); + diff --git a/ais_tools/core/tagblock_decode.c b/ais_tools/core/tagblock_decode.c new file mode 100644 index 0000000..ced4f4d --- /dev/null +++ b/ais_tools/core/tagblock_decode.c @@ -0,0 +1,125 @@ +#include +#include +#include +#include "core.h" +#include "tagblock.h" + +int decode_timestamp_field(struct TAGBLOCK_FIELD* field, PyObject* dict) +{ + const char* key = lookup_long_key(field->key); + char * end; + + long value = strtol(field->value, &end, 10); + if (errno || *end) + return FAIL; + + if (value > 40000000000) + value = value / 1000; + PyDict_SetItemString(dict, key, PyLong_FromLong(value)); + return SUCCESS; +} + +int decode_int_field(struct TAGBLOCK_FIELD* field, PyObject* dict) +{ + const char* key = lookup_long_key(field->key); + char* end; + + long value = strtol(field->value, &end, 10); + if (errno || *end) + return FAIL; + + PyDict_SetItemString(dict, key, PyLong_FromLong(value)); + + return SUCCESS; +} + +int decode_text_field(struct TAGBLOCK_FIELD* field, PyObject* dict) +{ + const char* key = lookup_long_key(field->key); + PyDict_SetItemString(dict, key, PyUnicode_FromString(field->value)); + + return SUCCESS; +} + +int decode_group_field(struct TAGBLOCK_FIELD* field, PyObject* dict) +{ + size_t idx = 0; + char * save_ptr = NULL; + long values[ARRAY_LENGTH(group_field_keys)]; + char buffer[MAX_VALUE_LEN]; + + if (safe_strcpy(buffer, field->value, ARRAY_LENGTH(buffer)) >= ARRAY_LENGTH(buffer)) + return FAIL; + + char * f = strtok_r(buffer, GROUP_SEPARATOR, &save_ptr); + while (f && idx < ARRAY_LENGTH(values)) + { + char* end; + values[idx++] = strtol(f, &end, 10); + if (errno || *end) + return FAIL; + + f = strtok_r(NULL, GROUP_SEPARATOR, &save_ptr); + } + if (idx == ARRAY_LENGTH(values)) + { + for (idx = 0; idx < ARRAY_LENGTH(values); idx++) + PyDict_SetItemString(dict, group_field_keys[idx], PyLong_FromLong(values[idx])); + return SUCCESS; + } + + return FAIL; +} + +int decode_custom_field(struct TAGBLOCK_FIELD* field, PyObject* dict) +{ + char custom_key[MAX_KEY_LEN]; + snprintf(custom_key, ARRAY_LENGTH(custom_key), "%s%s", CUSTOM_FIELD_PREFIX, field->key); + PyDict_SetItemString(dict, custom_key, PyUnicode_FromString(field->value)); + + return SUCCESS; +} + +PyObject * decode_tagblock(char * tagblock_str) +{ + struct TAGBLOCK_FIELD fields[MAX_TAGBLOCK_FIELDS]; + int num_fields = 0; + int status = SUCCESS; + + PyObject* dict = PyDict_New(); + + num_fields = split_fields(fields, tagblock_str, ARRAY_LENGTH(fields)); + if (num_fields < 0) + status = FAIL; + + for (int i = 0; i < num_fields && status==SUCCESS; i++) + { + struct TAGBLOCK_FIELD* field = &fields[i]; + const char* key = field->key; + + switch(key[0]) + { + case 'c': + status = decode_timestamp_field(field, dict); + break; + case 'd': + case 's': + case 't': + status = decode_text_field(field, dict); + break; + case 'g': + status = decode_group_field(field, dict); + break; + case 'n': + case 'r': + status = decode_int_field(field, dict); + break; + default: + status = decode_custom_field(field, dict); + } + } + if (status == SUCCESS) + return dict; + else + return NULL; +} diff --git a/ais_tools/core/tagblock_encode.c b/ais_tools/core/tagblock_encode.c new file mode 100644 index 0000000..a580f22 --- /dev/null +++ b/ais_tools/core/tagblock_encode.c @@ -0,0 +1,95 @@ +#include +#include +#include +#include "core.h" +#include "tagblock.h" + + +size_t encode_group_fields(char* buffer, size_t buf_size, struct TAGBLOCK_FIELD* group_fields) +{ + const char * end = buffer + buf_size - 1; + char * ptr = buffer; + + for (size_t i = 0; i < ARRAY_LENGTH(group_field_keys); i++) + { + // make sure that all values are non-empty + if (group_fields[i].value == NULL) + return 0; + + ptr = unsafe_strcpy(ptr, end, group_fields[i].value); + if (i < ARRAY_LENGTH(group_field_keys) - 1) + ptr = unsafe_strcpy(ptr, end, GROUP_SEPARATOR); + } + *ptr++ = '\0'; // very important! unsafe_strcpy does not add a null at the end of the string + + return ptr - buffer; +} + + +int encode_fields(struct TAGBLOCK_FIELD* fields, size_t max_fields, PyObject* dict, char* buffer, size_t buf_size) +{ + PyObject *key, *value; + Py_ssize_t pos = 0; + size_t field_idx = 0; + struct TAGBLOCK_FIELD group_fields[ARRAY_LENGTH(group_field_keys)]; + + init_fields (group_fields, ARRAY_LENGTH(group_fields)); + + while (PyDict_Next(dict, &pos, &key, &value)) + { + if (field_idx == max_fields) + return FAIL; // no more room in fields + + const char* key_str = PyUnicode_AsUTF8(PyObject_Str(key)); + const char* value_str = PyUnicode_AsUTF8(PyObject_Str(value)); + + int group_field_idx = lookup_group_field_key(key_str); + if (group_field_idx >= 0) + { + group_fields[group_field_idx].value = value_str; + } + else + { + const char* short_key = lookup_short_key (key_str); + + if (short_key) + safe_strcpy(fields[field_idx].key, short_key, ARRAY_LENGTH(fields[field_idx].key)); + else + extract_custom_short_key(fields[field_idx].key, ARRAY_LENGTH(fields[field_idx].key), key_str); + + fields[field_idx].value = value_str; + field_idx++; + } + } + + // encode group field and add it to the field list + // check the return code to see if there is a complete set of group fields + if (encode_group_fields(buffer, buf_size, group_fields)) + { + if (field_idx >= max_fields) + return FAIL; // no more room to add another field + + safe_strcpy(fields[field_idx].key, TAGBLOCK_GROUP, ARRAY_LENGTH(fields[field_idx].key)); + fields[field_idx].value = buffer; + field_idx++; + } + + return field_idx; +} + +int encode_tagblock(char * dest, PyObject *dict, size_t dest_buf_size) +{ + struct TAGBLOCK_FIELD fields[MAX_TAGBLOCK_FIELDS]; + char value_buffer [MAX_VALUE_LEN]; + + init_fields (fields, ARRAY_LENGTH(fields)); + + int num_fields = encode_fields(fields, ARRAY_LENGTH(fields), dict, value_buffer, ARRAY_LENGTH(value_buffer)); + if (num_fields < 0) + return FAIL_TOO_MANY_FIELDS; + + int str_len = join_fields(dest, dest_buf_size, fields, num_fields); + if (str_len < 0) + return FAIL_STRING_TOO_LONG; + return str_len; +} \ No newline at end of file diff --git a/ais_tools/core/tagblock_fields.c b/ais_tools/core/tagblock_fields.c new file mode 100644 index 0000000..0d5328c --- /dev/null +++ b/ais_tools/core/tagblock_fields.c @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include "core.h" +#include "checksum.h" +#include "tagblock.h" + + +/* static mapping of short (one-character) field names to long field names (to be used as dict keys) */ +typedef struct {char short_key[2]; const char* long_key;} KEY_MAP; +static KEY_MAP key_map[] = { + {"c", TAGBLOCK_TIMESTAMP}, + {"d", TAGBLOCK_DESTINATION}, + {"n", TAGBLOCK_LINE_COUNT}, + {"r", TAGBLOCK_RELATIVE_TIME}, + {"s", TAGBLOCK_STATION}, + {"t", TAGBLOCK_TEXT} +}; + +/* array of long field names (to be used as dict keys) corresponding to the 3 values in a + * group field. eg in the tagblock "g:1-2-3", TAGBLOCK_SENTENCE=1, TAGBLOCK_GROUPSIZE=2 and TAGBLOCK_ID=3 + */ +const char* group_field_keys[3] = {TAGBLOCK_SENTENCE, TAGBLOCK_GROUPSIZE, TAGBLOCK_ID}; + +/* + * find the long tagblock field key that corresponds to a given short key + * + * Returns a pointer to the long key found in KEYMAP if there is a matching short key, + * else returns NULL + * +*/ +const char* lookup_long_key(const char *short_key) +{ + for (size_t i = 0; i < ARRAY_LENGTH(key_map); i++) + if (0 == strcmp(key_map[i].short_key, short_key)) + return key_map[i].long_key; + return NULL; +} + +/* + * find the xhort tagblock field key that corresponds to a given long key + * + * Returns a pointer to the short key found in KEYMAP if there is a matching long key, + * else returns NULL + * +*/ +const char* lookup_short_key(const char* long_key) +{ + for (size_t i = 0; i < ARRAY_LENGTH(key_map); i++) + if (0 == strcmp(key_map[i].long_key, long_key)) + return key_map[i].short_key; + return NULL; +} + +/* + * find the group key index in the range [0,2] that corresponds to the given long key + * + * If the given key matches a key in group_field_keys, returns the 0-based index + * If not match is found, returns FAIL (-1) + * +*/ +int lookup_group_field_key(const char* long_key) +{ + for (size_t i = 0; i < ARRAY_LENGTH(group_field_keys); i++) + if (0 == strcmp(long_key, group_field_keys[i])) + return i; + return FAIL; +} + +/* + * Create a short key from a long key that begins the with custom field prefix. This will be + * everything in the source key that comes after the end of the prefix. + * + * The new key is written into the given buffer. If the buffer is not long enough, the + * copied value is truncated, so the destination buffer will always end up with a + * null terminated string in it + * + * If the given key does not match the custom field prefix, then the entirety of of the + * given key is copied to the destination buffer. + */ +void extract_custom_short_key(char* buffer, size_t buf_size, const char* long_key) +{ + size_t prefix_len = ARRAY_LENGTH(CUSTOM_FIELD_PREFIX) - 1; + + if (0 == strncmp(CUSTOM_FIELD_PREFIX, long_key, prefix_len)) + safe_strcpy(buffer, &long_key[prefix_len], buf_size); + else + safe_strcpy(buffer, long_key, buf_size); +} + +/* + * Initialize an array of TAGBLOCK_FIELD to have empty, null terminated strings for the key + * field and NULL pointers for the value field + */ +void init_fields(struct TAGBLOCK_FIELD* fields, size_t num_fields) +{ + for (size_t i = 0; i < num_fields; i++) + { + fields[i].key[0] = '\0'; + fields[i].value = NULL; + } +} + + +/* + * Split a tagblok string into key/value pairs + * + * expects a tagblock_str with structure like + * k:value,k:value + * k:value,k:value*cc + * \\k:value,k:value*cc\\other_stuff + * + * NB THIS FUNCTION WILL MODIFY tagblock_str + * + * Uses strtok to cut up the source string into small strings. The resulting TAGBLOCK_FIELD + * objects will contain pointers to the resulting substrings stored in the original string + * So you should not use or modify the tagblock_str after calling this function + * + * Returns the number of elements written to the array of TAGBLOCK_FIELD + * If there are too many fields to fit in the array, returns FAIL (-1) + */ +int split_fields(struct TAGBLOCK_FIELD* fields, char* tagblock_str, int max_fields) +{ + int idx = 0; + char * ptr; + char * field_save_ptr = NULL; + char * field; + + char * key_value_save_ptr = NULL; + + // skip leading tagblock delimiter + if (*tagblock_str == *TAGBLOCK_SEPARATOR) + tagblock_str++; + + // seek forward to find either the checksum, the next tagblock separator, or the end of the string. + // Terminate the string at the first delimiter found + for (ptr = tagblock_str; *ptr != *TAGBLOCK_SEPARATOR && *ptr != *CHECKSUM_SEPARATOR && *ptr != '\0'; ptr++); + *ptr = '\0'; + + // get the first comma delimited field + field = strtok_r(tagblock_str, FIELD_SEPARATOR, &field_save_ptr); + while (field && *field && idx < max_fields) + { + // for each field, split into key part and value part + const char* key = strtok_r(field, KEY_VALUE_SEPARATOR, &key_value_save_ptr); + const char* value = strtok_r(NULL, KEY_VALUE_SEPARATOR, &key_value_save_ptr); + + // if we don't have both key and value, then fail + if (key && value) + { + safe_strcpy(fields[idx].key, key, ARRAY_LENGTH(fields[idx].key)); + fields[idx].value = value; + idx++; + } + else + return FAIL; + + // advance to the next field + field = strtok_r(NULL, FIELD_SEPARATOR, &field_save_ptr); + } + return idx; +} + +/* + * Join an array of fields into a tagblock string + * + * Writes a formatted tagblock string into the provided buffer using all the key/value pairs + * in the given array of TAGBLOCK_FIELD. + * + * Returns the length of the resulting tagblock string, excluding the NUL + * If the buffer is not big enough to contain the string, returns FAIL (-1) + */ +int join_fields(char* tagblock_str, size_t buf_size, const struct TAGBLOCK_FIELD* fields, size_t num_fields) +{ + const char * end = tagblock_str + buf_size - 1; + size_t last_field_idx = num_fields - 1; + char * ptr = tagblock_str; + char checksum[3]; + + for (size_t idx = 0; idx < num_fields; idx++) + { + ptr = unsafe_strcpy(ptr, end, fields[idx].key); + ptr = unsafe_strcpy(ptr, end, KEY_VALUE_SEPARATOR); + ptr = unsafe_strcpy(ptr, end, fields[idx].value); + if (idx < last_field_idx) + { + ptr = unsafe_strcpy(ptr, end, FIELD_SEPARATOR); + } + } + *ptr = '\0'; // very important! unsafe_strcpy does not add a null at the end of the string + + checksum_str(checksum, tagblock_str, ARRAY_LENGTH(checksum)); + + ptr = unsafe_strcpy(ptr, end, CHECKSUM_SEPARATOR); + ptr = unsafe_strcpy(ptr, end, checksum); + + if (ptr == end) + return FAIL; + + *ptr = '\0'; // very important! unsafe_strcpy does not add a null at the end of the string + return ptr - tagblock_str; +} + +/* + * Merge one array of TAGBLOCK_FIELD into another + * + * Read fields in `update_fields` and overwrite or append to `fields` + * Overwrite if the keys matchm else append + * + * Return the new length of the destination array + * Returns FAIL (-1) if the destination array is not large enough to hold the combined set of fields + */ + +int merge_fields( struct TAGBLOCK_FIELD* fields, size_t num_fields, size_t max_fields, + struct TAGBLOCK_FIELD* update_fields, size_t num_update_fields) +{ + for (size_t update_idx = 0; update_idx < num_update_fields; update_idx++) + { + char* key = update_fields[update_idx].key; + + size_t fields_idx = 0; + while (fields_idx < num_fields) + { + if (0 == strcmp(key, fields[fields_idx].key)) + break; + fields_idx++; + } + if (fields_idx == num_fields) + { + if (num_fields < max_fields) + num_fields++; + else + return FAIL; + } + + fields[fields_idx] = update_fields[update_idx]; + } + + return num_fields; +} \ No newline at end of file diff --git a/ais_tools/core/tagblock_join.c b/ais_tools/core/tagblock_join.c new file mode 100644 index 0000000..a303f8c --- /dev/null +++ b/ais_tools/core/tagblock_join.c @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include "core.h" + + +int join_tagblock(char* buffer, size_t buf_size, const char* tagblock_str, const char* nmea_str) +{ + char* end = buffer + buf_size - 1; + char* ptr = buffer; + + if (*tagblock_str && *nmea_str) + { + if (*tagblock_str != *TAGBLOCK_SEPARATOR) + ptr = unsafe_strcpy(ptr, end, TAGBLOCK_SEPARATOR); + ptr = unsafe_strcpy(ptr, end, tagblock_str); + + if (*nmea_str != *TAGBLOCK_SEPARATOR) + ptr = unsafe_strcpy(ptr, end, TAGBLOCK_SEPARATOR); + ptr = unsafe_strcpy(ptr, end, nmea_str); + } + else + { + ptr = unsafe_strcpy(ptr, end, tagblock_str); + ptr = unsafe_strcpy(ptr, end, nmea_str); + } + + if (ptr <= end) + { + *ptr = '\0'; + return ptr - buffer; + } + else + { + *end = '\0'; + return FAIL; + } + + return SUCCESS; +} diff --git a/ais_tools/core/tagblock_split.c b/ais_tools/core/tagblock_split.c new file mode 100644 index 0000000..3948d20 --- /dev/null +++ b/ais_tools/core/tagblock_split.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include "core.h" + +/* + * Split a sentence into the tagblock part and the nmea part + * + * This method modifies the given message to write a null character to terminate + * the tagblock part + * + * returns the length of the tagblock part + * + * Four cases for the given message string + * + * starts with '!' - no tagblock, entire message in nmea + * starts with '\!' - no tagblock, strip off '\',entire message in nmea + * starts with '\[^!]' - is a tagblock , strip off leading '\', nmea is whatever comes after the next '\'. + * starts with '[^\!]' - is a tagblock, nmea is whatever comes after the next '\'. + * starts with '[^!]' and there are no `\` delimiters - tagblock is empty, entire message in nmea + */ +int split_tagblock(char* message, const char** tagblock, const char** nmea) +{ + + char* ptr; + int tagblock_len = 0; + + ptr = message; + if (*ptr == *TAGBLOCK_SEPARATOR) + ptr ++; + if (*ptr == *AIVDM_START) + { + *nmea = ptr; + *tagblock = EMPTY_STRING; + } + else + { + *tagblock = ptr; + for (; *ptr != '\0' && *ptr != *TAGBLOCK_SEPARATOR; ptr++); + tagblock_len = ptr - *tagblock; + if (*ptr) + { + *ptr = '\0'; + *nmea = ptr + 1; + } + else + *nmea = EMPTY_STRING; + } + + return tagblock_len; +} \ No newline at end of file diff --git a/ais_tools/core/tagblock_update.c b/ais_tools/core/tagblock_update.c new file mode 100644 index 0000000..9cb0443 --- /dev/null +++ b/ais_tools/core/tagblock_update.c @@ -0,0 +1,48 @@ +#include +#include +#include +#include "core.h" +#include "tagblock.h" + + +int update_tagblock(char * dest, size_t dest_size, char* message, PyObject * dict) +{ + const char* tagblock_str; + const char* nmea_str; + char tagblock_buffer[MAX_TAGBLOCK_STR_LEN]; + struct TAGBLOCK_FIELD fields[MAX_TAGBLOCK_FIELDS]; + int num_fields = 0; + + split_tagblock(message, &tagblock_str, &nmea_str); + + if (safe_strcpy(tagblock_buffer, tagblock_str, ARRAY_LENGTH(tagblock_buffer)) >= ARRAY_LENGTH(tagblock_buffer)) + return FAIL_STRING_TOO_LONG; + + num_fields = split_fields(fields, tagblock_buffer, ARRAY_LENGTH(fields)); + if (num_fields < 0) + return FAIL_TOO_MANY_FIELDS; + + struct TAGBLOCK_FIELD update_fields[MAX_TAGBLOCK_FIELDS]; + int num_update_fields = 0; + char value_buffer[MAX_VALUE_LEN]; + num_update_fields = encode_fields(update_fields, ARRAY_LENGTH(update_fields), dict, + value_buffer, ARRAY_LENGTH(value_buffer)); + if (num_update_fields < 0) + return FAIL_TOO_MANY_FIELDS; + + num_fields = merge_fields(fields, num_fields, ARRAY_LENGTH(fields), update_fields, num_update_fields); + if (num_fields < 0) + return FAIL_TOO_MANY_FIELDS; + + char updated_tagblock_str[MAX_TAGBLOCK_STR_LEN]; + int msg_len = join_fields(updated_tagblock_str, ARRAY_LENGTH(updated_tagblock_str), fields, num_fields); + if (msg_len < 0) + return FAIL_STRING_TOO_LONG; + + msg_len = join_tagblock(dest, dest_size, updated_tagblock_str, nmea_str); + if (msg_len < 0) + return FAIL_STRING_TOO_LONG; + + return msg_len; +} + diff --git a/ais_tools/nmea.py b/ais_tools/nmea.py index 64554b7..19179b1 100644 --- a/ais_tools/nmea.py +++ b/ais_tools/nmea.py @@ -3,7 +3,7 @@ import re from ais import DecodeError -from ais_tools.checksum import is_checksum_valid +from ais_tools.core import is_checksum_valid from ais_tools.tagblock import split_tagblock from ais_tools.tagblock import decode_tagblock @@ -14,6 +14,10 @@ def expand_nmea(line, validate_checksum=False): tagblock_str, nmea = split_tagblock(line) + if not nmea: + nmea = tagblock_str + tagblock_str = '' + tagblock = decode_tagblock(tagblock_str, validate_checksum=validate_checksum) nmea = nmea.strip() diff --git a/ais_tools/tagblock.py b/ais_tools/tagblock.py index 7a10886..44f51c7 100644 --- a/ais_tools/tagblock.py +++ b/ais_tools/tagblock.py @@ -2,13 +2,9 @@ from datetime import timezone from ais import DecodeError -from ais_tools.checksum import checksumstr -from ais_tools.checksum import is_checksum_valid +from ais_tools.core import is_checksum_valid +from ais_tools import core -# import warnings -# with warnings.catch_warnings(): -# warnings.simplefilter("ignore") -# from ais.stream import parseTagBlock # noqa: F401 TAGBLOCK_T_FORMAT = '%Y-%m-%d %H.%M.%S' @@ -45,8 +41,10 @@ def create_tagblock(station, timestamp=None, add_tagblock_t=True): ) if add_tagblock_t: params['T'] = datetime.fromtimestamp(t, tz=timezone.utc).strftime(TAGBLOCK_T_FORMAT) - param_str = ','.join(["{}:{}".format(k, v) for k, v in params.items()]) - return '{}*{}'.format(param_str, checksumstr(param_str)) + return core.encode_tagblock(params) + + # param_str = ','.join(["{}:{}".format(k, v) for k, v in params.items()]) + # return '{}*{}'.format(param_str, checksum_str(param_str)) def split_tagblock(nmea): @@ -56,22 +54,17 @@ def split_tagblock(nmea): Note that if the nmea is a concatenated multipart message then only the tagblock of the first message will be split off """ - tagblock = '' - if nmea.startswith("\\") and not nmea.startswith("\\!"): - parts = nmea[1:].split("\\", 1) - if len(parts) == 2: - tagblock, nmea = parts - return tagblock, nmea + return core.split_tagblock(nmea) + def join_tagblock(tagblock, nmea): """ Join a tagblock to an AIVDM message that does not already have a tagblock """ - if tagblock and nmea: - return "\\{}\\{}".format(tagblock.lstrip('\\'), nmea.lstrip('\\')) - else: - return "{}{}".format(tagblock, nmea) + + return core.join_tagblock(tagblock, nmea) + def add_tagblock(tagblock, nmea, overwrite=True): @@ -87,86 +80,29 @@ def add_tagblock(tagblock, nmea, overwrite=True): return join_tagblock(tagblock, nmea) -tagblock_fields = { - 'c': 'tagblock_timestamp', - 'n': 'tagblock_line_count', - 'r': 'tagblock_relative_time', - 'd': 'tagblock_destination', - 's': 'tagblock_station', - 't': 'tagblock_text', -} - -tagblock_fields_reversed = {v: k for k, v in tagblock_fields.items()} - -tagblock_group_fields = ["tagblock_sentence", "tagblock_groupsize", "tagblock_id"] - - def encode_tagblock(**kwargs): - group_fields = {} - fields = {} - - for k, v in kwargs.items(): - if k in tagblock_group_fields: - group_fields[k] = str(v) - elif k in tagblock_fields_reversed: - fields[tagblock_fields_reversed[k]] = v - else: - fields[k.replace('tagblock_', '')] = v - - if len(group_fields) == 3: - fields['g'] = '-'.join([group_fields[k] for k in tagblock_group_fields]) - - base_str = ','.join(["{}:{}".format(k, v) for k, v in fields.items()]) - return '{}*{}'.format(base_str, checksumstr(base_str)) + try: + return core.encode_tagblock(kwargs) + except ValueError as e: + raise DecodeError(e) def decode_tagblock(tagblock_str, validate_checksum=False): - - tagblock = tagblock_str.rsplit("*", 1)[0] - - fields = {} - - if not tagblock: - return fields - if validate_checksum and not is_checksum_valid(tagblock_str): raise DecodeError('Invalid checksum') - - for field in tagblock.split(","): - try: - key, value = field.split(":") - - if key == 'g': - parts = [int(part) for part in value.split("-") if part] - if len(parts) != 3: - raise DecodeError('Unable to decode tagblock group') - fields.update(dict(zip(tagblock_group_fields, parts))) - else: - if key in ['n', 'r']: - value = int(value) - elif key == 'c': - value = int(value) - if value > 40000000000: - value = value / 1000.0 - - fields[tagblock_fields.get(key, key)] = value - except ValueError: - raise DecodeError('Unable to decode tagblock string') - - return fields + try: + return core.decode_tagblock(tagblock_str) + except ValueError as e: + raise DecodeError(e) def update_tagblock(nmea, **kwargs): - tagblock_str, nmea = split_tagblock(nmea) - tagblock = decode_tagblock(tagblock_str) - tagblock.update(kwargs) - tagblock_str = encode_tagblock(**tagblock) - return join_tagblock(tagblock_str, nmea) + return core.update_tagblock(nmea, kwargs) def safe_update_tagblock(nmea, **kwargs): try: - nmea = update_tagblock(nmea, **kwargs) - except DecodeError: + nmea = core.update_tagblock(nmea, kwargs) + except ValueError: pass return nmea diff --git a/setup.py b/setup.py index 6eba8b7..4b1428f 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ import codecs import sys +import os from setuptools import Extension package = __import__('ais_tools') @@ -24,6 +25,16 @@ else: extra_compile_args += ["-std=c11", "-Wall", "-Werror", "-O3"] +source_path = 'ais_tools/core/' +sources = [f'{source_path}{file}' for file in os.listdir(source_path) if file.endswith('.c')] +core_module = Extension( + "ais_tools.core", + extra_compile_args=extra_compile_args, + extra_link_args=extra_link_args, + sources=sources, + include_dirs=[source_path], + undef_macros=undef_macros, +) DEPENDENCIES = [ "libais", @@ -65,14 +76,5 @@ [console_scripts] ais-tools=ais_tools.cli:cli ''', - ext_modules=[ - Extension( - "ais_tools.checksum", - extra_compile_args=extra_compile_args, - extra_link_args=extra_link_args, - sources=["ais_tools/checksum.c"], - include_dirs=["ais_tools/"], - undef_macros=undef_macros, - ) - ], + ext_modules=[core_module], ) diff --git a/tests/test_checksum.py b/tests/test_checksum.py index 561e5c2..6f9ccd6 100644 --- a/tests/test_checksum.py +++ b/tests/test_checksum.py @@ -1,8 +1,8 @@ import pytest -from ais_tools.checksum import checksum -from ais_tools.checksum import is_checksum_valid -from ais_tools.checksum import checksumstr +from ais_tools.core import checksum +from ais_tools.core import is_checksum_valid +from ais_tools.core import checksum_str import warnings with warnings.catch_warnings(): @@ -27,7 +27,7 @@ def test_checksum(str, expected): ('', '00'), ]) def test_checksum_str(str, expected): - actual = checksumstr(str) + actual = checksum_str(str) assert actual == expected if len(str) > 1: @@ -36,6 +36,7 @@ def test_checksum_str(str, expected): @pytest.mark.parametrize("str,expected", [ ("", False), + ("*00", True), ("*", False), ("4", False), ("40", False), diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 08380e2..16f0b42 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -10,9 +10,10 @@ def test_issue_40(): runner = CliRunner() input = "\\g:BAD-GROUP,c:1326055296*3C\\!AIVDM,2,1,3,A,E7`B1:dW7oHth@@@@@@@@@@@@@@6@6R;mMQM@10888Qr8`8888888888,0*65" args = '--station=test' + expected = "\\g:BAD-GROUP,c:1326055296,s:test*65\\!AIVDM,2,1,3,A,E7`B1:dW7oHth@@@@@@@@@@@@@@6@6R;mMQM@10888Qr8`8888888888,0*65" result = runner.invoke(update_tagblock, input=input, args=args) assert not result.exception - assert result.output.strip() == input + assert result.output.strip() == expected def test_tagblock_group_with_extra_delimiters(): diff --git a/tests/test_nmea.py b/tests/test_nmea.py index a1e5746..c3eebf6 100644 --- a/tests/test_nmea.py +++ b/tests/test_nmea.py @@ -46,6 +46,14 @@ def test_expand_nmea_fail(nmea): with pytest.raises(DecodeError): tagblock, body, pad = expand_nmea(nmea, validate_checksum=False) +@pytest.mark.parametrize("nmea", [ + "\\!AIVDM,1,1,,A,BADCHECKSUM,0*00", + "\\s:00*49\\!AIVDM,1,1,,A,BADCHECKSUM,0*00", +]) +def test_expand_nmea_validate_fail(nmea): + with pytest.raises(DecodeError): + tagblock, body, pad = expand_nmea(nmea, validate_checksum=True) + @pytest.mark.parametrize("nmea", [ (['!AIVDM,2,1,7,A,@*00']), diff --git a/tests/test_tagblock.py b/tests/test_tagblock.py index d96d9e7..001fd8f 100644 --- a/tests/test_tagblock.py +++ b/tests/test_tagblock.py @@ -3,6 +3,7 @@ from ais_tools import tagblock from ais_tools.tagblock import DecodeError +from ais_tools import core @pytest.mark.parametrize("line,expected", [ ("\\s:rORBCOMM000,q:u,c:1509502436,T:2017-11-01 02.13.56*50\\!AIVDM,1,1,,A,13`el0gP000H=3JN9jb>4?wb0>`<,0*7B", @@ -32,9 +33,10 @@ def test_create_tagblock(station, timestamp, add_tagblock_t, expected): @pytest.mark.parametrize("nmea,expected", [ ("!AIVDM", ('', '!AIVDM')), - ("\\!AIVDM", ('', '\\!AIVDM')), + ("\\!AIVDM", ('', '!AIVDM')), ("\\c:1000,s:sta*5B\\!AIVDM", ('c:1000,s:sta*5B', '!AIVDM')), - ("NOT A MESSAGE", ('', 'NOT A MESSAGE')), + ("\\c:1000,s:sta*5B", ('c:1000,s:sta*5B', '')), + ("NOT A MESSAGE", ('NOT A MESSAGE', '')), ]) def test_split_tagblock(nmea, expected): assert expected == tagblock.split_tagblock(nmea) @@ -78,9 +80,17 @@ def test_encode_tagblock(fields, expected): assert expected == tagblock.encode_tagblock(**fields) +@pytest.mark.parametrize("fields", [ + ({'tagblock_text': '0' * 1024,}) +]) +def test_encode_tagblock_fail(fields): + with pytest.raises(DecodeError): + tagblock.encode_tagblock(**fields) + + @pytest.mark.parametrize("tagblock_str,expected", [ ('*00', {}), - ('z:123*70', {'z': '123'}), + ('z:123*70', {'tagblock_z': '123'}), ('r:123*78', {'tagblock_relative_time': 123}), ('c:123456789*68', {'tagblock_timestamp': 123456789}), ('c:123456789,s:test,g:1-2-3*5A', @@ -111,8 +121,10 @@ def test_decode_tagblock_invalid_checksum(tagblock_str): ('invalid'), ('c:invalid'), ('c:123456789,z'), + ('n:not_a_number'), ('g:BAD,c:1326055296'), - ('g:1-2,c:1326055296') + ('g:1-2,c:1326055296'), + ('g:1-2-BAD'), ]) def test_decode_tagblock_invalid(tagblock_str): with pytest.raises(DecodeError, match='Unable to decode tagblock'): @@ -122,6 +134,7 @@ def test_decode_tagblock_invalid(tagblock_str): @pytest.mark.parametrize("tagblock_str,new_fields,expected", [ ('!AIVDM', {'q': 123}, '\\q:123*7B\\!AIVDM'), ('\\!AIVDM', {'q': 123}, '\\q:123*7B\\!AIVDM'), + ('\\s:00*00\\!AIVDM', {}, '\\s:00*49\\!AIVDM'), ('\\s:00*00\\!AIVDM', {'tagblock_station': 99}, '\\s:99*49\\!AIVDM'), ('\\s:00*00\\!AIVDM\\s:00*00\\!AIVDM', {'tagblock_station': 99}, '\\s:99*49\\!AIVDM\\s:00*00\\!AIVDM'), ('\\c:123456789*68\\!AIVDM', {}, '\\c:123456789*68\\!AIVDM'), @@ -130,3 +143,65 @@ def test_decode_tagblock_invalid(tagblock_str): ]) def test_update_tagblock(tagblock_str, new_fields, expected): assert expected == tagblock.update_tagblock(tagblock_str, **new_fields) + + +@pytest.mark.parametrize("tagblock_str,new_fields,expected", [ + ('!AIVDM', {'q': 123}, '\\q:123*7B\\!AIVDM'), + ('\\s:00*49\\!AIVDM', {}, '\\s:00*49\\!AIVDM'), + ('\\s:00*49\\!AIVDM', {'tagblock_text' : '0' * 1024}, '\\s:00*49\\!AIVDM'), +]) +def test_safe_update_tagblock(tagblock_str, new_fields, expected): + assert expected == tagblock.safe_update_tagblock(tagblock_str, **new_fields) + + +@pytest.mark.parametrize("tagblock_str,expected", [ + ('', {}), + ('*00', {}), + ('a:1', {'tagblock_a': '1'}), + ('d:dest*00', {'tagblock_destination': 'dest'}), + ('n:42', {'tagblock_line_count': 42}), + ("c:123456789", {'tagblock_timestamp': 123456789}), + ('g:1-2-3', {'tagblock_sentence': 1, 'tagblock_groupsize': 2, 'tagblock_id': 3}), + ('s:rMT5858,*0E', {'tagblock_station': 'rMT5858'}) +]) +def test_tagblock_decode(tagblock_str, expected): + assert core.decode_tagblock(tagblock_str) == expected + + +@pytest.mark.parametrize("fields,expected", [ + ({},'*00'), + ({'a': 1}, 'a:1*6A'), + ({'tagblock_a': 1}, 'a:1*6A'), + ({'tagblock_a': None}, 'a:None*71'), + ({'tagblock_timestamp': 12345678}, "c:12345678*51"), + ({'tagblock_sentence': 1, 'tagblock_groupsize': 2, 'tagblock_id': 3}, 'g:1-2-3*6D'), + ({'tagblock_line_count': 1}, 'n:1*65') +]) +def test_tagblock_encode(fields, expected): + assert core.encode_tagblock(fields) == expected + + +@pytest.mark.parametrize("fields", [ + ({}), + ({'tagblock_a': '1'}), + ({'tagblock_timestamp': 12345678}), + ({'tagblock_timestamp': 12345678, + 'tagblock_destination': 'D', + 'tagblock_line_count': 2, + 'tagblock_station': 'S', + 'tagblock_text': 'T', + 'tagblock_relative_time': 12345678, + 'tagblock_sentence': 1, + 'tagblock_groupsize': 2, + 'tagblock_id': 3 + }), +]) +def test_encode_decode(fields): + assert core.decode_tagblock(core.encode_tagblock(fields)) == fields + + +# def test_update(): +# tagblock_str="\\z:1*71\\" +# fields = {'tagblock_text': 'ABC'} +# expected = "z:1,t:ABC*53" +# assert core.update_tagblock(tagblock_str, fields) == expected diff --git a/utils/perf-test.py b/utils/perf-test.py index d707bcc..81cb7ec 100644 --- a/utils/perf-test.py +++ b/utils/perf-test.py @@ -3,6 +3,7 @@ import pstats from pstats import SortKey from ais_tools.aivdm import AIVDM, AisToolsDecoder +from ais_tools.tagblock import update_tagblock # from ais_tools.aivdm import LibaisDecoder @@ -16,7 +17,6 @@ "\\g:2-2-2243*5A" \ "\\!AIVDM,2,2,1,B,p4l888888888880,2*36" - def libais_vs_aistools(): tests = [type_1, type_18] @@ -60,6 +60,10 @@ def full_decode(n): msg.add_parser_version() +def update_tgblock(n): + for i in range(n): + update_tagblock(message1, tagblock_text='T') + def run_perf_test(func): cProfile.run(func, 'perf-test.stats') @@ -68,7 +72,8 @@ def run_perf_test(func): def main(): - run_perf_test('decode(10000)') + run_perf_test('update_tgblock(1000000)') + # run_perf_test('decode(10000)') # run_perf_test('full_decode(100000)') # checksum_compare()