diff --git a/docs/api/asdf/yaml.h.rst b/docs/api/asdf/yaml.h.rst new file mode 100644 index 00000000..b87a6800 --- /dev/null +++ b/docs/api/asdf/yaml.h.rst @@ -0,0 +1,4 @@ +asdf/yaml.h +=========== + +.. autodoc:: include/asdf/yaml.h diff --git a/docs/conf.py b/docs/conf.py index c2c07534..7ad5df33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -100,6 +100,7 @@ def read_config_h() -> tuple[str, str, str]: # libasdf identifiers that should be documented but aren't yet ('c:identifier', 'asdf_emitter_cfg_t'), + ('c:identifier', 'asdf_event_t'), ('c:identifier', 'asdf_history_entry_t'), ('c:identifier', 'asdf_parser_cfg_t'), ] diff --git a/docs/index.rst b/docs/index.rst index e1e63020..cd9bcfda 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,6 +48,7 @@ headers. :maxdepth: 2 api/asdf/extension.h + api/asdf/yaml.h Resources diff --git a/include/asdf/core/ndarray.h b/include/asdf/core/ndarray.h index 9d12764e..b75a3195 100644 --- a/include/asdf/core/ndarray.h +++ b/include/asdf/core/ndarray.h @@ -200,6 +200,22 @@ ASDF_EXPORT uint64_t asdf_ndarray_nbytes(const asdf_ndarray_t *ndarray); ASDF_EXPORT void *asdf_ndarray_data_alloc(asdf_ndarray_t *ndarray); +/** + * Allocate a temporary data buffer for an ndarray to be written to a file, + * with automatic cleanup after the write completes. + * + * Like `asdf_ndarray_data_alloc` but the allocated memory is freed + * automatically after `asdf_write_to` (or `asdf_close`) is called. + * Extension authors should use this instead of `asdf_ndarray_data_alloc` + * when building ndarrays inside a serialize callback. + * + * :param file: The `asdf_file_t *` to register the cleanup with + * :param ndarray: An `asdf_ndarray_t *` whose shape and datatype are already set + * :return: A `void *` to the zero-initialized buffer, or NULL on OOM + */ +ASDF_EXPORT void *asdf_ndarray_data_alloc_temp(asdf_file_t *file, asdf_ndarray_t *ndarray); + + /** * Free ndarray data allocated with `asdf_ndarray_data_alloc` * diff --git a/include/asdf/file.h b/include/asdf/file.h index b83c4479..ebc38d81 100644 --- a/include/asdf/file.h +++ b/include/asdf/file.h @@ -143,19 +143,19 @@ static asdf_file_t *asdf_open_mem(const void *buf, size_t size); #define ASDF__OPEN_1(source) \ - _Generic((source), \ + _Generic( \ + (source), \ FILE *: asdf_open_fp(source, NULL), \ const char *: asdf_open_file(source, "r"), \ char *: asdf_open_file(source, "r"), \ - void *: asdf_open_mem(NULL, 0) \ - ) + void *: asdf_open_mem(NULL, 0)) #define ASDF__OPEN_2(source, ...) \ - _Generic((source), \ + _Generic( \ + (source), \ FILE *: asdf_open_fp, \ const char *: asdf_open_file, \ char *: asdf_open_file, \ - const void *: asdf_open_mem \ - )(source, __VA_ARGS__) + const void *: asdf_open_mem)(source, __VA_ARGS__) /** @@ -193,12 +193,12 @@ static asdf_file_t *asdf_open_mem(const void *buf, size_t size); * `asdf_open_mem_ex` depending on the argument types */ #define asdf_open_ex(source, ...) /* NOLINT(readability-identifier-naming) */ \ - _Generic((source), \ + _Generic( \ + (source), \ const char *: asdf_open_file_ex, \ char *: asdf_open_file_ex, \ FILE *: asdf_open_fp_ex, \ - void *: asdf_open_mem_ex \ - )(source, __VA_ARGS__) + void *: asdf_open_mem_ex)(source, __VA_ARGS__) /** * Opens an ASDF file for reading @@ -237,22 +237,43 @@ static inline asdf_file_t *asdf_open_mem(const void *buf, size_t size) { #define ASDF__WRITE_TO_1(source, dest) \ - _Generic((dest), \ + _Generic( \ + (dest), \ const char *: asdf_write_to_file, \ char *: asdf_write_to_file, \ - FILE *: asdf_write_to_fp \ - )(file, dest) + FILE *: asdf_write_to_fp)(file, dest) #define ASDF__WRITE_TO_2(source, dest, ...) asdf_write_to_mem(source, dest, __VA_ARGS__) +/** + * Write the contents of an ``asdf_file_t`` to a destination + * + * This is a type-generic macro that dispatches to one of the following based + * on the type and number of arguments after ``file``: + * + * * ``asdf_write_to(file, filename)`` -- where ``filename`` is a + * ``const char *`` or ``char *``: calls `asdf_write_to_file` + * * ``asdf_write_to(file, fp)`` -- where ``fp`` is a ``FILE *``: calls + * `asdf_write_to_fp` + * * ``asdf_write_to(file, buf, size)`` -- where ``buf`` is a ``void **`` and + * ``size`` is a ``size_t *``: calls `asdf_write_to_mem` + * + * :param file: The `asdf_file_t *` to write + * :param ...: Destination argument(s) -- see above + * :return: 0 on success, non-zero on failure + */ #define asdf_write_to(file, ...) /* NOLINT(readability-identifier-naming) */ \ ASDF__PP_CAT(ASDF__WRITE_TO_, ASDF__PP_NARGS(__VA_ARGS__))(file, __VA_ARGS__) /** * Write the contents of the ``asdf_file_t`` to the given filesystem path + * + * :param file: The `asdf_file_t *` to write + * :param filename: Path to the output file; created or truncated as needed + * :return: 0 on success, non-zero on failure */ ASDF_EXPORT int asdf_write_to_file(asdf_file_t *file, const char *filename); @@ -260,6 +281,10 @@ ASDF_EXPORT int asdf_write_to_file(asdf_file_t *file, const char *filename); /** * Write the contents of the ``asdf_file_t`` to the given writeable ``FILE *`` * stream + * + * :param file: The `asdf_file_t *` to write + * :param fp: An open, writeable ``FILE *`` stream + * :return: 0 on success, non-zero on failure */ ASDF_EXPORT int asdf_write_to_fp(asdf_file_t *file, FILE *fp); @@ -267,14 +292,18 @@ ASDF_EXPORT int asdf_write_to_fp(asdf_file_t *file, FILE *fp); /** * Write the contents of the ``asdf_file_t`` to a memory buffer * - * If the value of ``*buf`` is non-NULL then a user-provided buffer is assumed - * and its size is read from ``*size``. If the size is not large enough to - * hold the file, then it is simply truncated and a non-zero return value is - * returned. + * If ``*buf`` is non-NULL, a user-provided buffer is assumed and its size is + * read from ``*size``. If the buffer is not large enough to hold the file, + * the output is truncated and a non-zero value is returned. + * + * If ``*buf`` is NULL, a buffer is allocated with `malloc()` and a pointer to + * it is stored in ``*buf``; the allocated size is written to ``*size``. The + * caller is responsible for freeing the buffer with `free()`. * - * Otherwise, memory is allocated with `malloc()` and the size of the buffer is - * returned into the ``size`` argument. The user is responsible for freeing - * the buffer with `free()`. + * :param file: The `asdf_file_t *` to write + * :param buf: Address of a ``void *`` buffer pointer (in/out) + * :param size: Address of a ``size_t`` holding the buffer size (in/out) + * :return: 0 on success, non-zero on failure */ ASDF_EXPORT int asdf_write_to_mem(asdf_file_t *file, void **buf, size_t *size); diff --git a/include/asdf/value.h b/include/asdf/value.h index d495f6af..6ad0b54e 100644 --- a/include/asdf/value.h +++ b/include/asdf/value.h @@ -406,38 +406,135 @@ ASDF_EXPORT void asdf_mapping_item_destroy(asdf_mapping_item_t *item); */ typedef struct asdf_sequence asdf_sequence_t; +/** Return true if ``value`` holds a YAML sequence */ ASDF_EXPORT bool asdf_value_is_sequence(asdf_value_t *value); + +/** + * Return the number of items in ``sequence`` + * + * :param sequence: The `asdf_sequence_t *` to query + * :return: The number of items currently in the sequence + */ ASDF_EXPORT int asdf_sequence_size(asdf_sequence_t *sequence); + +/** + * Return the value at ``index`` in ``sequence`` + * + * Negative indices are supported (e.g. ``-1`` for the last item). Returns + * ``NULL`` if ``index`` is out of range. + * + * :param sequence: The `asdf_sequence_t *` to query + * :param index: Zero-based index; negative indices count from the end + * :return: The `asdf_value_t *` at that position, or ``NULL`` + */ ASDF_EXPORT asdf_value_t *asdf_sequence_get(asdf_sequence_t *sequence, int index); + +/** + * Obtain a typed `asdf_sequence_t *` view of a generic value + * + * On success writes the `asdf_sequence_t *` into ``*out`` and returns + * ``ASDF_VALUE_OK``. Returns an error code and leaves ``*out`` unchanged if + * ``value`` is not a sequence. + * + * :param value: The generic `asdf_value_t *` to inspect + * :param out: Receives the `asdf_sequence_t *` on success + * :return: ``ASDF_VALUE_OK`` on success, otherwise an `asdf_value_err_t` error + */ ASDF_EXPORT asdf_value_err_t asdf_value_as_sequence(asdf_value_t *value, asdf_sequence_t **out); + +/** + * Return the generic `asdf_value_t *` view of a sequence + * + * The returned pointer shares ownership with ``sequence``; the caller must + * not free it independently. + * + * :param sequence: The `asdf_sequence_t *` to wrap + * :return: The same object as a generic `asdf_value_t *` + */ ASDF_EXPORT asdf_value_t *asdf_value_of_sequence(asdf_sequence_t *sequence); + +/** + * Create a new, empty sequence attached to ``file`` + * + * The caller owns the returned sequence and must eventually release it with + * `asdf_sequence_destroy`, or transfer ownership by inserting it into a + * mapping or parent sequence. Returns ``NULL`` on allocation failure. + * + * :param file: The `asdf_file_t *` that will own the sequence + * :return: A new `asdf_sequence_t *`, or ``NULL`` on failure + */ ASDF_EXPORT asdf_sequence_t *asdf_sequence_create(asdf_file_t *file); + +/** + * Set the YAML node style used when serializing ``sequence`` + * + * :param sequence: The `asdf_sequence_t *` to modify + * :param style: The desired `asdf_yaml_node_style_t` (e.g. block or flow) + */ ASDF_EXPORT void asdf_sequence_set_style(asdf_sequence_t *sequence, asdf_yaml_node_style_t style); + +/** + * Free a sequence and all values it contains + * + * Must not be called on a sequence that has been inserted into a mapping or + * parent sequence -- ownership transfers at that point. + * + * :param sequence: The `asdf_sequence_t *` to free + */ ASDF_EXPORT void asdf_sequence_destroy(asdf_sequence_t *sequence); /** Opaque struct holding sequence iterator state */ typedef void *asdf_sequence_iter_t; +/** + * Initialize an `asdf_sequence_iter_t` to its starting state + * + * Call this once before the first call to `asdf_sequence_iter`. + * + * :return: An initialized `asdf_sequence_iter_t` + */ ASDF_EXPORT asdf_sequence_iter_t asdf_sequence_iter_init(void); /** - * Iterate over a sequence value + * Advance a sequence iterator and return the next value * - * .. todo:: + * Typical usage:: * - * Finish documenting me. + * asdf_sequence_iter_t iter = asdf_sequence_iter_init(); + * asdf_value_t *val; + * while ((val = asdf_sequence_iter(seq, &iter)) != NULL) { + * // use val ... + * } + * + * Returns ``NULL`` when iteration is exhausted. + * + * :param sequence: The `asdf_sequence_t *` to iterate over + * :param iter: Pointer to the iterator state; updated on each call + * :return: The next `asdf_value_t *` in the sequence, or ``NULL`` when done */ ASDF_EXPORT asdf_value_t *asdf_sequence_iter(asdf_sequence_t *sequence, asdf_sequence_iter_t *iter); /** - * Append values to sequences + * Append a value to a sequence * - * .. todo:: + * ``asdf_sequence_append`` appends an existing generic `asdf_value_t *`. + * Ownership of ``value`` transfers to ``sequence`` on success. + * + * The ``asdf_sequence_append_`` variants construct a new value from a C + * scalar and append it in one step. ``asdf_sequence_append_string`` takes an + * explicit byte length; ``asdf_sequence_append_string0`` expects a + * NUL-terminated string. ``asdf_sequence_append_null`` takes no value + * argument. All other variants accept the corresponding C type directly. * - * Document these. + * ``asdf_sequence_append_mapping`` and ``asdf_sequence_append_sequence`` + * transfer ownership of the supplied container to the parent sequence on + * success. + * + * All functions return ``ASDF_VALUE_OK`` on success or an `asdf_value_err_t` + * error code on failure. */ ASDF_EXPORT asdf_value_err_t asdf_sequence_append(asdf_sequence_t *sequence, asdf_value_t *value); ASDF_EXPORT asdf_value_err_t @@ -467,6 +564,56 @@ ASDF_EXPORT asdf_value_err_t asdf_sequence_append_sequence(asdf_sequence_t *sequence, asdf_sequence_t *value); +/** + * Create a new sequence pre-populated from a C array in a single call + * + * Each ``asdf_sequence_of_`` function allocates a new sequence attached + * to ``file`` and appends ``size`` elements from ``arr``. On success the + * caller owns the returned sequence and must eventually release it with + * `asdf_sequence_destroy` (or transfer ownership by inserting it into a + * mapping or parent sequence). Returns ``NULL`` on allocation failure. + * + * ``asdf_sequence_of_null`` creates a sequence of ``size`` null values; it + * takes no array argument. + * + * ``asdf_sequence_of_string`` accepts an array of ``(str, len)`` pairs via + * separate ``arr`` and ``lens`` pointer arguments. + * ``asdf_sequence_of_string0`` is the null-terminated-string variant. + * + * All other variants mirror the corresponding ``asdf_sequence_append_`` + * scalar types. + */ +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_null(asdf_file_t *file, int size); + +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_string( + asdf_file_t *file, const char *const *arr, const size_t *lens, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_string0( + asdf_file_t *file, const char *const *arr, int size); + +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_bool(asdf_file_t *file, const bool *arr, int size); + +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_int8(asdf_file_t *file, const int8_t *arr, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_int16( + asdf_file_t *file, const int16_t *arr, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_int32( + asdf_file_t *file, const int32_t *arr, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_int64( + asdf_file_t *file, const int64_t *arr, int size); + +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_uint8( + asdf_file_t *file, const uint8_t *arr, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_uint16( + asdf_file_t *file, const uint16_t *arr, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_uint32( + asdf_file_t *file, const uint32_t *arr, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_uint64( + asdf_file_t *file, const uint64_t *arr, int size); + +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_float(asdf_file_t *file, const float *arr, int size); +ASDF_EXPORT asdf_sequence_t *asdf_sequence_of_double( + asdf_file_t *file, const double *arr, int size); + + /** * Remove a value from a sequence and return the removed value * diff --git a/include/asdf/yaml.h b/include/asdf/yaml.h index bf0383fa..3d0d24e4 100644 --- a/include/asdf/yaml.h +++ b/include/asdf/yaml.h @@ -1,3 +1,14 @@ +/** + * YAML-specific types and utilities + * + * This header provides types and accessor functions that relate specifically to + * the YAML layer of ASDF. It covers node style hints for serialization, the + * YAML event types surfaced through the event-based API, and functions for + * extracting YAML-specific fields from `asdf_event_t` objects. + */ + +// + #ifndef ASDF_YAML_H #define ASDF_YAML_H @@ -7,38 +18,104 @@ ASDF_BEGIN_DECLS +/** + * YAML node style hint used when serializing mappings and sequences + */ typedef enum { + /** Let libfyaml choose the style */ ASDF_YAML_NODE_STYLE_AUTO = 0, + /** Emit inline using ``{...}`` / ``[...]`` notation */ ASDF_YAML_NODE_STYLE_FLOW, + /** Emit in indented block notation */ ASDF_YAML_NODE_STYLE_BLOCK } asdf_yaml_node_style_t; +/** + * YAML event types returned inside an `asdf_event_t` of type + * ``ASDF_YAML_EVENT`` + * + * These mirror the libfyaml event model and are extracted via + * `asdf_yaml_event_type`. + */ typedef enum { - // Not a YAML event + /** Sentinel: the enclosing `asdf_event_t` does not carry a YAML sub-event */ ASDF_YAML_NONE_EVENT = 0, + /** Start of the YAML stream */ ASDF_YAML_STREAM_START_EVENT, + /** End of the YAML stream */ ASDF_YAML_STREAM_END_EVENT, + /** Start of a YAML document (``---``) */ ASDF_YAML_DOCUMENT_START_EVENT, + /** End of a YAML document (``...``) */ ASDF_YAML_DOCUMENT_END_EVENT, + /** Start of a YAML mapping node */ ASDF_YAML_MAPPING_START_EVENT, + /** End of a YAML mapping node */ ASDF_YAML_MAPPING_END_EVENT, + /** Start of a YAML sequence node */ ASDF_YAML_SEQUENCE_START_EVENT, + /** End of a YAML sequence node */ ASDF_YAML_SEQUENCE_END_EVENT, + /** A YAML scalar node */ ASDF_YAML_SCALAR_EVENT, + /** A YAML alias node (reference to an anchor) */ ASDF_YAML_ALIAS_EVENT } asdf_yaml_event_type_t; +/** + * A YAML tag directive associating a handle (e.g. ``"!!"`` ) with a prefix + * (e.g. ``"tag:yaml.org,2002:"`` ) + */ typedef struct { const char *handle; const char *prefix; } asdf_yaml_tag_handle_t; +/** + * Return the raw scalar string from a ``ASDF_YAML_SCALAR_EVENT`` + * + * The string is owned by ``event`` and must not be freed by the caller. + * Returns ``NULL`` if ``event`` does not carry a scalar sub-event. + * + * :param event: The `asdf_event_t *` to query + * :param lenp: If non-NULL, receives the byte length of the returned string + * :return: Pointer to the scalar value, or ``NULL`` + */ ASDF_EXPORT const char *asdf_yaml_event_scalar_value(const asdf_event_t *event, size_t *lenp); + +/** + * Return the YAML tag string from a YAML event, if any + * + * The string is owned by ``event`` and must not be freed by the caller. + * Returns ``NULL`` if the event carries no tag. + * + * :param event: The `asdf_event_t *` to query + * :param lenp: If non-NULL, receives the byte length of the returned string + * :return: Pointer to the tag string, or ``NULL`` + */ ASDF_EXPORT const char *asdf_yaml_event_tag(const asdf_event_t *event, size_t *lenp); + +/** + * Return the `asdf_yaml_event_type_t` of the YAML sub-event carried by + * ``event`` + * + * Returns ``ASDF_YAML_NONE_EVENT`` if ``event`` is not of type + * ``ASDF_YAML_EVENT``. + * + * :param event: The `asdf_event_t *` to query + * :return: The `asdf_yaml_event_type_t` of the YAML sub-event + */ ASDF_EXPORT asdf_yaml_event_type_t asdf_yaml_event_type(const asdf_event_t *event); + +/** + * Return a human-readable name for the YAML event type of ``event`` + * + * :param event: The `asdf_event_t *` to query + * :return: A static string such as ``"ASDF_YAML_SCALAR_EVENT"`` + */ ASDF_EXPORT const char *asdf_yaml_event_type_text(const asdf_event_t *event); ASDF_END_DECLS diff --git a/src/core/ndarray.c b/src/core/ndarray.c index 5ca6111e..ea8e8d2f 100644 --- a/src/core/ndarray.c +++ b/src/core/ndarray.c @@ -631,6 +631,40 @@ void *asdf_ndarray_data_alloc(asdf_ndarray_t *ndarray) { } +static void ndarray_write_data_cleanup(void *userdata) { + asdf_ndarray_internal_t *internal = userdata; + free(internal->data); + free(internal); +} + + +void *asdf_ndarray_data_alloc_temp(asdf_file_t *file, asdf_ndarray_t *ndarray) { + if (UNLIKELY(!file || !ndarray)) + return NULL; + + asdf_ndarray_internal_t *internal = calloc(1, sizeof(asdf_ndarray_internal_t)); + + if (UNLIKELY(!internal)) { + ASDF_ERROR_OOM(file); + return NULL; + } + + uint64_t nbytes = asdf_ndarray_nbytes(ndarray); + void *data = calloc(1, (size_t)nbytes); + + if (UNLIKELY(!data)) { + free(internal); + ASDF_ERROR_OOM(file); + return NULL; + } + + internal->data = data; + ndarray->internal = internal; + asdf_file_write_cleanup_add(file, ndarray_write_data_cleanup, internal); + return data; +} + + void asdf_ndarray_data_dealloc(asdf_ndarray_t *ndarray) { if (UNLIKELY(!ndarray)) return; diff --git a/src/event.c b/src/event.c index 850a4bf4..f4ef68b7 100644 --- a/src/event.c +++ b/src/event.c @@ -319,8 +319,8 @@ void asdf_event_free(asdf_parser_t *parser, asdf_event_t *event) { if (!event) return; - struct asdf_event_p - *event_p = (struct asdf_event_p *)((char *)event - offsetof(struct asdf_event_p, event)); + struct asdf_event_p *event_p = (struct asdf_event_p *)((char *)event - + offsetof(struct asdf_event_p, event)); asdf_event_cleanup(parser, &event_p->event); free(event_p); parser->current_event_p = NULL; diff --git a/src/file.c b/src/file.c index 7a16866a..dfa83cdc 100644 --- a/src/file.c +++ b/src/file.c @@ -265,6 +265,35 @@ asdf_file_t *asdf_open_mem_ex(const void *buf, size_t size, asdf_config_t *confi } +void asdf_file_write_cleanup_add(asdf_file_t *file, void (*callback)(void *), void *data) { + asdf_write_cleanup_t *node = calloc(1, sizeof(asdf_write_cleanup_t)); + + if (UNLIKELY(!node)) { + ASDF_ERROR_OOM(file); + return; + } + + node->callback = callback; + node->data = data; + node->next = file->write_cleanups; + file->write_cleanups = node; +} + + +void asdf_file_run_write_cleanups(asdf_file_t *file) { + asdf_write_cleanup_t *node = file->write_cleanups; + + while (node) { + asdf_write_cleanup_t *next = node->next; + node->callback(node->data); + free(node); + node = next; + } + + file->write_cleanups = NULL; +} + + int asdf_write_to_file(asdf_file_t *file, const char *filename) { if (UNLIKELY(!file)) return -1; @@ -286,6 +315,7 @@ int asdf_write_to_file(asdf_file_t *file, const char *filename) { ret = 0; cleanup: + asdf_file_run_write_cleanups(file); asdf_emitter_destroy(emitter); file->emitter = NULL; return ret; @@ -313,6 +343,7 @@ int asdf_write_to_fp(asdf_file_t *file, FILE *fp) { ret = 0; cleanup: + asdf_file_run_write_cleanups(file); asdf_emitter_destroy(emitter); file->emitter = NULL; return ret; @@ -436,6 +467,7 @@ int asdf_write_to_mem(asdf_file_t *file, void **buf, size_t *size) { *size = alloc_size; } cleanup: + asdf_file_run_write_cleanups(file); asdf_emitter_destroy(emitter); file->emitter = NULL; return ret; @@ -446,6 +478,7 @@ void asdf_close(asdf_file_t *file) { if (!file) return; + asdf_file_run_write_cleanups(file); fy_document_destroy(file->tree); asdf_emitter_destroy(file->emitter); asdf_parser_destroy(file->parser); diff --git a/src/file.h b/src/file.h index eeef0cab..a0f1236e 100644 --- a/src/file.h +++ b/src/file.h @@ -44,6 +44,13 @@ typedef enum { #define ASDF_FILE_TAG_MAP_DEFAULT_SIZE 20 +typedef struct asdf_write_cleanup { + void (*callback)(void *); + void *data; + struct asdf_write_cleanup *next; +} asdf_write_cleanup_t; + + typedef struct asdf_file { asdf_base_t base; asdf_config_t *config; @@ -80,12 +87,21 @@ typedef struct asdf_file { * metadata on output */ asdf_history_entry_t **history_entries; + /** Linked list of cleanup callbacks to run after each write */ + asdf_write_cleanup_t *write_cleanups; } asdf_file_t; /** Internal helper to get the `struct fy_document` for the tree, if any */ ASDF_LOCAL struct fy_document *asdf_file_tree_document(asdf_file_t *file); +/** Internal helper to register a cleanup callback to run after each write */ +ASDF_LOCAL void asdf_file_write_cleanup_add( + asdf_file_t *file, void (*callback)(void *), void *data); + +/** Internal helper to run and free all registered write cleanup callbacks */ +ASDF_LOCAL void asdf_file_run_write_cleanups(asdf_file_t *file); + /** Internal helper to set and/or retrieve a normalized tag */ ASDF_LOCAL const char *asdf_file_tag_normalize(asdf_file_t *file, const char *tag); diff --git a/src/gwcs/fitswcs_imaging.c b/src/gwcs/fitswcs_imaging.c index 997360fe..c7732a14 100644 --- a/src/gwcs/fitswcs_imaging.c +++ b/src/gwcs/fitswcs_imaging.c @@ -183,6 +183,150 @@ static asdf_value_err_t asdf_gwcs_fits_deserialize( } +/** + * Serialize a 1D float64 ndarray of 2 elements from a plain double[2] array. + * + * Returns the serialized value, or NULL on error. + */ +static asdf_value_t *serialize_double2_ndarray(asdf_file_t *file, const double src[2]) { + uint64_t shape[1] = {2}; + asdf_ndarray_t ndarray = { + .ndim = 1, + .shape = shape, + .datatype = {.type = ASDF_DATATYPE_FLOAT64}, + .byteorder = ASDF_BYTEORDER_LITTLE, + }; + + void *data = asdf_ndarray_data_alloc_temp(file, &ndarray); + + if (!data) + return NULL; + + memcpy(data, src, 2 * sizeof(double)); + return asdf_value_of_ndarray(file, &ndarray); +} + + +static asdf_value_t *asdf_gwcs_fits_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_fits_t *fits = obj; + asdf_mapping_t *map = NULL; + asdf_value_t *value = NULL; + asdf_value_t *val = NULL; + asdf_value_err_t err = ASDF_VALUE_ERR_EMIT_FAILURE; + + map = asdf_mapping_create(file); + + if (!map) + goto cleanup; + + err = asdf_gwcs_transform_serialize_base(file, &fits->base, map); + + if (ASDF_IS_ERR(err)) + goto cleanup; + + // crpix -- 1D float64 ndarray shape [2] + val = serialize_double2_ndarray(file, fits->crpix); + + if (!val) { + err = ASDF_VALUE_ERR_OOM; + goto cleanup; + } + + err = asdf_mapping_set(map, "crpix", val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(val); + goto cleanup; + } + + // crval -- 1D float64 ndarray shape [2] + val = serialize_double2_ndarray(file, fits->crval); + + if (!val) { + err = ASDF_VALUE_ERR_OOM; + goto cleanup; + } + + err = asdf_mapping_set(map, "crval", val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(val); + goto cleanup; + } + + // cdelt -- 1D float64 ndarray shape [2] + val = serialize_double2_ndarray(file, fits->cdelt); + + if (!val) { + err = ASDF_VALUE_ERR_OOM; + goto cleanup; + } + + err = asdf_mapping_set(map, "cdelt", val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(val); + goto cleanup; + } + + // pc -- 2D float64 ndarray shape [2, 2] + uint64_t pc_shape[2] = {2, 2}; + asdf_ndarray_t pc_ndarray = { + .ndim = 2, + .shape = pc_shape, + .datatype = {.type = ASDF_DATATYPE_FLOAT64}, + .byteorder = ASDF_BYTEORDER_LITTLE, + }; + void *pc_data = asdf_ndarray_data_alloc_temp(file, &pc_ndarray); + + if (!pc_data) { + err = ASDF_VALUE_ERR_OOM; + goto cleanup; + } + + memcpy(pc_data, fits->pc, 4 * sizeof(double)); + val = asdf_value_of_ndarray(file, &pc_ndarray); + + if (!val) { + err = ASDF_VALUE_ERR_EMIT_FAILURE; + goto cleanup; + } + + err = asdf_mapping_set(map, "pc", val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(val); + goto cleanup; + } + + // projection -- generic transform tagged with its projection type + val = asdf_gwcs_transform_value_of(file, &fits->projection); + + if (!val) { + err = ASDF_VALUE_ERR_EMIT_FAILURE; + goto cleanup; + } + + err = asdf_mapping_set(map, "projection", val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(val); + goto cleanup; + } + + value = asdf_value_of_mapping(map); + map = NULL; // owned by value + +cleanup: + asdf_mapping_destroy(map); + return value; +} + + static void asdf_gwcs_fits_dealloc(void *value) { if (!value) return; @@ -201,7 +345,7 @@ ASDF_REGISTER_EXTENSION( ASDF_GWCS_TAG_PREFIX "fitswcs_imaging-1.0.0", asdf_gwcs_fits_t, &libasdf_software, - NULL, + asdf_gwcs_fits_serialize, asdf_gwcs_fits_deserialize, NULL, /* TODO: copy */ asdf_gwcs_fits_dealloc, diff --git a/src/gwcs/frame.c b/src/gwcs/frame.c index dac4bc05..7b9735c5 100644 --- a/src/gwcs/frame.c +++ b/src/gwcs/frame.c @@ -65,6 +65,12 @@ static asdf_value_err_t get_frame_axes_string_param( if (!ASDF_IS_OPTIONAL_OK(err)) goto cleanup; + if (!frames_seq) { + // Property is absent (optional), nothing to parse + err = ASDF_VALUE_OK; + goto cleanup; + } + uint32_t size = (uint32_t)asdf_sequence_size(frames_seq); if (size < min_axes || size > max_axes) { @@ -104,6 +110,12 @@ static asdf_value_err_t get_frame_axes_order_param( if (!ASDF_IS_OPTIONAL_OK(err)) goto cleanup; + if (!axes_seq) { + // Property is absent (optional), nothing to parse + err = ASDF_VALUE_OK; + goto cleanup; + } + uint32_t size = (uint32_t)asdf_sequence_size(axes_seq); if (size < min_axes || size > max_axes) { @@ -181,6 +193,82 @@ asdf_value_err_t asdf_gwcs_frame_parse( } +asdf_value_err_t asdf_gwcs_frame_serialize_common( + asdf_file_t *file, + const char *name, + uint32_t naxes, + const char *const *axes_names, + const uint32_t *axes_order, + const char *const *unit, + const char *const *axis_physical_types, + asdf_mapping_t *map) { + asdf_value_err_t err; + + err = asdf_mapping_set_string0(map, "name", name ? name : ""); + + if (ASDF_IS_ERR(err)) + return err; + + if (naxes > 0 && axes_names && axes_names[0]) { + asdf_sequence_t *seq = asdf_sequence_of_string0(file, axes_names, (int)naxes); + + if (!seq) + return ASDF_VALUE_ERR_OOM; + + err = asdf_mapping_set_sequence(map, "axes_names", seq); + + if (ASDF_IS_ERR(err)) { + asdf_sequence_destroy(seq); + return err; + } + } + + if (naxes > 0 && axes_order) { + asdf_sequence_t *seq = asdf_sequence_of_uint32(file, axes_order, (int)naxes); + + if (!seq) + return ASDF_VALUE_ERR_OOM; + + err = asdf_mapping_set_sequence(map, "axes_order", seq); + + if (ASDF_IS_ERR(err)) { + asdf_sequence_destroy(seq); + return err; + } + } + + if (naxes > 0 && unit && unit[0]) { + asdf_sequence_t *seq = asdf_sequence_of_string0(file, unit, (int)naxes); + + if (!seq) + return ASDF_VALUE_ERR_OOM; + + err = asdf_mapping_set_sequence(map, "unit", seq); + + if (ASDF_IS_ERR(err)) { + asdf_sequence_destroy(seq); + return err; + } + } + + if (naxes > 0 && axis_physical_types && axis_physical_types[0]) { + asdf_sequence_t *seq = asdf_sequence_of_string0(file, axis_physical_types, (int)naxes); + + if (!seq) + return ASDF_VALUE_ERR_OOM; + + err = asdf_mapping_set_sequence(map, "axis_physical_types", seq); + + if (ASDF_IS_ERR(err)) { + asdf_sequence_destroy(seq); + return err; + } + } + + return ASDF_VALUE_OK; +} + + static asdf_value_err_t asdf_gwcs_base_frame_deserialize( asdf_value_t *value, UNUSED(const void *userdata), void **out) { asdf_gwcs_frame_t *frame = NULL; @@ -206,6 +294,29 @@ static asdf_value_err_t asdf_gwcs_base_frame_deserialize( } +static asdf_value_t *asdf_gwcs_base_frame_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_base_frame_t *frame = obj; + asdf_mapping_t *map = asdf_mapping_create(file); + + if (!map) + return NULL; + + asdf_value_err_t err = asdf_gwcs_frame_serialize_common( + file, frame->name, 0, NULL, NULL, NULL, NULL, map); + + if (ASDF_IS_ERR(err)) { + asdf_mapping_destroy(map); + return NULL; + } + + return asdf_value_of_mapping(map); +} + + static void asdf_gwcs_base_frame_dealloc(void *value) { if (!value) return; @@ -243,6 +354,22 @@ asdf_value_err_t asdf_value_as_gwcs_frame(asdf_value_t *value, asdf_gwcs_frame_t } +asdf_value_t *asdf_gwcs_frame_value_of(asdf_file_t *file, const asdf_gwcs_frame_t *frame) { + if (!frame) + return NULL; + + switch (frame->type) { + case ASDF_GWCS_FRAME_2D: + return asdf_value_of_gwcs_frame2d(file, (const asdf_gwcs_frame2d_t *)frame); + case ASDF_GWCS_FRAME_CELESTIAL: + return asdf_value_of_gwcs_frame_celestial(file, (const asdf_gwcs_frame_celestial_t *)frame); + case ASDF_GWCS_FRAME_GENERIC: + default: + return asdf_value_of_gwcs_base_frame(file, (const asdf_gwcs_base_frame_t *)frame); + } +} + + // Generic destructor for frames of different types from a value, depending on the tag void asdf_gwcs_frame_destroy(asdf_gwcs_frame_t *frame) { if (!frame) @@ -267,7 +394,7 @@ ASDF_REGISTER_EXTENSION( ASDF_GWCS_TAG_PREFIX "frame-1.2.0", asdf_gwcs_base_frame_t, &libasdf_software, - NULL, + asdf_gwcs_base_frame_serialize, asdf_gwcs_base_frame_deserialize, NULL, /* TODO: copy */ asdf_gwcs_base_frame_dealloc, diff --git a/src/gwcs/frame.h b/src/gwcs/frame.h index 6c85c101..9226817f 100644 --- a/src/gwcs/frame.h +++ b/src/gwcs/frame.h @@ -28,3 +28,26 @@ typedef struct { */ ASDF_LOCAL asdf_value_err_t asdf_gwcs_frame_parse( asdf_value_t *value, asdf_gwcs_frame_t *frame, asdf_gwcs_frame_common_params_t *params); + +/** + * Common serialization helper for all frame types + * + * Writes name, axes_names, axes_order, unit, and axis_physical_types into + * the supplied mapping. Pass naxes=0 and NULL arrays for a bare frame. + */ +ASDF_LOCAL asdf_value_err_t asdf_gwcs_frame_serialize_common( + asdf_file_t *file, + const char *name, + uint32_t naxes, + const char *const *axes_names, + const uint32_t *axes_order, + const char *const *unit, + const char *const *axis_physical_types, + asdf_mapping_t *map); + +/** + * Polymorphic value constructor: dispatches to the appropriate typed + * asdf_value_of_gwcs_frame* function based on frame->type. + */ +ASDF_LOCAL asdf_value_t *asdf_gwcs_frame_value_of( + asdf_file_t *file, const asdf_gwcs_frame_t *frame); diff --git a/src/gwcs/frame2d.c b/src/gwcs/frame2d.c index 8c050dc0..d9cfc2cf 100644 --- a/src/gwcs/frame2d.c +++ b/src/gwcs/frame2d.c @@ -1,3 +1,4 @@ +#include "../extension_util.h" #include "../util.h" #include "../value.h" @@ -37,6 +38,36 @@ static asdf_value_err_t asdf_gwcs_frame2d_deserialize( } +static asdf_value_t *asdf_gwcs_frame2d_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_frame2d_t *frame2d = obj; + asdf_mapping_t *map = asdf_mapping_create(file); + + if (!map) + return NULL; + + asdf_value_err_t err = asdf_gwcs_frame_serialize_common( + file, + frame2d->base.name, + 2, + frame2d->axes_names, + frame2d->axes_order, + frame2d->unit, + frame2d->axis_physical_types, + map); + + if (ASDF_IS_ERR(err)) { + asdf_mapping_destroy(map); + return NULL; + } + + return asdf_value_of_mapping(map); +} + + static void asdf_gwcs_frame2d_dealloc(void *value) { if (!value) return; @@ -51,7 +82,7 @@ ASDF_REGISTER_EXTENSION( ASDF_GWCS_TAG_PREFIX "frame2d-1.2.0", asdf_gwcs_frame2d_t, &libasdf_software, - NULL, + asdf_gwcs_frame2d_serialize, asdf_gwcs_frame2d_deserialize, NULL, /* TODO: copy */ asdf_gwcs_frame2d_dealloc, diff --git a/src/gwcs/frame_celestial.c b/src/gwcs/frame_celestial.c index 87fb4bbc..b6fdc767 100644 --- a/src/gwcs/frame_celestial.c +++ b/src/gwcs/frame_celestial.c @@ -1,3 +1,4 @@ +#include "../extension_util.h" #include "../util.h" #include "../value.h" @@ -38,6 +39,46 @@ static asdf_value_err_t asdf_gwcs_frame_celestial_deserialize( } +static asdf_value_t *asdf_gwcs_frame_celestial_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_frame_celestial_t *frame_celestial = obj; + + // Count actual axes (may be 2 or 3) + uint32_t naxes = 0; + + while (naxes < 3 && frame_celestial->axes_names[naxes]) + naxes++; + + if (naxes < 2) + naxes = 2; + + asdf_mapping_t *map = asdf_mapping_create(file); + + if (!map) + return NULL; + + asdf_value_err_t err = asdf_gwcs_frame_serialize_common( + file, + frame_celestial->base.name, + naxes, + frame_celestial->axes_names, + frame_celestial->axes_order, + frame_celestial->unit, + frame_celestial->axis_physical_types, + map); + + if (ASDF_IS_ERR(err)) { + asdf_mapping_destroy(map); + return NULL; + } + + return asdf_value_of_mapping(map); +} + + static void asdf_gwcs_frame_celestial_dealloc(void *value) { if (!value) return; @@ -52,7 +93,7 @@ ASDF_REGISTER_EXTENSION( ASDF_GWCS_TAG_PREFIX "celestial_frame-1.2.0", asdf_gwcs_frame_celestial_t, &libasdf_software, - NULL, + asdf_gwcs_frame_celestial_serialize, asdf_gwcs_frame_celestial_deserialize, NULL, /* TODO: copy */ asdf_gwcs_frame_celestial_dealloc, diff --git a/src/gwcs/step.c b/src/gwcs/step.c index 9bc04290..7f7bd6b7 100644 --- a/src/gwcs/step.c +++ b/src/gwcs/step.c @@ -90,6 +90,63 @@ static asdf_value_err_t asdf_gwcs_step_deserialize( } +static asdf_value_t *asdf_gwcs_step_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_step_t *step = obj; + asdf_mapping_t *map = NULL; + asdf_value_t *value = NULL; + asdf_value_err_t err = ASDF_VALUE_ERR_EMIT_FAILURE; + + map = asdf_mapping_create(file); + + if (!map) + goto cleanup; + + // frame is required + asdf_value_t *frame_val = asdf_gwcs_frame_value_of(file, step->frame); + + if (!frame_val) + goto cleanup; + + err = asdf_mapping_set(map, "frame", frame_val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(frame_val); + goto cleanup; + } + + // transform is optional; null means no transform (last step in wcs) + if (step->transform) { + asdf_value_t *transform_val = asdf_gwcs_transform_value_of(file, step->transform); + + if (!transform_val) + goto cleanup; + + err = asdf_mapping_set(map, "transform", transform_val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(transform_val); + goto cleanup; + } + } else { + err = asdf_mapping_set_null(map, "transform"); + + if (ASDF_IS_ERR(err)) + goto cleanup; + } + + value = asdf_value_of_mapping(map); + map = NULL; // owned by value + +cleanup: + asdf_mapping_destroy(map); + return value; +} + + static void asdf_gwcs_step_dealloc(void *value) { if (!value) return; @@ -112,7 +169,7 @@ ASDF_REGISTER_EXTENSION( ASDF_GWCS_TAG_PREFIX "step-1.3.0", asdf_gwcs_step_t, &libasdf_software, - NULL, + asdf_gwcs_step_serialize, asdf_gwcs_step_deserialize, NULL, /* TODO: copy */ asdf_gwcs_step_dealloc, diff --git a/src/gwcs/transform.c b/src/gwcs/transform.c index 250211be..fa36e89e 100644 --- a/src/gwcs/transform.c +++ b/src/gwcs/transform.c @@ -141,6 +141,135 @@ asdf_value_err_t asdf_value_as_gwcs_transform(asdf_value_t *value, asdf_gwcs_tra } +/** + * Reverse lookup: transform type enum → tag string (without version). + * + * The entries intentionally mirror the forward map in + * `asdf_gwcs_transform_map_create` so that a round-trip through serialize / + * deserialize succeeds. + */ +static const char *const transform_type_to_tag_map[ASDF_GWCS_TRANSFORM_LAST] = { + [ASDF_GWCS_TRANSFORM_GENERIC] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "transform", + [ASDF_GWCS_TRANSFORM_AIRY] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "airy", + [ASDF_GWCS_TRANSFORM_BONNE_EQUAL_AREA] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "bonne_equal_area", + [ASDF_GWCS_TRANSFORM_COBE_QUAD_SPHERICAL_CUBE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "cobe_quad_spherical_cube", + [ASDF_GWCS_TRANSFORM_CONIC_EQUAL_AREA] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "conic_equal_area", + [ASDF_GWCS_TRANSFORM_CONIC_EQUIDISTANT] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "conic_equidistant", + [ASDF_GWCS_TRANSFORM_CONIC_ORTHOMORPHIC] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "conic_orthomorphic", + [ASDF_GWCS_TRANSFORM_CONIC_PERSPECTIVE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "conic_perspective", + [ASDF_GWCS_TRANSFORM_CYLINDRICAL_EQUAL_AREA] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "cylindrical_equal_area", + [ASDF_GWCS_TRANSFORM_CYLINDRICAL_PERSPECTIVE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "cylindrical_perspective", + [ASDF_GWCS_TRANSFORM_FITSWCS_IMAGING] = ASDF_GWCS_TAG_PREFIX "fitswcs_imaging", + [ASDF_GWCS_TRANSFORM_GNOMONIC] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "gnomonic", + [ASDF_GWCS_TRANSFORM_HAMMER_AITOFF] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "hammer_aitoff", + [ASDF_GWCS_TRANSFORM_HEALPIX_POLAR] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "healpix_polar", + [ASDF_GWCS_TRANSFORM_MOLLEWEIDE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "molleweide", + [ASDF_GWCS_TRANSFORM_PARABOLIC] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "parabolic", + [ASDF_GWCS_TRANSFORM_PLATE_CARREE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "plate_carree", + [ASDF_GWCS_TRANSFORM_POLYCONIC] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "polyconic", + [ASDF_GWCS_TRANSFORM_SANSON_FLAMSTEED] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "sanson_flamsteed", + [ASDF_GWCS_TRANSFORM_SLANT_ORTHOGRAPHIC] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "slant_orthographic", + [ASDF_GWCS_TRANSFORM_STEREOGRAPHIC] = ASDF_GWCS_TRANSFORM_TAG_PREFIX "stereographic", + [ASDF_GWCS_TRANSFORM_QUAD_SPHERICAL_CUBE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "quad_spherical_cube", + [ASDF_GWCS_TRANSFORM_SLANT_ZENITHAL_PERSPECTIVE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "slant_zenithal_perspective", + [ASDF_GWCS_TRANSFORM_TANGENTIAL_SPHERICAL_CUBE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "tangential_spherical_cube", + [ASDF_GWCS_TRANSFORM_ZENITHAL_EQUAL_AREA] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "zenithal_equal_area", + [ASDF_GWCS_TRANSFORM_ZENITHAL_EQUIDISTANT] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "zenithal_equidistant", + [ASDF_GWCS_TRANSFORM_ZENITHAL_PERSPECTIVE] = ASDF_GWCS_TRANSFORM_TAG_PREFIX + "zenithal_perspective", +}; + + +const char *asdf_gwcs_transform_type_to_tag(asdf_gwcs_transform_type_t type) { + if (type < 0 || (unsigned int)type >= ASDF_GWCS_TRANSFORM_LAST) + return NULL; + + return transform_type_to_tag_map[type]; +} + + +asdf_value_err_t asdf_gwcs_transform_serialize_base( + asdf_file_t *file, const asdf_gwcs_transform_t *transform, asdf_mapping_t *map) { + asdf_value_err_t err = ASDF_VALUE_OK; + + if (transform->name) { + err = asdf_mapping_set_string0(map, "name", transform->name); + + if (ASDF_IS_ERR(err)) + return err; + } + + if (transform->bounding_box) { + asdf_value_t *bb_val = asdf_value_of_gwcs_bounding_box(file, transform->bounding_box); + + if (!bb_val) + return ASDF_VALUE_ERR_EMIT_FAILURE; + + err = asdf_mapping_set(map, "bounding_box", bb_val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(bb_val); + return err; + } + } + + return ASDF_VALUE_OK; +} + + +static asdf_value_t *asdf_gwcs_generic_transform_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_transform_t *transform = obj; + asdf_mapping_t *map = asdf_mapping_create(file); + + if (!map) + return NULL; + + asdf_value_err_t err = asdf_gwcs_transform_serialize_base(file, transform, map); + + if (ASDF_IS_ERR(err)) { + asdf_mapping_destroy(map); + return NULL; + } + + return asdf_value_of_mapping(map); +} + + +asdf_value_t *asdf_gwcs_transform_value_of( + asdf_file_t *file, const asdf_gwcs_transform_t *transform) { + if (!transform) + return NULL; + + if (transform->type == ASDF_GWCS_TRANSFORM_FITSWCS_IMAGING) + return asdf_value_of_gwcs_fits(file, (const asdf_gwcs_fits_t *)transform); + + const char *tag = asdf_gwcs_transform_type_to_tag(transform->type); + + if (!tag) + return NULL; + + asdf_extension_t tmp_ext = { + .tag = tag, + .software = &libasdf_software, + .serialize = asdf_gwcs_generic_transform_serialize, + }; + + return asdf_value_of_extension_type(file, transform, &tmp_ext); +} + + /** * Hard-coded mapping between known transform tags and their associated * `asdf_gwcs_transform_type_t` value diff --git a/src/gwcs/transform.h b/src/gwcs/transform.h index 7c290bce..d140f9ef 100644 --- a/src/gwcs/transform.h +++ b/src/gwcs/transform.h @@ -12,6 +12,26 @@ ASDF_LOCAL asdf_value_err_t asdf_gwcs_transform_parse(asdf_value_t *value, asdf_gwcs_transform_t *transform); +/** + * Return the tag string (without version) for a given transform type, or + * NULL if the type is unknown. + */ +ASDF_LOCAL const char *asdf_gwcs_transform_type_to_tag(asdf_gwcs_transform_type_t type); + +/** + * Serialize the base transform fields (name, bounding_box) into an existing + * mapping. Called by type-specific serializers. + */ +ASDF_LOCAL asdf_value_err_t asdf_gwcs_transform_serialize_base( + asdf_file_t *file, const asdf_gwcs_transform_t *transform, asdf_mapping_t *map); + +/** + * Polymorphic value constructor: dispatches to asdf_value_of_gwcs_fits for + * fitswcs_imaging transforms, or uses a temporary extension for generic ones. + */ +ASDF_LOCAL asdf_value_t *asdf_gwcs_transform_value_of( + asdf_file_t *file, const asdf_gwcs_transform_t *transform); + /** * Release memory held by fields in an `asdf_gwcs_transform_t` and clear it in preparation * for releasing the transform's memory diff --git a/src/gwcs/transforms/property/bounding_box.c b/src/gwcs/transforms/property/bounding_box.c index 35cc744e..e9b15f00 100644 --- a/src/gwcs/transforms/property/bounding_box.c +++ b/src/gwcs/transforms/property/bounding_box.c @@ -120,6 +120,61 @@ static asdf_value_err_t asdf_gwcs_bounding_box_deserialize( } +static asdf_value_t *asdf_gwcs_bounding_box_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_bounding_box_t *bbox = obj; + asdf_mapping_t *bbox_map = NULL; + asdf_mapping_t *intervals_map = NULL; + asdf_value_t *value = NULL; + asdf_value_err_t err = ASDF_VALUE_ERR_EMIT_FAILURE; + + bbox_map = asdf_mapping_create(file); + if (UNLIKELY(!bbox_map)) + goto cleanup; + + intervals_map = asdf_mapping_create(file); + if (UNLIKELY(!intervals_map)) + goto cleanup; + + for (uint32_t idx = 0; idx < bbox->n_intervals; idx++) { + const asdf_gwcs_interval_t *interval = &bbox->intervals[idx]; + asdf_sequence_t *bounds_seq = asdf_sequence_of_double(file, interval->bounds, 2); + + if (!bounds_seq) { + err = ASDF_VALUE_ERR_OOM; + goto cleanup; + } + + asdf_sequence_set_style(bounds_seq, ASDF_YAML_NODE_STYLE_FLOW); + + err = asdf_mapping_set_sequence(intervals_map, interval->input_name, bounds_seq); + + if (ASDF_IS_ERR(err)) { + asdf_sequence_destroy(bounds_seq); + goto cleanup; + } + } + + err = asdf_mapping_set_mapping(bbox_map, "intervals", intervals_map); + + if (ASDF_IS_ERR(err)) + goto cleanup; + + intervals_map = NULL; // owned by bbox_map now + + value = asdf_value_of_mapping(bbox_map); + bbox_map = NULL; // owned by value + +cleanup: + asdf_mapping_destroy(intervals_map); + asdf_mapping_destroy(bbox_map); + return value; +} + + static void asdf_gwcs_bounding_box_dealloc(void *value) { if (!value) return; @@ -138,7 +193,7 @@ ASDF_REGISTER_EXTENSION( ASDF_GWCS_BOUNDING_BOX_TAG, asdf_gwcs_bounding_box_t, &libasdf_software, - NULL, + asdf_gwcs_bounding_box_serialize, asdf_gwcs_bounding_box_deserialize, NULL, /* TODO: copy */ asdf_gwcs_bounding_box_dealloc, diff --git a/src/gwcs/wcs.c b/src/gwcs/wcs.c index 2a6beb62..f3be74ce 100644 --- a/src/gwcs/wcs.c +++ b/src/gwcs/wcs.c @@ -19,6 +19,63 @@ static asdf_gwcs_err_t asdf_gwcs_finalize_fitswcs_imaging(asdf_file_t *file, asd } +static asdf_value_t *asdf_gwcs_serialize( + asdf_file_t *file, const void *obj, UNUSED(const void *userdata)) { + if (UNLIKELY(!file || !obj)) + return NULL; + + const asdf_gwcs_t *gwcs = obj; + asdf_mapping_t *map = NULL; + asdf_sequence_t *steps_seq = NULL; + asdf_value_t *value = NULL; + asdf_value_err_t err = ASDF_VALUE_ERR_EMIT_FAILURE; + + map = asdf_mapping_create(file); + + if (!map) + goto cleanup; + + err = asdf_mapping_set_string0(map, "name", gwcs->name ? gwcs->name : ""); + + if (ASDF_IS_ERR(err)) + goto cleanup; + + steps_seq = asdf_sequence_create(file); + + if (!steps_seq) + goto cleanup; + + for (uint32_t idx = 0; idx < gwcs->n_steps; idx++) { + asdf_value_t *step_val = asdf_value_of_gwcs_step(file, &gwcs->steps[idx]); + + if (!step_val) + goto cleanup; + + err = asdf_sequence_append(steps_seq, step_val); + + if (ASDF_IS_ERR(err)) { + asdf_value_destroy(step_val); + goto cleanup; + } + } + + err = asdf_mapping_set_sequence(map, "steps", steps_seq); + + if (ASDF_IS_ERR(err)) + goto cleanup; + + steps_seq = NULL; // owned by map + + value = asdf_value_of_mapping(map); + map = NULL; // owned by value + +cleanup: + asdf_sequence_destroy(steps_seq); + asdf_mapping_destroy(map); + return value; +} + + static asdf_value_err_t asdf_gwcs_deserialize( asdf_value_t *value, UNUSED(const void *userdata), void **out) { asdf_gwcs_t *gwcs = NULL; @@ -132,7 +189,7 @@ ASDF_REGISTER_EXTENSION( ASDF_GWCS_TAG_PREFIX "wcs-1.4.0", asdf_gwcs_t, &libasdf_software, - NULL, + asdf_gwcs_serialize, asdf_gwcs_deserialize, NULL, /* TODO: copy */ asdf_gwcs_dealloc, diff --git a/src/parse_util.c b/src/parse_util.c index e3f42dbf..7663390c 100644 --- a/src/parse_util.c +++ b/src/parse_util.c @@ -132,8 +132,8 @@ void asdf_parse_event_recycle(asdf_parser_t *parser, asdf_event_t *event) { if (!parser || !event) return; - struct asdf_event_p - *event_p = (struct asdf_event_p *)((char *)event - offsetof(struct asdf_event_p, event)); + struct asdf_event_p *event_p = (struct asdf_event_p *)((char *)event - + offsetof(struct asdf_event_p, event)); event_p->next = parser->event_freelist; parser->event_freelist = event_p; diff --git a/src/value.c b/src/value.c index cd106f27..3c4ee8f5 100644 --- a/src/value.c +++ b/src/value.c @@ -961,6 +961,134 @@ ASDF_SEQUENCE_APPEND_CONTAINER_TYPE(mapping); ASDF_SEQUENCE_APPEND_CONTAINER_TYPE(sequence); +/** Bulk sequence constructors (asdf_sequence_of_) */ + +/* + * Helper: allocate a new sequence and return both the asdf_sequence_t and its + * backing fy_document. Returns NULL on failure. + */ +static asdf_sequence_t *sequence_of_begin(asdf_file_t *file, struct fy_document **tree_out) { + asdf_sequence_t *seq = asdf_sequence_create(file); + + if (!seq) + return NULL; + + struct fy_document *tree = asdf_file_tree_document(file); + + if (!tree) { + asdf_sequence_destroy(seq); + return NULL; + } + + *tree_out = tree; + return seq; +} + + +asdf_sequence_t *asdf_sequence_of_null(asdf_file_t *file, int size) { + struct fy_document *tree = NULL; + asdf_sequence_t *seq = sequence_of_begin(file, &tree); + + if (!seq) + return NULL; + + for (int idx = 0; idx < size; idx++) { + struct fy_node *node = asdf_node_of_null(tree); + + if (!node || fy_node_sequence_append(seq->value.node, node) != 0) { + ASDF_ERROR_OOM(file); + asdf_sequence_destroy(seq); + return NULL; + } + } + + return seq; +} + + +asdf_sequence_t *asdf_sequence_of_string( + asdf_file_t *file, const char *const *arr, const size_t *lens, int size) { + struct fy_document *tree = NULL; + asdf_sequence_t *seq = sequence_of_begin(file, &tree); + + if (!seq) + return NULL; + + for (int idx = 0; idx < size; idx++) { + struct fy_node *node = asdf_node_of_string(tree, arr[idx], lens[idx]); + + if (!node || fy_node_sequence_append(seq->value.node, node) != 0) { + ASDF_ERROR_OOM(file); + asdf_sequence_destroy(seq); + return NULL; + } + } + + return seq; +} + + +asdf_sequence_t *asdf_sequence_of_string0(asdf_file_t *file, const char *const *arr, int size) { + struct fy_document *tree = NULL; + asdf_sequence_t *seq = sequence_of_begin(file, &tree); + + if (!seq) + return NULL; + + for (int idx = 0; idx < size; idx++) { + struct fy_node *node = asdf_node_of_string0(tree, arr[idx]); + + if (!node || fy_node_sequence_append(seq->value.node, node) != 0) { + ASDF_ERROR_OOM(file); + asdf_sequence_destroy(seq); + return NULL; + } + } + + return seq; +} + + +/* + * Macro to generate asdf_sequence_of_ for scalar types whose C type + * matches the name (bool, float, double). + */ +#define ASDF_SEQUENCE_OF_TYPE(type, ctype) \ + asdf_sequence_t *asdf_sequence_of_##type(asdf_file_t *file, const ctype *arr, int size) { \ + struct fy_document *tree = NULL; \ + asdf_sequence_t *seq = sequence_of_begin(file, &tree); \ + if (!seq) \ + return NULL; \ + for (int idx = 0; idx < size; idx++) { \ + struct fy_node *node = asdf_node_of_##type(tree, arr[idx]); \ + if (!node || fy_node_sequence_append(seq->value.node, node) != 0) { \ + ASDF_ERROR_OOM(file); \ + asdf_sequence_destroy(seq); \ + return NULL; \ + } \ + } \ + return seq; \ + } + +/* + * Variant for integer types whose C type has a _t suffix (int8_t, uint32_t...). + */ +#define ASDF_SEQUENCE_OF_INT_TYPE(type) ASDF_SEQUENCE_OF_TYPE(type, type##_t) + + +ASDF_SEQUENCE_OF_TYPE(bool, bool) +ASDF_SEQUENCE_OF_INT_TYPE(int8) +ASDF_SEQUENCE_OF_INT_TYPE(int16) +ASDF_SEQUENCE_OF_INT_TYPE(int32) +ASDF_SEQUENCE_OF_INT_TYPE(int64) +ASDF_SEQUENCE_OF_INT_TYPE(uint8) +ASDF_SEQUENCE_OF_INT_TYPE(uint16) +ASDF_SEQUENCE_OF_INT_TYPE(uint32) +ASDF_SEQUENCE_OF_INT_TYPE(uint64) +ASDF_SEQUENCE_OF_TYPE(float, float) +ASDF_SEQUENCE_OF_TYPE(double, double) + + asdf_value_t *asdf_sequence_pop(asdf_sequence_t *sequence, int index) { if (UNLIKELY(!sequence)) return NULL; diff --git a/tests/test-gwcs.c b/tests/test-gwcs.c index 9d25b4e4..b068f68a 100644 --- a/tests/test-gwcs.c +++ b/tests/test-gwcs.c @@ -7,6 +7,171 @@ #include "util.h" +/** + * Assert that a deserialized asdf_gwcs_fits_t matches the expected values used + * in the write tests (crpix/crval/cdelt/pc/projection). + */ +static void check_fits_values(const asdf_gwcs_fits_t *fits_out) { + double crpix[2] = {12099.5, -88700.5}; + double crval[2] = {270.0, 64.60237301}; + double cdelt[2] = {1.52777778e-05, 1.52777778e-05}; + double pc[2][2] = {{1.0, 0.0}, {-0.0, 1.0}}; + + for (int idx = 0; idx < 2; idx++) { + assert_double_equal(fits_out->crpix[idx], crpix[idx], 5); + assert_double_equal(fits_out->crval[idx], crval[idx], 5); + assert_double_equal(fits_out->cdelt[idx], cdelt[idx], 5); + for (int jdx = 0; jdx < 2; jdx++) { + assert_double_equal(fits_out->pc[idx][jdx], pc[idx][jdx], 5); + } + } + + assert_int(fits_out->projection.type, ==, ASDF_GWCS_TRANSFORM_GNOMONIC); +} + + +MU_TEST(test_asdf_set_gwcs_fits) { + const char *path = get_temp_file_path(fixture->tempfile_prefix, ".asdf"); + asdf_file_t *file = asdf_open(NULL); + assert_not_null(file); + + asdf_gwcs_fits_t fits = { + .base = {.type = ASDF_GWCS_TRANSFORM_FITSWCS_IMAGING}, + .crpix = {12099.5, -88700.5}, + .crval = {270.0, 64.60237301}, + .cdelt = {1.52777778e-05, 1.52777778e-05}, + .pc = {{1.0, 0.0}, {-0.0, 1.0}}, + .projection = {.type = ASDF_GWCS_TRANSFORM_GNOMONIC}, + }; + + assert_int(asdf_set_gwcs_fits(file, "transform", &fits), ==, ASDF_VALUE_OK); + assert_int(asdf_write_to(file, path), ==, 0); + asdf_close(file); + + // Re-open and validate the round-tripped data + file = asdf_open(path, "r"); + assert_not_null(file); + + asdf_gwcs_fits_t *fits_out = NULL; + asdf_value_err_t err = asdf_get_gwcs_fits(file, "transform", &fits_out); + assert_int(err, ==, ASDF_VALUE_OK); + assert_not_null(fits_out); + + asdf_gwcs_transform_t *transform = (asdf_gwcs_transform_t *)fits_out; + assert_int(transform->type, ==, ASDF_GWCS_TRANSFORM_FITSWCS_IMAGING); + assert_null(transform->name); + assert_null(transform->bounding_box); + + check_fits_values(fits_out); + + // ctype is NULL when reading fits without the full gwcs context + assert_null(fits_out->ctype[0]); + assert_null(fits_out->ctype[1]); + + asdf_gwcs_fits_destroy(fits_out); + asdf_close(file); + return MUNIT_OK; +} + + +MU_TEST(test_asdf_set_gwcs) { + const char *path = get_temp_file_path(fixture->tempfile_prefix, ".asdf"); + asdf_file_t *file = asdf_open(NULL); + assert_not_null(file); + + asdf_gwcs_frame2d_t detector_frame = { + .base = {.type = ASDF_GWCS_FRAME_2D, .name = "detector"}, + .axes_names = {"x", "y"}, + .axes_order = {0, 1}, + .axis_physical_types = {"custom:x", "custom:y"}, + }; + + asdf_gwcs_fits_t fits = { + .base = {.type = ASDF_GWCS_TRANSFORM_FITSWCS_IMAGING}, + .crpix = {12099.5, -88700.5}, + .crval = {270.0, 64.60237301}, + .cdelt = {1.52777778e-05, 1.52777778e-05}, + .pc = {{1.0, 0.0}, {-0.0, 1.0}}, + .projection = {.type = ASDF_GWCS_TRANSFORM_GNOMONIC}, + }; + + asdf_gwcs_frame_celestial_t icrs_frame = { + .base = {.type = ASDF_GWCS_FRAME_CELESTIAL, .name = "icrs"}, + .axes_names = {"lon", "lat", NULL}, + .axes_order = {0, 1, 0}, + .axis_physical_types = {"pos.eq.ra", "pos.eq.dec", NULL}, + }; + + asdf_gwcs_step_t steps[2] = { + {.frame = (asdf_gwcs_frame_t *)&detector_frame, + .transform = (const asdf_gwcs_transform_t *)&fits}, + {.frame = (asdf_gwcs_frame_t *)&icrs_frame, .transform = NULL}, + }; + + asdf_gwcs_t gwcs = { + .name = "test_wcs", + .n_steps = 2, + .steps = steps, + }; + + assert_int(asdf_set_gwcs(file, "wcs", &gwcs), ==, ASDF_VALUE_OK); + assert_int(asdf_write_to(file, path), ==, 0); + asdf_close(file); + + // Re-open and validate the round-tripped data + file = asdf_open(path, "r"); + assert_not_null(file); + + asdf_gwcs_t *gwcs_out = NULL; + asdf_value_err_t err = asdf_get_gwcs(file, "wcs", &gwcs_out); + assert_int(err, ==, ASDF_VALUE_OK); + assert_not_null(gwcs_out); + + assert_string_equal(gwcs_out->name, "test_wcs"); + assert_int(gwcs_out->n_steps, ==, 2); + assert_not_null(gwcs_out->steps); + + const asdf_gwcs_step_t *step = &gwcs_out->steps[0]; + assert_not_null(step->frame); + assert_int(step->frame->type, ==, ASDF_GWCS_FRAME_2D); + assert_string_equal(step->frame->name, "detector"); + asdf_gwcs_frame2d_t *frame2d_out = (asdf_gwcs_frame2d_t *)step->frame; + assert_string_equal(frame2d_out->axes_names[0], "x"); + assert_string_equal(frame2d_out->axes_names[1], "y"); + assert_string_equal(frame2d_out->axis_physical_types[0], "custom:x"); + assert_string_equal(frame2d_out->axis_physical_types[1], "custom:y"); + assert_int(frame2d_out->axes_order[0], ==, 0); + assert_int(frame2d_out->axes_order[1], ==, 1); + assert_not_null(step->transform); + assert_int(step->transform->type, ==, ASDF_GWCS_TRANSFORM_FITSWCS_IMAGING); + + asdf_gwcs_fits_t *fits_out = (asdf_gwcs_fits_t *)step->transform; + check_fits_values(fits_out); + + // ctype should be filled in since step[1] has a celestial frame with + // recognized axis_physical_types + assert_string_equal(fits_out->ctype[0], "RA---TAN"); + assert_string_equal(fits_out->ctype[1], "DEC--TAN"); + + step = &gwcs_out->steps[1]; + assert_not_null(step->frame); + assert_int(step->frame->type, ==, ASDF_GWCS_FRAME_CELESTIAL); + assert_string_equal(step->frame->name, "icrs"); + asdf_gwcs_frame_celestial_t *frame_celestial_out = (asdf_gwcs_frame_celestial_t *)step->frame; + assert_string_equal(frame_celestial_out->axes_names[0], "lon"); + assert_string_equal(frame_celestial_out->axes_names[1], "lat"); + assert_string_equal(frame_celestial_out->axis_physical_types[0], "pos.eq.ra"); + assert_string_equal(frame_celestial_out->axis_physical_types[1], "pos.eq.dec"); + assert_int(frame_celestial_out->axes_order[0], ==, 0); + assert_int(frame_celestial_out->axes_order[1], ==, 1); + assert_null(step->transform); + + asdf_gwcs_destroy(gwcs_out); + asdf_close(file); + return MUNIT_OK; +} + + MU_TEST(test_asdf_get_gwcs_fits) { const char *path = get_fixture_file_path("roman_wcs.asdf"); asdf_file_t *file = asdf_open(path, "r"); @@ -111,7 +276,9 @@ MU_TEST(test_asdf_get_gwcs) { MU_TEST_SUITE( gwcs, MU_RUN_TEST(test_asdf_get_gwcs_fits), - MU_RUN_TEST(test_asdf_get_gwcs) + MU_RUN_TEST(test_asdf_get_gwcs), + MU_RUN_TEST(test_asdf_set_gwcs_fits), + MU_RUN_TEST(test_asdf_set_gwcs) );