diff --git a/libdispatch/dinfermodel.c b/libdispatch/dinfermodel.c index d17e5bff45..87d618fed8 100644 --- a/libdispatch/dinfermodel.c +++ b/libdispatch/dinfermodel.c @@ -1573,6 +1573,7 @@ NC_interpret_magic_number(char* magic, NCmodel* model) { int formatx = (i <= 1) ? (NC_FORMATX_UDF0 + i) : (NC_FORMATX_UDF2 + i - 2); model->impl = formatx; + model->format = NC_FORMAT_NETCDF4; status = NC_NOERR; break; } diff --git a/ncdump/CMakeLists.txt b/ncdump/CMakeLists.txt index 923919deb1..2ec85581c3 100644 --- a/ncdump/CMakeLists.txt +++ b/ncdump/CMakeLists.txt @@ -208,6 +208,20 @@ endif() # Add this test by hand, as it is also called from a script. # Editing the script would break autotools compatibility. build_bin_test_no_prefix(tst_special_atts) + if(NETCDF_ENABLE_PLUGINS) + add_library(tst_udf_format_plugin MODULE tst_udf_format_plugin.c) + target_link_libraries(tst_udf_format_plugin netcdf ${ALL_TLL_LIBS}) + set_target_properties(tst_udf_format_plugin PROPERTIES PREFIX "") + if(WIN32) + set_target_properties(tst_udf_format_plugin PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR} + LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR} + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR} + RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR}) + endif() + endif() endif(USE_HDF5) # Base tests @@ -262,6 +276,9 @@ endif() if(USE_HDF5) add_sh_test(ncdump tst_formatx4) + if(NETCDF_ENABLE_PLUGINS) + add_sh_test(ncdump tst_udf_format) + endif() add_sh_test(ncdump_sh tst_fillbug) add_sh_test(ncdump_shell tst_h_scalar) add_sh_test(ncdump tst_mud) diff --git a/ncdump/Makefile.am b/ncdump/Makefile.am index bde8d7bef1..b2910cc9db 100644 --- a/ncdump/Makefile.am +++ b/ncdump/Makefile.am @@ -122,15 +122,26 @@ tst_vlen_data tst_comp tst_comp2 tst_nans tst_special_atts \ tst_unicode tst_fillbug tst_compress tst_chunking tst_h_scalar \ tst_enum_undef +if NETCDF_ENABLE_PLUGINS +check_LTLIBRARIES = tst_udf_format_plugin.la +tst_udf_format_plugin_la_SOURCES = tst_udf_format_plugin.c +tst_udf_format_plugin_la_LDFLAGS = -module -avoid-version -shared -no-undefined -rpath /tmp +tst_udf_format_plugin_la_LIBADD = ${top_builddir}/liblib/libnetcdf.la +endif # NETCDF_ENABLE_PLUGINS + check_PROGRAMS += tst_vlen_demo # Tests for netCDF-4 behavior. TESTS += tst_fileinfo.sh tst_hdf5_offset.sh tst_inttags4.sh \ -tst_netcdf4.sh tst_fillbug.sh \ -tst_grp_spec.sh tst_mud.sh tst_h_scalar.sh tst_formatx4.sh \ +tst_netcdf4.sh tst_fillbug.sh tst_grp_spec.sh tst_mud.sh \ +tst_h_scalar.sh tst_formatx4.sh \ run_utf8_nc4_tests.sh run_ncgen_nc4_tests.sh \ tst_ncgen4.sh test_scope.sh +if NETCDF_ENABLE_PLUGINS +TESTS += tst_udf_format.sh +endif # NETCDF_ENABLE_PLUGINS + # Record interscript dependencies so parallel builds work. tst_nccopy4.log: run_ncgen_tests.log tst_output.log tst_ncgen4.log \ tst_fillbug.log tst_netcdf4_4.log tst_h_scalar.log tst_netcdf4.log @@ -220,7 +231,8 @@ ref_rcapi.txt ref_tst_enum_undef.cdl tst_calendars_nc4.cdl ref_times_nc4.cdl ref EXTRA_DIST += L512.bin EXTRA_DIST += tst_ctests.sh ref_ctest_small_3.c ref_ctest_small_4.c \ - ref_ctest_special_atts_4.c test_ncdump.sh + ref_ctest_special_atts_4.c test_ncdump.sh tst_udf_format.sh \ + tst_udf_format_plugin.c EXTRA_DIST += testpathcvt.sh ref_pathcvt.txt @@ -255,7 +267,8 @@ tst_roman_szip_unlim.cdl tst_perdimpspecs.nc tmppds.* \ keyword1.nc keyword2.nc keyword3.nc keyword4.nc \ tmp_keyword1.cdl tmp_keyword2.cdl tmp_keyword3.cdl tmp_keyword4.cdl \ type_*.nc copy_type_*.cdl \ -scope_*.nc copy_scope_*.cdl keyword5.nc tst_enum_undef.cdl tst_times_nc4.cdl +scope_*.nc copy_scope_*.cdl keyword5.nc tst_enum_undef.cdl tst_times_nc4.cdl \ + tst_udf_format.ncrc tst_udf_format.udf tst_udf_format.out tst_udf_format.err # Remove directories clean-local: diff --git a/ncdump/ncdump.c b/ncdump/ncdump.c index ab3d8d4dfc..f98e3766a2 100644 --- a/ncdump/ncdump.c +++ b/ncdump/ncdump.c @@ -276,7 +276,10 @@ kind_string(int kind) case NC_FORMAT_NETCDF4_CLASSIC: return "netCDF-4 classic model"; default: - error("unrecognized file format: %d", kind); + if(kind == NC_FORMATX_UDF0 || kind == NC_FORMATX_UDF1 + || (kind >= NC_FORMATX_UDF2 && kind <= NC_FORMATX_UDF9)) + return "user-defined format"; + error("unrecognized file format: %d", kind); return "unrecognized"; } } @@ -315,8 +318,13 @@ kind_string_extended(int kind, int mode) snprintf(text,sizeof(text),"%s mode=%08x", "unknown",mode); break; default: - error("unrecognized extended format: %d",kind); - snprintf(text,sizeof(text),"%s mode=%08x", "unrecognized",mode); + if(kind == NC_FORMATX_UDF0 || kind == NC_FORMATX_UDF1 + || (kind >= NC_FORMATX_UDF2 && kind <= NC_FORMATX_UDF9)) + snprintf(text,sizeof(text),"%s mode=%08x", "user-defined format",mode); + else { + error("unrecognized extended format: %d",kind); + snprintf(text,sizeof(text),"%s mode=%08x", "unrecognized",mode); + } break; } return text; diff --git a/ncdump/tst_udf_format.sh b/ncdump/tst_udf_format.sh new file mode 100755 index 0000000000..68479436de --- /dev/null +++ b/ncdump/tst_udf_format.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# +# This is part of the netCDF package. Copyright 2026 University +# Corporation for Atmospheric Research/Unidata. See COPYRIGHT file +# for conditions of use. +# +# Test for netCDF-C issue #3411. ncdump could open a UDF file and +# print its metadata, but then rejected the UDF extended format code +# while generating the virtual _Format attribute or processing -K. +# +# The test autoloads tst_udf_format_plugin through an RC file and opens +# a fixture recognized by its UDF magic number. It verifies that -hs +# prints the generic user-defined _Format value and -K prints the +# generic extended format name with its mode. +# +# Author: Edward Hartnett, 2026-07-21 + +if test "x$srcdir" = x ; then srcdir=`pwd`; fi +. ../test_common.sh + +for directory in . .libs; do + for extension in so dylib dll; do + if test -f $directory/tst_udf_format_plugin.$extension; then + plugin=`pwd`/$directory/tst_udf_format_plugin.$extension + break 2 + fi + done +done + +if test -z "$plugin" || test ! -f "$plugin"; then + echo "*** Fail: UDF format test plugin was not built" + echo "Working directory: `pwd`" + echo "Plugin candidates:" + find . -maxdepth 2 -type f -name 'tst_udf_format_plugin*' -print + echo "Current directory contents:" + ls -la + if test -d .libs; then + echo ".libs contents:" + ls -la .libs + fi + exit 1 +fi + +cat > tst_udf_format.ncrc < tst_udf_format.udf + +if ! NCRCENV_RC=`pwd`/tst_udf_format.ncrc ${NCDUMP} -hs tst_udf_format.udf > tst_udf_format.out 2> tst_udf_format.err; then + cat tst_udf_format.err + rm -f tst_udf_format.ncrc tst_udf_format.udf tst_udf_format.out tst_udf_format.err + exit 1 +fi + +if ! grep ':_Format = "user-defined format" ;' tst_udf_format.out; then + cat tst_udf_format.out + rm -f tst_udf_format.ncrc tst_udf_format.udf tst_udf_format.out tst_udf_format.err + exit 1 +fi + +if ! NCRCENV_RC=`pwd`/tst_udf_format.ncrc ${NCDUMP} -K tst_udf_format.udf > tst_udf_format.out 2> tst_udf_format.err; then + cat tst_udf_format.err + rm -f tst_udf_format.ncrc tst_udf_format.udf tst_udf_format.out tst_udf_format.err + exit 1 +fi + +if ! grep 'user-defined format mode=' tst_udf_format.out; then + cat tst_udf_format.out + rm -f tst_udf_format.ncrc tst_udf_format.udf tst_udf_format.out tst_udf_format.err + exit 1 +fi + +rm -f tst_udf_format.ncrc tst_udf_format.udf tst_udf_format.out tst_udf_format.err diff --git a/ncdump/tst_udf_format_plugin.c b/ncdump/tst_udf_format_plugin.c new file mode 100644 index 0000000000..df63629302 --- /dev/null +++ b/ncdump/tst_udf_format_plugin.c @@ -0,0 +1,169 @@ +/* This is part of the netCDF package. Copyright 2026 University + Corporation for Atmospheric Research/Unidata. See COPYRIGHT file + for conditions of use. + + Minimal self-registering UDF0 plugin for tst_udf_format.sh. It + provides an empty root dataset and reports NC_FORMATX_UDF0 through + both format inquiry functions. + + This plugin reproduces netCDF-C issue #3411 without depending on + an external UDF implementation. Before the fix, ncdump could read + the UDF dataset but rejected extended format code 8 while generating + its virtual _Format attribute or processing -K output. + + Author: Edward Hartnett, 2026-07-21 +*/ + +#include "config.h" +#include +#include "netcdf.h" +#include "netcdf_dispatch.h" + +static NC_Dispatch dispatcher; + +static int +plugin_open(const char *path, int mode, int basepe, size_t *chunksizehintp, + void *parameters, const NC_Dispatch *table, int ncid) +{ + return NC_NOERR; +} + +static int +plugin_close(int ncid, void *dispatchdata) +{ + return NC_NOERR; +} + +static int +plugin_inq_format(int ncid, int *formatp) +{ + if (formatp) + *formatp = NC_FORMATX_UDF0; + return NC_NOERR; +} + +static int +plugin_inq_format_extended(int ncid, int *formatp, int *modep) +{ + if (formatp) + *formatp = NC_FORMATX_UDF0; + if (modep) + *modep = NC_NOWRITE; + return NC_NOERR; +} + +static int +plugin_inq(int ncid, int *ndimsp, int *nvarsp, int *nattsp, int *unlimdimidp) +{ + if (ndimsp) + *ndimsp = 0; + if (nvarsp) + *nvarsp = 0; + if (nattsp) + *nattsp = 0; + if (unlimdimidp) + *unlimdimidp = -1; + return NC_NOERR; +} + +static int +plugin_inq_att(int ncid, int varid, const char *name, nc_type *xtypep, + size_t *lenp) +{ + return NC_ENOTATT; +} + +static int +plugin_inq_typeids(int ncid, int *ntypes, int *typeids) +{ + if (ntypes) + *ntypes = 0; + return NC_NOERR; +} + +static int +plugin_inq_grps(int ncid, int *numgrps, int *ncids) +{ + if (numgrps) + *numgrps = 0; + return NC_NOERR; +} + +static int +plugin_inq_grp_parent(int ncid, int *parent_ncid) +{ + return NC_ENOGRP; +} + +static int +plugin_inq_ncid(int ncid, const char *name, int *grp_ncid) +{ + if (name != NULL) + return NC_ENOGRP; + if (grp_ncid) + *grp_ncid = ncid; + return NC_NOERR; +} + +static int +plugin_inq_dimids(int ncid, int *ndims, int *dimids, int include_parents) +{ + if (ndims) + *ndims = 0; + return NC_NOERR; +} + +static int +plugin_inq_unlimdims(int ncid, int *nunlimdimsp, int *unlimdimidsp) +{ + if (nunlimdimsp) + *nunlimdimsp = 0; + return NC_NOERR; +} + +#if defined(_MSC_VER) +#define PLUGIN_EXPORT __declspec(dllexport) +#elif defined(__GNUC__) && __GNUC__ >= 4 +#define PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define PLUGIN_EXPORT +#endif + +PLUGIN_EXPORT NC_Dispatch * +tst_udf_format_init(void) +{ + memset(&dispatcher, 0, sizeof(dispatcher)); + dispatcher.model = NC_FORMATX_UDF0; + dispatcher.dispatch_version = NC_DISPATCH_VERSION; + dispatcher.create = NC_RO_create; + dispatcher.open = plugin_open; + dispatcher.redef = NC_RO_redef; + dispatcher._enddef = NC_RO__enddef; + dispatcher.sync = NC_RO_sync; + dispatcher.close = plugin_close; + dispatcher.set_fill = NC_RO_set_fill; + dispatcher.inq_format = plugin_inq_format; + dispatcher.inq_format_extended = plugin_inq_format_extended; + dispatcher.inq = plugin_inq; + dispatcher.inq_att = plugin_inq_att; + dispatcher.inq_grps = plugin_inq_grps; + dispatcher.inq_ncid = plugin_inq_ncid; + dispatcher.inq_grp_parent = plugin_inq_grp_parent; + dispatcher.inq_dimids = plugin_inq_dimids; + dispatcher.inq_unlimdims = plugin_inq_unlimdims; + dispatcher.inq_typeids = plugin_inq_typeids; + dispatcher.rename_att = NC_RO_rename_att; + dispatcher.del_att = NC_RO_del_att; + dispatcher.put_att = NC_RO_put_att; + dispatcher.def_dim = NC_RO_def_dim; + dispatcher.rename_dim = NC_RO_rename_dim; + dispatcher.def_var = NC_RO_def_var; + dispatcher.rename_var = NC_RO_rename_var; + dispatcher.put_vara = NC_RO_put_vara; + dispatcher.var_par_access = NC_NOTNC4_var_par_access; + dispatcher.def_var_fill = NC_RO_def_var_fill; + dispatcher.inq_var_filter_ids = NC_NOOP_inq_var_filter_ids; + dispatcher.inq_var_filter_info = NC_NOOP_inq_var_filter_info; + dispatcher.inq_filter_avail = NC_NOOP_inq_filter_avail; + return &dispatcher; +} diff --git a/unit_test/CMakeLists.txt b/unit_test/CMakeLists.txt index d07237b090..1d49117aef 100644 --- a/unit_test/CMakeLists.txt +++ b/unit_test/CMakeLists.txt @@ -19,6 +19,7 @@ ENDIF() SET(UNIT_TESTS test_ncuri) add_bin_test(unit_test test_ncuri) +add_bin_test(unit_test tst_udf_infermodel) SET(UNIT_TESTS test_dauth) add_bin_test(unit_test test_dauth) diff --git a/unit_test/Makefile.am b/unit_test/Makefile.am index 7448c603f6..8884010eb6 100644 --- a/unit_test/Makefile.am +++ b/unit_test/Makefile.am @@ -27,8 +27,8 @@ noinst_PROGRAMS = noinst_PROGRAMS += ncpluginpath ncpluginpath_SOURCES = ncpluginpath.c -check_PROGRAMS += tst_nclist test_ncuri test_pathcvt test_dauth -TESTS += tst_nclist test_ncuri run_pathcvt.sh test_dauth +check_PROGRAMS += tst_nclist test_ncuri test_pathcvt test_dauth tst_udf_infermodel +TESTS += tst_nclist test_ncuri run_pathcvt.sh test_dauth tst_udf_infermodel # Performance tests if BUILD_BENCHMARKS @@ -75,7 +75,7 @@ EXTRA_DIST += ref_get.txt ref_set.txt EXTRA_DIST += ref_xget.txt ref_xset.txt EXTRA_DIST += ref_provparse.txt EXTRA_DIST += reclaim_tests.baseline -CLEANFILES = reclaim_tests*.txt reclaim_tests.nc reclaim_tests.dmp tmp_*.txt +CLEANFILES = reclaim_tests*.txt reclaim_tests.nc reclaim_tests.dmp tmp_*.txt tst_udf_infermodel.udf # Remove directories clean-local: diff --git a/unit_test/tst_udf_infermodel.c b/unit_test/tst_udf_infermodel.c new file mode 100644 index 0000000000..a32518fb34 --- /dev/null +++ b/unit_test/tst_udf_infermodel.c @@ -0,0 +1,57 @@ +/* This is part of the netCDF package. Copyright 2026 University + Corporation for Atmospheric Research/Unidata. See COPYRIGHT file + for conditions of use. + + Test for netCDF-C issue #3410. A UDF magic number could identify + a UDF implementation while leaving NCmodel.format unset. Callers + need both fields: implementation selects the UDF dispatch table and + format records the data model and supplies the corresponding mode. + + The test begins with a preselected UDF implementation and a zero + format, then uses a UDF magic number to complete inference. It + verifies that the UDF is retained, NC_FORMAT_NETCDF4 is assigned, + and NC_NETCDF4 is returned in the open mode. + + Author: Edward Hartnett, 2026-07-21 +*/ + +#include "config.h" +#include +#include +#include +#include "err_macros.h" +#include "ncdispatch.h" + +#define FILE_NAME "tst_udf_infermodel.udf" + +int +main(int argc, char **argv) +{ + NC_Dispatch dispatch; + NCmodel model; + FILE *file; + int omode = NC_NOWRITE; + + printf("\n*** Testing UDF magic format inference.\n"); + printf("*** testing UDF magic establishes the enhanced data model..."); + + memset(&dispatch, 0, sizeof(dispatch)); + dispatch.model = NC_FORMATX_UDF0; + dispatch.dispatch_version = NC_DISPATCH_VERSION; + if (nc_def_user_format(NC_UDF0, &dispatch, "TINF")) ERR; + + if (!(file = fopen(FILE_NAME, "wb"))) ERR; + if (fwrite("TINF00000", 1, 9, file) != 9) ERR; + if (fclose(file)) ERR; + + memset(&model, 0, sizeof(model)); + model.impl = NC_FORMATX_UDF0; + if (NC_infermodel(FILE_NAME, &omode, 0, 0, NULL, &model, NULL)) ERR; + if (model.impl != NC_FORMATX_UDF0) ERR; + if (model.format != NC_FORMAT_NETCDF4) ERR; + if (!(omode & NC_NETCDF4)) ERR; + + remove(FILE_NAME); + SUMMARIZE_ERR; + FINAL_RESULTS; +}