From 7137488b43b0b95093372f3265a7960c374b2ed1 Mon Sep 17 00:00:00 2001 From: Nathan Rusch Date: Fri, 18 Sep 2026 19:27:57 -0700 Subject: [PATCH 1/3] python: Avoid exceptions when decoding non-UTF-8 data from string attrs Signed-off-by: Nathan Rusch --- src/python/py_imagespec.cpp | 6 +++--- src/python/py_oiio.cpp | 2 +- src/python/py_oiio.h | 31 +++++++++++++++++++++++++++++++ src/python/py_paramvalue.cpp | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/python/py_imagespec.cpp b/src/python/py_imagespec.cpp index 3e1643c135..8bf22e96a2 100644 --- a/src/python/py_imagespec.cpp +++ b/src/python/py_imagespec.cpp @@ -159,7 +159,7 @@ declare_imagespec(py_module& m) int chan) { return spec.channelformat(chan); }) .def("channel_name", [](const ImageSpec& spec, int chan) { - return oiio_py::str(spec.channel_name(chan)); + return py_str_escaped(spec.channel_name(chan)); }) .def("channelindex", [](const ImageSpec& spec, const std::string& name) { @@ -200,7 +200,7 @@ declare_imagespec(py_module& m) "get_string_attribute", [](const ImageSpec& spec, const std::string& name, const std::string& def) { - return oiio_py::str(spec.get_string_attribute(name, def)); + return py_str_escaped(spec.get_string_attribute(name, def)); }, "name"_a, "defaultval"_a = "") .def( @@ -233,7 +233,7 @@ declare_imagespec(py_module& m) .def_static( "metadata_val", [](const ParamValue& p, bool human) { - return oiio_py::str(ImageSpec::metadata_val(p, human)); + return py_str_escaped(ImageSpec::metadata_val(p, human)); }, "param"_a, "human"_a = false) .def( diff --git a/src/python/py_oiio.cpp b/src/python/py_oiio.cpp index 84ad07e3fd..62107b78f4 100644 --- a/src/python/py_oiio.cpp +++ b/src/python/py_oiio.cpp @@ -512,7 +512,7 @@ declare_global_attribute_functions(py_module& m) m.def( "get_string_attribute", [](const std::string& name, const std::string& def) { - return oiio_py::str(OIIO::get_string_attribute(name, def)); + return py_str_escaped(OIIO::get_string_attribute(name, def)); }, "name"_a, "defaultval"_a = ""); m.def("getattribute", &oiio_getattribute_typed, "name"_a, diff --git a/src/python/py_oiio.h b/src/python/py_oiio.h index cc2139b6e4..932fed9466 100644 --- a/src/python/py_oiio.h +++ b/src/python/py_oiio.h @@ -469,6 +469,28 @@ py_to_stdvector(std::vector& vals, const py::object& obj) } +// Make a UTF-8 `py::str` from a string-like C++ type, using `'surrogateescape'` +// error handling for any non-UTF-8 byte sequences. Compared to calling +// `py::str` directly, this avoids raising a `UnicodeDecodeError` if the input +// is not valid UTF-8. +inline py::str +py_str_escaped(string_view value) +{ + py::handle py_str = PyUnicode_DecodeUTF8(value.data(), value.length(), + "surrogateescape"); +#if defined(OIIO_PY_BACKEND_NANOBIND) + if (!py_str) { + py::raise_python_error(); + } + return py::steal(py_str); +#else + if (!py_str) { + throw py::error_already_set(); + } + return py::reinterpret_steal(py_str); +#endif +} + template inline py::tuple @@ -477,6 +499,9 @@ C_to_tuple(cspan vals) return oiio_py::make_tuple(vals.size(), [&](size_t i) { if constexpr (std::is_same_v) { return py::cast(static_cast(vals[i])); + } else if constexpr (std::is_same_v, std::string> + || std::is_same_v, const char*>) { + return py_str_escaped(vals[i]); } else { return py::cast(vals[i]); } @@ -500,6 +525,9 @@ C_to_tuple(const T* vals, size_t size) return oiio_py::make_tuple(size, [&](size_t i) { if constexpr (std::is_same_v) { return py::cast(static_cast(vals[i])); + } else if constexpr (std::is_same_v, std::string> + || std::is_same_v, const char*>) { + return py_str_escaped(vals[i]); } else { return py::cast(vals[i]); } @@ -540,6 +568,9 @@ C_to_val_or_tuple(const T* vals, TypeDesc type, int nvalues = 1) if (n == 1 && !type.arraylen) { if constexpr (std::is_same_v) { return py::cast(static_cast(vals[0])); + } else if constexpr (std::is_same_v, std::string> + || std::is_same_v, const char*>) { + return py_str_escaped(vals[0]); } else { return py::cast(vals[0]); } diff --git a/src/python/py_paramvalue.cpp b/src/python/py_paramvalue.cpp index a38a2f6f2b..748a6247ca 100644 --- a/src/python/py_paramvalue.cpp +++ b/src/python/py_paramvalue.cpp @@ -158,7 +158,7 @@ declare_paramvalue(py_module& m) py::class_(m, "ParamValue") .OIIO_PY_PROP_RO("name", [](const ParamValue& self) { - return oiio_py::str(self.name().string()); + return py_str_escaped(self.name().string()); }) .OIIO_PY_PROP_RO("type", [](const ParamValue& self) { return self.type(); }) From ad68187bdb1d75254d194a364c69c5e62846f8f4 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Mon, 21 Sep 2026 11:49:39 -0700 Subject: [PATCH 2/3] Apply suggestion from @lgritz Signed-off-by: Larry Gritz --- src/python/py_oiio.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/python/py_oiio.h b/src/python/py_oiio.h index 932fed9466..1e47b50033 100644 --- a/src/python/py_oiio.h +++ b/src/python/py_oiio.h @@ -473,6 +473,11 @@ py_to_stdvector(std::vector& vals, const py::object& obj) // error handling for any non-UTF-8 byte sequences. Compared to calling // `py::str` directly, this avoids raising a `UnicodeDecodeError` if the input // is not valid UTF-8. +// +// It is important to do this for string return values that come from +// "untrusted" sources, such as being read from other files, so that we ensure +// that we are returning valid UTF-8 str's to the calling Python so that +// exceptions are not raised. inline py::str py_str_escaped(string_view value) { From 0212718090d405514b35003f3e908829e50bf841 Mon Sep 17 00:00:00 2001 From: Nathan Rusch Date: Mon, 21 Sep 2026 12:45:12 -0700 Subject: [PATCH 3/3] Apply safe string decoding to `ImageSpec.serialize`/`.to_xml` Signed-off-by: Nathan Rusch --- src/python/py_imagespec.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/py_imagespec.cpp b/src/python/py_imagespec.cpp index 8bf22e96a2..38524c05fe 100644 --- a/src/python/py_imagespec.cpp +++ b/src/python/py_imagespec.cpp @@ -250,11 +250,11 @@ declare_imagespec(py_module& m) verb = ImageSpec::SerialDetailed; else if (Strutil::iequals(verbose, "detailedhuman")) verb = ImageSpec::SerialDetailedHuman; - return oiio_py::str(spec.serialize(fmt, verb)); + return py_str_escaped(spec.serialize(fmt, verb)); }, "format"_a = "text", "verbose"_a = "detailed") .def("to_xml", - [](const ImageSpec& spec) { return oiio_py::str(spec.to_xml()); }) + [](const ImageSpec& spec) { return py_str_escaped(spec.to_xml()); }) .def("from_xml", [](ImageSpec& self, const std::string& xml) { self.from_xml(xml.c_str());