diff --git a/.gitignore b/.gitignore index 0d20b64..77de6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ *.pyc +build/ +*.so +*.o +*.a +*.egg-info/ +__pycache__/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cb46c7e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: c -sudo: true - -env: - global: - - CODECOV_TOKEN=4792f32d-1685-4e2d-8cc4-b82e9578a605 - -before_install: - - sudo apt-get update -qq - - sudo apt-get install python-libxml2 libxml2-dev python-dev - -script: - - sed 's/$(CC)/$(CC) -coverage/g' Makefile > t_makefile - - cp t_makefile Makefile - - rm -f t_makefile - - make GCOV=1 build - - make GCOV=1 unit - - find build/ -name '*.gcno' -exec mv {} ./ \; - - find build/ -name '*.gcda' -exec mv {} ./ \; - - make GCOV=1 dmidump - - sudo ./dmidump /dev/mem /dev/null - - make GCOV=1 version - -after_success: - - bash <(curl -s https://codecov.io/bash) -F unittest diff --git a/Makefile b/Makefile index 2f47a75..7bd4060 100644 --- a/Makefile +++ b/Makefile @@ -38,18 +38,21 @@ #. $AutoHeaderSerial::20100225 $ #. ******* AUTOHEADER END v1.2 ******* -PY_BIN := python3 +PY_BIN ?= python3 VERSION := $(shell cd src;$(PY_BIN) -c "from setup_common import *; print(get_version());") PACKAGE := python-dmidecode PY_VER := $(shell $(PY_BIN) -c 'import sys; print("%d.%d"%sys.version_info[0:2])') -PY_MV := $(shell echo $(PY_VER) | cut -b 1) -PY := python$(PY_VER) -SO_PATH := build/lib.linux-$(shell uname -m)-$(PY_VER) +PY_MV := $(shell $(PY_BIN) -c 'import sys; print(sys.version_info[0])') +PY_TAG := $(shell $(PY_BIN) -c 'import sys; print("python%d.%d"%sys.version_info[0:2])') + ifeq ($(PY_MV),2) - SO := $(SO_PATH)/dmidecodemod.so + PLATFORM := $(shell $(PY_BIN) -c 'from distutils.util import get_platform; print(get_platform())') + BUILD_LIB := build/lib.$(PLATFORM)-$(PY_VER) + SO := $(BUILD_LIB)/dmidecodemod.so else - SOABI := $(shell $(PY_BIN) -c 'import sysconfig; print(sysconfig.get_config_var("SOABI"))') - SO := $(SO_PATH)/dmidecodemod.$(SOABI).so + SOABI := $(shell $(PY_BIN) -c 'import sysconfig; print(sysconfig.get_config_var("SOABI") or "")') + BUILD_LIB := $(shell $(PY_BIN) -c 'import sys, sysconfig; print("build/lib.%s-%s" % (sysconfig.get_platform(), sys.implementation.cache_tag))') + SO := $(BUILD_LIB)/dmidecodemod.$(SOABI).so endif SHELL := /bin/bash @@ -58,23 +61,21 @@ SHELL := /bin/bash all : build dmidump -build: $(PY)-dmidecodemod.so -$(PY)-dmidecodemod.so: $(SO) - cp $< $@ -$(SO): - $(PY) src/setup.py build +build: + $(PY_BIN) src/setup.py build + cp "$(SO)" "$(PY_TAG)-dmidecodemod.so" dmidump : src/util.o src/efi.o src/dmilog.o $(CC) -o $@ src/dmidump.c $^ -g -Wall -D_DMIDUMP_MAIN_ install: - $(PY) src/setup.py install + $(PY_BIN) src/setup.py install uninstall: - $(PY) src/setup.py uninstall + $(PY_BIN) src/setup.py uninstall clean: - -$(PY) src/setup.py clean --all + -$(PY_BIN) src/setup.py clean --all -rm -f *.so lib/*.o core dmidump src/*.o -rm -rf build -rm -rf rpm @@ -111,4 +112,3 @@ conflicts: @comm -12 \ <(dpkg-deb -c ../../DPKGS/python-dmidecode_$(VERSION)-1_amd64.deb | awk '$$NF!~/\/$$/{print$$NF}'|sort) \ <(dpkg-deb -c ../../DPKGS/python-dmidecode-dbg_$(VERSION)-1_amd64.deb | awk '$$NF!~/\/$$/{print$$NF}'|sort) - diff --git a/PORTING_PLAN.md b/PORTING_PLAN.md new file mode 100644 index 0000000..5ad1244 --- /dev/null +++ b/PORTING_PLAN.md @@ -0,0 +1,426 @@ +# Porting Plan: Migrating from libxml2 to xml.etree.ElementTree + +## Executive Summary + +This document outlines the plan to migrate the python-dmidecode project from using libxml2 via its Python bindings to using xml.etree.ElementTree from the Python standard library. This migration will eliminate the external dependency on libxml2, improving compatibility and simplifying deployment. + +## Implementation Status (Current Working Tree) + +Porting work is in progress on branch `xml-elementtree-port`. + +- Python XML API has been switched to `xml.etree.ElementTree` and now returns wrapper types `dmidecode.XmlNode` / `dmidecode.XmlDoc` (`dmidecode.py`). +- C extension `xmlapi()` no longer returns Python libxml2 objects; it serializes libxml2 `xmlNode` to XML bytes and returns `bytes` to Python (`src/dmidecodemodule.c`). `xmlapi()` accepts both positional and keyword arguments for compatibility. +- Unit tests were updated to validate `dmidecode.XmlNode` / `dmidecode.XmlDoc` instead of `libxml2.xmlNode` / `libxml2.xmlDoc` (`unit-tests/unit`). + +Known gaps/blockers right now: + +- Build no longer force-links `xml2mod` via `src/setup_common.py` (no Python libxml2 bindings required). +- Python 3 capsule cleanup must free the `options` struct, not the capsule object (fix crash-on-exit / invalid free). +- Example `examples/dmidump.py` updated to use ElementTree wrappers. +- CI/packaging metadata must not require Python libxml2 bindings. +- CI configuration still installs Python libxml2 bindings (`.travis.yml`). +- libxml2 C library is still used throughout the C codebase for XML construction (the current work removes Python libxml2 *bindings* usage first). + +## Current State Analysis + +### Current libxml2 Usage + +The project currently uses libxml2 in several key areas: + +1. **Python API** (`dmidecode.py`): + - Imports `xml.etree.ElementTree` (ElementTree) + - Returns wrapper objects (`XmlNode` / `XmlDoc`) around ElementTree objects + - Provides `dmidecodeXML` class with XML query methods that parse XML bytes from the extension + +2. **C Extension** (`src/dmidecodemodule.c`, `src/libxml_wrap.h`): + - Uses libxml2 C API extensively + - Creates XML documents and nodes using libxml2 functions + - `xmlapi()` returns serialized XML (`bytes`) instead of Python libxml2 wrapper objects. Input args can be positional or keywords (`query_type`, `result_type`, `section`, `typeid`). + +3. **Testing** (`unit-tests/unit`): + - Validates return types as `dmidecode.XmlNode` / `dmidecode.XmlDoc` + +### Key Files Affected + +- `dmidecode.py` - Main Python module +- `unit-tests/unit` - Test suite +- `src/dmidecodemodule.c` - C extension +- `src/libxml_wrap.h` - libxml2 wrapping headers +- `src/setup.py` - Build configuration +- `debian/control`, `contrib/python-dmidecode.spec` - Packaging files + +## SWOT Analysis of Alternative XML Libraries + +### xml.etree.ElementTree (Recommended) + +**Strengths:** +- ✅ Built into Python standard library - no additional dependencies +- ✅ Simple, intuitive API for XML manipulation +- ✅ Good performance for most use cases +- ✅ Lightweight and easy to use +- ✅ Supports XPath-like expressions with `find()` and `findall()` + +**Weaknesses:** +- ❌ No direct equivalent to libxml2's xmlNode/xmlDoc distinction +- ❌ Limited XPath support (only basic subset) +- ❌ Different object model than libxml2 (Element vs Node) + +**Opportunities:** +- 🔄 Can simplify the codebase by removing external dependency +- 🔄 Easier deployment and installation +- 🔄 Better compatibility across different systems + +**Threats:** +- ⚠️ Significant API changes required in both Python and C code +- ⚠️ Potential performance impact for large XML documents +- ⚠️ May need to implement custom wrapping logic + +### xml.dom.minidom (Standard Library Alternative) + +**Strengths:** +- ✅ Built into Python standard library - no additional dependencies +- ✅ DOM-compliant API (closer to libxml2's document/node model) +- ✅ Familiar to developers coming from JavaScript/DOM backgrounds +- ✅ Supports full XML document structure with Document, Node, Element hierarchy +- ✅ Better conceptual match to libxml2's xmlDoc/xmlNode distinction +- ✅ Supports DOM Level 2 Core API + +**Weaknesses:** +- ❌ Slower than ElementTree for most operations (more memory intensive) +- ❌ Verbose API compared to ElementTree +- ❌ No direct equivalent to libxml2's wrapping mechanism +- ❌ Limited XPath support (would need custom implementation) +- ❌ More complex object model with many node types (Element, Text, Comment, etc.) +- ❌ Poor performance with large XML documents due to DOM tree construction + +**Opportunities:** +- 🔄 Closer conceptual match to libxml2's document/node model +- 🔄 Easier migration path for DOM-oriented code +- 🔄 Could provide more familiar API for users expecting DOM-style access +- 🔄 Better support for mixed content and complex XML structures + +**Threats:** +- ⚠️ Performance issues with large DMI data (DMI tables can be substantial) +- ⚠️ Still requires significant code changes from libxml2 +- ⚠️ Less Pythonic API than ElementTree +- ⚠️ Memory consumption could be problematic for embedded systems +- ⚠️ Testing infrastructure would need significant updates + +**Specific Challenges for DMI Data:** +- DMI XML structures tend to be hierarchical but not extremely deep +- Performance impact may be noticeable but potentially acceptable +- Memory usage could be concern for systems with limited resources +- Would need custom wrapper classes to mimic libxml2 API + +### lxml (Third-party Alternative) + +**Strengths:** +- ✅ High performance (written in C) +- ✅ Full XPath 1.0 support +- ✅ Excellent compatibility with ElementTree API +- ✅ Advanced features: XSLT, validation, namespaces +- ✅ Memory efficient +- ✅ Actively maintained +- ✅ Can use both ElementTree and DOM-like interfaces + +**Weaknesses:** +- ❌ External dependency (not in standard library) +- ❌ Larger footprint than standard library options +- ❌ More complex installation (C extensions) +- ❌ Overkill for simple XML needs + +**Why Not Primary Choice:** +While lxml offers excellent performance and features, the standard library options provide better compatibility and simpler deployment for this use case. + +## Comparison: ElementTree vs minidom for DMI Data + +### Decision Factors for python-dmidecode + +| Factor | xml.etree.ElementTree | xml.dom.minidom | Importance | +|--------|----------------------|----------------|------------| +| **Standard Library** | ✅ Yes | ✅ Yes | ⭐⭐⭐⭐⭐ | +| **Performance** | ⚡⚡⚡ (Good) | ⚡ (Poor) | ⭐⭐⭐⭐ | +| **Memory Usage** | 🧠🧠 (Low) | 🧠🧠🧠🧠 (High) | ⭐⭐⭐⭐ | +| **API Simplicity** | ✅✅✅ (Simple) | ❌❌ (Complex) | ⭐⭐⭐ | +| **Conceptual Match** | ❌ (Element-based) | ✅ (Node-based) | ⭐⭐ | +| **XPath Support** | ❌ (Limited) | ❌ (None) | ⭐ | +| **Migration Complexity** | ⚠️⚠️ (Moderate) | ⚠️⚠️⚠️ (High) | ⭐⭐⭐⭐ | +| **Code Maintainability** | ✅✅✅ (High) | ✅✅ (Moderate) | ⭐⭐⭐⭐ | + +### Recommendation Rationale + +**Choose xml.etree.ElementTree because:** + +1. **Performance Matters**: DMI data can be substantial, and ElementTree's better performance is crucial for system tools +2. **Simplicity Wins**: The simpler API will be easier to maintain and extend +3. **Memory Efficiency**: Lower memory usage is important for system-level tools +4. **Modern Python**: ElementTree represents the modern Python approach to XML +5. **Better Trade-off**: While minidom is conceptually closer to libxml2, the performance and simplicity advantages of ElementTree outweigh this benefit + +**When minidom might be considered:** +- If the codebase had extensive DOM manipulation requirements +- If there were many existing users relying on DOM-style API +- If XPath support was critical (though neither standard library option excels here) + +### Performance Considerations for DMI Data + +Typical DMI XML structures: +- **Size**: Usually 10-100KB (can be larger on complex systems) +- **Depth**: 3-6 levels deep +- **Complexity**: Mostly hierarchical with some mixed content +- **Usage Pattern**: Read-heavy, infrequent writes + +ElementTree should handle this workload efficiently, while minidom could show noticeable performance degradation on larger systems. + +## Porting Plan + +### Phase 1: Preparation and Analysis ✅ (Completed) + +1. **Document Current Usage** ✅ + - Identified all libxml2 usage patterns + - Mapped current API surface + - Documented test requirements + +2. **Set Up Development Environment** 🛠️ + - Create a branch for the porting work + - Ensure all tests pass with current libxml2 implementation + - Set up continuous integration for testing + +3. **Create Migration Mapping** 📋 + - Map libxml2 concepts to ElementTree equivalents: + - `libxml2.xmlNode` → `xml.etree.ElementTree.Element` + - `libxml2.xmlDoc` → `xml.etree.ElementTree.ElementTree` (root element) + - libxml2 wrapping functions → Custom Python classes + +### Phase 2: Python API Migration 🐍 + +**Objective**: Update the Python-facing API to use ElementTree + +Status: 🚧 In progress (core API switched; compatibility surface not complete) + +1. **Update dmidecode.py** + ```python + # Replace + import libxml2 + + # With + import xml.etree.ElementTree as ET + + # Create wrapper classes + class XmlNode: + def __init__(self, element): + self.element = element + + class XmlDoc: + def __init__(self, element_tree): + self.element_tree = element_tree + ``` + +2. **Update Result Types** + - Modify `DMIXML_NODE` and `DMIXML_DOC` constants + - Update `SetResultType()` method + - Ensure type checking works with new classes + +3. **Update Query Methods** + - Modify `QuerySection()` and `QueryTypeId()` to return new wrapper objects + - Maintain backward compatibility where possible + +### Phase 3: C Extension Refactoring ⚙️ + +**Objective**: Modify the C extension to work with ElementTree + +Status: 🚧 In progress (XML serialization bridge implemented; build/packaging cleanup pending) + +1. **Replace libxml2 Wrapping** + - Remove `libxml_wrap.h` dependency + - Create new Python object creation functions + - Implement XML serialization/deserialization bridge + +2. **Add XML Serialization Layer** + ```c + // Strategy: Serialize libxml2 XML to string, then parse with ElementTree + char* serialize_libxml2_to_string(xmlNode* node) { + // Implement XML serialization + } + + PyObject* create_elementtree_object(xmlNode* node) { + char* xml_string = serialize_libxml2_to_string(node); + // Call Python ElementTree.parse() on the string + } + ``` + +3. **Update Build System** + - Remove libxml2 dependencies from `setup.py` + - Update build scripts + - Modify packaging metadata + +### Phase 4: Testing Infrastructure Update 🧪 + +**Objective**: Update tests to work with new XML library + +Status: 🚧 In progress (unit test type checks updated; broader behavioral coverage still needed) + +1. **Update unit-tests/unit** + ```python + # Replace + import libxml2 + test(isinstance(output_node, libxml2.xmlNode)) + + # With + from xml.etree.ElementTree import Element + test(isinstance(output_node.element, Element)) + ``` + +2. **Create Test Compatibility Layer** + - Add helper functions to compare XML structures + - Ensure all existing test cases pass + - Update test assertions for new API + +### Phase 5: Gradual Migration Strategy 🎯 + +**Objective**: Implement the migration in manageable steps + +1. **Step 1: Add ElementTree Support Alongside libxml2** + - Create new classes and functions with `_et` suffix + - Allow both APIs to coexist temporarily + - Add feature flag to switch between implementations + +2. **Step 2: Update Documentation** + - Document new API + - Provide migration guide for users + - Update examples to use new API + +3. **Step 3: Deprecate libxml2 API** + - Mark old API as deprecated + - Add warnings for libxml2 usage + - Provide clear migration path + +4. **Step 4: Remove libxml2 Dependency** + - Remove all libxml2-related code + - Update packaging to remove libxml2 dependencies + - Final testing and validation + +### Phase 6: Performance Optimization ⚡ + +**Objective**: Ensure good performance with ElementTree + +1. **Profile XML Processing** + - Identify performance bottlenecks + - Optimize XML serialization/deserialization + +2. **Implement Caching** + - Cache parsed XML structures + - Reduce redundant XML processing + +3. **Memory Management** + - Ensure proper cleanup of XML resources + - Prevent memory leaks in C extension + +### Phase 7: Final Testing and Release 🚀 + +**Objective**: Ensure quality and prepare for release + +1. **Comprehensive Testing** + - Run all unit tests + - Test with various DMI data samples + - Performance benchmarking + +2. **Update Packaging** + - Remove libxml2 from dependencies + - Update setup.py and packaging metadata + - Update distribution packages (RPM, DEB, etc.) + +3. **Release Preparation** + - Update changelog + - Create release notes highlighting the change + - Plan for user communication and support + +## Key Challenges and Solutions + +### Challenge 1: C Extension Compatibility +**Problem**: The C extension currently creates libxml2 objects and wraps them directly. + +**Solution**: +- Add XML serialization layer in C extension +- Convert libxml2 XML to string format +- Parse string with ElementTree in Python layer +- May require temporary dual API support + +### Challenge 2: API Compatibility +**Problem**: Users may have code that expects libxml2.xmlNode/xmlDoc objects. + +**Solution**: +- Provide wrapper classes that mimic libxml2 API +- Offer gradual migration path +- Document breaking changes clearly + +### Challenge 3: Performance Impact +**Problem**: ElementTree may be slower than libxml2 for some operations. + +**Solution**: +- Profile and optimize critical paths +- Consider caching strategies +- Evaluate if performance impact is acceptable + +### Challenge 4: Testing Complexity +**Problem**: Need to ensure all existing functionality works with new XML library. + +**Solution**: +- Maintain comprehensive test suite +- Add XML comparison utilities +- Test with diverse DMI data samples + +## Estimated Timeline + +| Phase | Duration | Status | +|-------|----------|--------| +| Preparation and Analysis | 1-2 days | ✅ Completed | +| Python API Migration | 2-3 days | 🚧 In progress | +| C Extension Refactoring | 3-5 days | 🚧 In progress | +| Testing Updates | 2-3 days | 🚧 In progress | +| Performance Optimization | 2 days | ⏳ Pending | +| Final Testing and Release | 1-2 days | ⏳ Pending | + +**Total Estimate**: 2-3 weeks for complete migration + +## Risk Assessment + +### High Risk Items +- C extension refactoring complexity +- Performance degradation with large DMI datasets +- Breaking changes for existing users + +### Mitigation Strategies +- Implement in phases with backward compatibility +- Performance testing early and often +- Clear communication about breaking changes +- Provide migration guide and tools + +## Migration Checklist + +- [x] Create porting branch +- [~] Update Python API to use ElementTree (core switched; compatibility methods pending) +- [~] Modify C extension for ElementTree compatibility (xmlapi bridge done; build/cleanup pending) +- [~] Update test suite (type checks updated; more assertions pending) +- [x] Fix build to remove `xml2mod` linking requirement +- [ ] Update examples to work with ElementTree wrappers +- [ ] Drop Python libxml2 binding dependencies from packaging +- [ ] Performance testing and optimization +- [ ] Documentation updates +- [ ] Packaging updates +- [ ] Final integration testing +- [ ] Release preparation + +## Success Criteria + +1. **Functional Equivalence**: All existing functionality works with new XML library +2. **Performance Acceptability**: No significant performance regression +3. **API Compatibility**: Clear migration path for existing users +4. **Test Coverage**: All tests pass with new implementation +5. **Documentation**: Complete and accurate documentation of changes + +## Conclusion + +This porting plan provides a comprehensive approach to migrating from libxml2 to xml.etree.ElementTree. The migration will eliminate the external dependency on libxml2, improving the project's compatibility and maintainability while preserving all existing functionality. + +The recommended approach uses a phased migration strategy to minimize disruption and provide a clear path forward for both developers and users of the python-dmidecode library. diff --git a/contrib/python-dmidecode.spec b/contrib/python-dmidecode.spec index 2c7f942..f0dd707 100644 --- a/contrib/python-dmidecode.spec +++ b/contrib/python-dmidecode.spec @@ -10,15 +10,13 @@ Group: System Environment/Libraries URL: http://projects.autonomy.net.au/python-dmidecode/ Source0: http://src.autonomy.net.au/python-dmidecode/%{name}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root -Requires: libxml2-python -BuildRequires: libxml2-python BuildRequires: libxml2-devel BuildRequires: python-devel %description python-dmidecode is a python extension module that uses the code-base of the 'dmidecode' utility, and presents the data -as python data structures or as XML data using libxml2. +as python data structures or as XML data. %prep %setup -q @@ -105,7 +103,7 @@ rm -rf $RPM_BUILD_ROOT - Only build the python-dmidecode module, not everything * Wed Jul 13 2009 David Sommerseth - 3.10.6-5 -- Added missing BuildRequres for libxml2-python +- Added missing BuildRequres for libxml2-python (historical; no longer required for ElementTree port) * Wed Jul 13 2009 David Sommerseth - 3.10.6-4 - Added missing BuildRequres for python-devel @@ -121,4 +119,3 @@ rm -rf $RPM_BUILD_ROOT * Sat Mar 7 2009 Clark Williams - 2.10.3-1 - Initial build. - diff --git a/debian/control b/debian/control index 9a0297b..730451f 100644 --- a/debian/control +++ b/debian/control @@ -5,14 +5,14 @@ Priority: optional Homepage: http://projects.autonomy.net.au/python-dmidecode Maintainer: Nima Talebi Build-Depends: debhelper (>> 7), python-support (>= 0.5.3), python, - python-all-dev (>= 2.5.4-2), python-all-dbg, libxml2-dev, python-libxml2 + python-all-dev (>= 2.5.4-2), python-all-dbg, libxml2-dev Standards-Version: 3.8.4 Package: python-dmidecode XB-Python-Version: ${python:Versions} Architecture: any Provides: ${python:Provides} -Depends: ${python:Depends}, ${shlibs:Depends}, ${misc:Depends}, python-libxml2 +Depends: ${python:Depends}, ${shlibs:Depends}, ${misc:Depends} Description: Python extension module for dmidecode DMI (the desktop management interface) provides a standardized description of a computer's hardware, including characteristics such as BIOS serial number diff --git a/dmidecode.py b/dmidecode.py index ac81365..06e0ec7 100644 --- a/dmidecode.py +++ b/dmidecode.py @@ -25,12 +25,42 @@ # are deemed to be part of the source code. # -import libxml2 +import xml.etree.ElementTree as ET from dmidecodemod import * DMIXML_NODE='n' DMIXML_DOC='d' +class XmlNode: + """ + Wrapper class to provide libxml2.xmlNode-like interface using ElementTree.Element + """ + def __init__(self, element): + self.element = element + self._obj = element # Maintain compatibility with libxml2 interface + + def __getattr__(self, name): + """Delegate attribute access to the underlying Element""" + element = object.__getattribute__(self, 'element') + return getattr(element, name) + +class XmlDoc: + """ + Wrapper class to provide libxml2.xmlDoc-like interface using ElementTree.ElementTree + """ + def __init__(self, element_tree): + self.element_tree = element_tree + self._obj = element_tree.getroot() # Maintain compatibility + + def getroot(self): + """Get the root element""" + return self.element_tree.getroot() + + def __getattr__(self, name): + """Delegate attribute access to the underlying ElementTree""" + element_tree = object.__getattribute__(self, 'element_tree') + return getattr(element_tree, name) + class dmidecodeXML: "Native Python API for retrieving dmidecode information as XML" @@ -40,7 +70,7 @@ def __init__(self): def SetResultType(self, type): """ Sets the result type of queries. The value can be DMIXML_NODE or DMIXML_DOC, - which will return an libxml2::xmlNode or libxml2::xmlDoc object, respectively + which will return an XmlNode or XmlDoc object, respectively """ if type == DMIXML_NODE: @@ -51,39 +81,38 @@ def SetResultType(self, type): raise TypeError("Invalid result type value") return True + def _create_xml_from_string(self, xml_string): + """ + Internal method to create XML objects from string representation + This will be used when the C extension returns XML as bytes (or str) + """ + try: + element = ET.fromstring(xml_string) + if self.restype == DMIXML_NODE: + return XmlNode(element) + else: # DMIXML_DOC + tree = ET.ElementTree(element) + return XmlDoc(tree) + except ET.ParseError as e: + raise ValueError(f"Failed to parse XML: {e}") from e + def QuerySection(self, sectname): """ Queries the DMI data structure for a given section name. A section can often contain several DMI type elements """ - if self.restype == DMIXML_NODE: - ret = libxml2.xmlNode( _obj = xmlapi(query_type='s', - result_type=self.restype, - section=sectname) ) - elif self.restype == DMIXML_DOC: - ret = libxml2.xmlDoc( _obj = xmlapi(query_type='s', - result_type=self.restype, - section=sectname) ) - else: - raise TypeError("Invalid result type value") - - return ret - + # Get XML data as string from C extension + xml_string = xmlapi('s', self.restype, sectname) + + # Convert to appropriate XML object + return self._create_xml_from_string(xml_string) def QueryTypeId(self, tpid): """ Queries the DMI data structure for a specific DMI type. """ - if self.restype == DMIXML_NODE: - ret = libxml2.xmlNode( _obj = xmlapi(query_type='t', - result_type=self.restype, - typeid=tpid)) - elif self.restype == DMIXML_DOC: - ret = libxml2.xmlDoc( _obj = xmlapi(query_type='t', - result_type=self.restype, - typeid=tpid)) - else: - raise TypeError("Invalid result type value") - - return ret - + # Get XML data as string from C extension + xml_string = xmlapi('t', self.restype, tpid) + + # Convert to appropriate XML object + return self._create_xml_from_string(xml_string) diff --git a/examples/dmidump.py b/examples/dmidump.py index 769750d..905e5a4 100755 --- a/examples/dmidump.py +++ b/examples/dmidump.py @@ -145,35 +145,45 @@ def print_warnings(): print() dmixml = dmidecode.dmidecodeXML() -# Fetch all DMI data into a libxml2.xmlDoc object +# Fetch all DMI data into an ElementTree-backed document wrapper print("*** Getting all DMI data into a XML document variable") dmixml.SetResultType(dmidecode.DMIXML_DOC) # Valid values: dmidecode.DMIXML_DOC, dmidecode.DMIXML_NODE xmldoc = dmixml.QuerySection('all') -# Dump the XML to dmidump.xml - formated in UTF-8 decoding +# Dump the XML to dmidump.xml (UTF-8). ElementTree does not pretty-print by default, +# so we indent the tree when available. print("*** Dumping XML document to dmidump.xml") -xmldoc.saveFormatFileEnc('dmidump.xml','UTF-8',1) +try: + import xml.etree.ElementTree as ET + tree = xmldoc.element_tree + if hasattr(ET, 'indent'): + ET.indent(tree, space=' ') + tree.write('dmidump.xml', encoding='utf-8', xml_declaration=True) +except Exception as e: + print("Failed to write dmidump.xml: %s" % e) + +# Do some simple queries on the XML document (ElementTree path syntax) +print("*** Doing some element queries against the XML document") +root = xmldoc.element_tree.getroot() + +keys = ['SystemInfo/Manufacturer', + 'SystemInfo/ProductName', + 'SystemInfo/SerialNumber', + 'SystemInfo/SystemUUID'] -# Do some XPath queries on the XML document -print("*** Doing some XPath queries against the XML document") -dmixp = xmldoc.xpathNewContext() - -# What to look for - XPath expressions -keys = ['/dmidecode/SystemInfo/Manufacturer', - '/dmidecode/SystemInfo/ProductName', - '/dmidecode/SystemInfo/SerialNumber', - '/dmidecode/SystemInfo/SystemUUID'] - -# Extract data and print it for k in keys: - data = dmixp.xpathEval(k) - for d in data: - print("%s: %s" % (k, d.get_content())) + val = root.findtext(k) + print("%s: %s" % (k, val)) -del dmixp del xmldoc # Query for only a particular DMI TypeID - 0x04 - Processor print("*** Quering for Type ID 0x04 - Processor - dumping XML document to stdout") -dmixml.QueryTypeId(0x04).saveFormatFileEnc('-','UTF-8',1) +try: + import xml.etree.ElementTree as ET + x = dmixml.QueryTypeId(0x04) + xml_out = ET.tostring(x.element, encoding='unicode') + print(xml_out) +except Exception as e: + print("Failed to dump XML: %s" % e) print_warnings() diff --git a/mock_dmidecodemod.py b/mock_dmidecodemod.py new file mode 100644 index 0000000..6a7d292 --- /dev/null +++ b/mock_dmidecodemod.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +Mock version of dmidecodemod for testing the ElementTree port. +This mock provides the same interface as the C extension but returns test XML data. +""" + +def xmlapi(query_type, result_type, section=None, typeid=None): + """ + Mock xmlapi function that returns test XML data as strings. + This simulates what the modified C extension will return. + """ + + # Test XML data for different query types + test_data = { + 'bios': """ + + + Test BIOS Vendor + 1.2.3 + 01/01/2023 + + """, + + 'system': """ + + + Test Manufacturer + Test Product + 1.0 + TEST123456 + + """, + + 'baseboard': """ + + + Test Board Manufacturer + Test Board + 1.0 + BOARD123 + + """, + + '0': """ + + + Test BIOS Vendor + 1.2.3 + 01/01/2023 + + """, + + '1': """ + + + Test Manufacturer + Test Product + 1.0 + TEST123456 + + """, + + '2': """ + + + Test Board Manufacturer + Test Board + 1.0 + BOARD123 + + """, + } + + # Determine what data to return based on query type + if query_type == 's': # Section query + if section in test_data: + return test_data[section] + else: + # Return a generic response for unknown sections + return f""" + + <{section}_information> + Test Manufacturer + + """ + + elif query_type == 't': # Type ID query + # The real extension passes typeid as a positional argument in the + # third slot when using the simplified xmlapi('t', rtype, tpid) API. + if typeid is None and section is not None: + typeid = section + typeid_str = str(typeid) + if typeid_str in test_data: + return test_data[typeid_str] + else: + # Return a generic response for unknown types + return f""" + + + Test Manufacturer + + """ + + else: + return """ + + Unknown query type + """ + +# Mock other functions that might be imported +version = "3.12.3 (ElementTree Port)" +dmi = "Test DMI String" diff --git a/src/dmidecode.c b/src/dmidecode.c index d40f0ee..714672d 100644 --- a/src/dmidecode.c +++ b/src/dmidecode.c @@ -1790,7 +1790,11 @@ void dmi_memory_module_error(xmlNode *node, u8 code) dmixml_AddAttribute(data_n, "flags", "0x%04x", code); if( !(code & (1 << 2)) ) { - dmixml_AddAttribute(data_n, "Error Status", "%s", status[code & 0x03]); + dmixml_AddAttribute(data_n, "Error", "%i", ((code & 0x03) == 0 ? 0 : 1)); + dmixml_AddAttribute(data_n, "Status", "%s", status[code & 0x03]); + } else { + dmixml_AddAttribute(data_n, "unknown", "1"); + dmixml_AddAttribute(data_n, "Error", "0"); } } @@ -6423,4 +6427,3 @@ int legacy_decode(Log_t *logp, int type, u8 *buf, const char *devmem, u32 flags, ((buf[0x0E] & 0xF0) << 4) + (buf[0x0E] & 0x0F), devmem, flags, xmlnode); return check; } - diff --git a/src/dmidecodemodule.c b/src/dmidecodemodule.c index efa730b..2d8bd87 100644 --- a/src/dmidecodemodule.c +++ b/src/dmidecodemodule.c @@ -42,7 +42,6 @@ #include #include -#include "libxml_wrap.h" #include "dmidecodemodule.h" #include "dmixml.h" @@ -53,6 +52,15 @@ #include "dmidump.h" #include +/* + * PyMethodDef.ml_meth is typed as PyCFunction (2-arg), but METH_KEYWORDS + * functions take 3 args. CPython provides _PyCFunction_CAST() to silence + * cast-function-type warnings by casting via void(*)(void). + */ +#ifndef _PyCFunction_CAST +#define _PyCFunction_CAST(func) ((PyCFunction)(void(*)(void))(func)) +#endif + #if (PY_VERSION_HEX < 0x03030000) char *PyUnicode_AsUTF8(PyObject *unicode) { PyObject *as_bytes = PyUnicode_AsUTF8String(unicode); @@ -479,7 +487,7 @@ xmlNode *__dmidecode_xml_getsection(options *opt, const char *section) { if(opt->type == -1) { char *err = log_retrieve(opt->logdata, LOG_ERR); log_clear_partial(opt->logdata, LOG_ERR, 0); - _pyReturnError(PyExc_RuntimeError, "Invalid type id '%s' -- %s", typeid, err); + _pyReturnError(PyExc_RuntimeError, __FILE__, __LINE__, "Invalid type id '%s' -- %s", typeid, err); free(err); return NULL; } @@ -657,7 +665,7 @@ static PyObject *dmidecode_get_slot(PyObject * self, PyObject * args) static PyObject *dmidecode_get_section(PyObject *self, PyObject *args) { - char *section = NULL; + const char *section = NULL; if (PyUnicode_Check(args)) { section = PyUnicode_AsUTF8(args); } else if (PyBytes_Check(args)) { @@ -690,34 +698,128 @@ static PyObject *dmidecode_get_type(PyObject * self, PyObject * args) return pydata; } -static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args, PyObject *keywds) +static int pyobj_parse_int(PyObject *obj, int *out) +{ + long v; + char *end = NULL; + + if (obj == NULL) { + return 0; + } + + if (PyLong_Check(obj)) { + v = PyLong_AsLong(obj); + if (PyErr_Occurred()) { + return -1; + } + *out = (int)v; + return 1; + } + + if (PyUnicode_Check(obj)) { + const char *s = PyUnicode_AsUTF8(obj); + if (s == NULL) { + return -1; + } + v = strtol(s, &end, 10); + if (end == s || *end != '\0') { + return 0; + } + *out = (int)v; + return 1; + } + + if (PyBytes_Check(obj)) { + char *s = PyBytes_AsString(obj); + if (s == NULL) { + return -1; + } + v = strtol(s, &end, 10); + if (end == s || *end != '\0') { + return 0; + } + *out = (int)v; + return 1; + } + + return 0; +} + +static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args, PyObject *kwds) { - static char *keywordlist[] = {"query_type", "result_type", "section", "typeid", NULL}; + (void)self; PyObject *pydata = NULL; - xmlDoc *dmixml_doc = NULL; + xmlDoc *temp_doc = NULL; xmlNode *dmixml_n = NULL; - char *sect_query = NULL, *qtype = NULL, *rtype = NULL; + xmlChar *xml_buffer = NULL; + const char *sect_query = NULL, *qtype = NULL, *rtype = NULL; + PyObject *section_arg = NULL; + PyObject *typeid_arg = NULL; int type_query = -1; - - // Parse the keywords - we only support keywords, as this is an internal API - if( !PyArg_ParseTupleAndKeywords(args, keywds, "ss|si", keywordlist, - &qtype, &rtype, §_query, &type_query) ) { + int buffer_size = 0; + int parsed; + static char *kwlist[] = { (char *)"query_type", (char *)"result_type", + (char *)"section", (char *)"typeid", NULL }; + + // Parse arguments. + // We support both of these call shapes: + // xmlapi('s', rtype, section) + // xmlapi('t', rtype, typeid) + // And the legacy 4-arg variant: + // xmlapi('t', rtype, section_placeholder, typeid) + // As well as keyword args: + // xmlapi(query_type='s', result_type=rtype, section=section) + // xmlapi(query_type='t', result_type=rtype, typeid=typeid) + if( !PyArg_ParseTupleAndKeywords(args, kwds, "ss|OO", kwlist, + &qtype, &rtype, §ion_arg, &typeid_arg) ) { return NULL; } + if( section_arg == Py_None ) { + section_arg = NULL; + } + if( typeid_arg == Py_None ) { + typeid_arg = NULL; + } + // Check for sensible arguments and retrieve the xmlNode with DMI data switch( *qtype ) { case 's': // Section / GroupName + if( section_arg == NULL ) { + PyReturnError(PyExc_TypeError, "section argument cannot be NULL") + } + if( PyUnicode_Check(section_arg) ) { + sect_query = PyUnicode_AsUTF8(section_arg); + } else if( PyBytes_Check(section_arg) ) { + sect_query = PyBytes_AsString(section_arg); + } else { + PyReturnError(PyExc_TypeError, "section argument must be str or bytes") + } if( sect_query == NULL ) { - PyReturnError(PyExc_TypeError, "section keyword cannot be NULL") + // Exception already set by PyUnicode_AsUTF8() or PyBytes_AsString() + return NULL; } dmixml_n = __dmidecode_xml_getsection(global_options, sect_query); break; case 't': // TypeID / direct TypeMap + // Prefer an explicit typeid= keyword / 4th positional arg. + parsed = pyobj_parse_int(typeid_arg, &type_query); + if( parsed < 0 ) { + return NULL; + } + + // Backwards compatibility: accept typeid passed in the 3rd slot. + if( parsed == 0 && section_arg != NULL ) { + parsed = pyobj_parse_int(section_arg, &type_query); + if( parsed < 0 ) { + return NULL; + } + } + if( type_query < 0 ) { PyReturnError(PyExc_TypeError, - "typeid keyword must be set and must be a positive integer"); + "typeid must be set and must be a positive integer"); } else if( type_query > 255 ) { PyReturnError(PyExc_ValueError, "typeid keyword must be an integer between 0 and 255"); @@ -735,30 +837,43 @@ static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args, PyObject *keyw return NULL; } - // Check for sensible return type and wrap the correct type into a Python Object - switch( *rtype ) { - case 'n': - pydata = libxml_xmlNodePtrWrap((xmlNode *) dmixml_n); - break; - - case 'd': - dmixml_doc = xmlNewDoc((xmlChar *) "1.0"); - if( dmixml_doc == NULL ) { - PyReturnError(PyExc_MemoryError, "Could not create new XML document"); - } - xmlDocSetRootElement(dmixml_doc, dmixml_n); - pydata = libxml_xmlDocPtrWrap((xmlDoc *) dmixml_doc); - break; - - default: - PyReturnError(PyExc_TypeError, "Internal error - invalid result type '%c'", *rtype); + // Convert the XML node to a string representation + // Use the variables declared at function scope + + // Create a temporary document to hold our node + temp_doc = xmlNewDoc((xmlChar *) "1.0"); + if( temp_doc == NULL ) { + PyReturnError(PyExc_MemoryError, "Could not create temporary XML document"); + } + + // Set the node as root element + xmlDocSetRootElement(temp_doc, dmixml_n); + + // Serialize the document to a string buffer + xmlDocDumpMemory(temp_doc, &xml_buffer, &buffer_size); + + // Free the temporary document (this doesn't free the original node) + xmlFreeDoc(temp_doc); + + // Create Python string from the XML buffer + pydata = PyBytes_FromStringAndSize((const char *)xml_buffer, buffer_size); + + // Free the XML buffer + xmlFree(xml_buffer); + + if( pydata == NULL ) { + PyReturnError(PyExc_MemoryError, "Could not create Python string from XML"); } - // Return XML data - Py_INCREF(pydata); + // Return XML data as string return pydata; } +static PyObject *dmidecode_xmlapi_kw(PyObject *self, PyObject *args, PyObject *kwds) +{ + return dmidecode_xmlapi(self, args, kwds); +} + static PyObject *dmidecode_dump(PyObject * self, PyObject * null) @@ -788,7 +903,7 @@ static PyObject *dmidecode_get_dev(PyObject * self, PyObject * null) static PyObject *dmidecode_set_dev(PyObject * self, PyObject * arg) { - char *f = NULL; + const char *f = NULL; if(PyUnicode_Check(arg)) { f = PyUnicode_AsUTF8(arg); } else if(PyBytes_Check(arg)) { @@ -835,7 +950,7 @@ static PyObject *dmidecode_set_dev(PyObject * self, PyObject * arg) static PyObject *dmidecode_set_pythonxmlmap(PyObject * self, PyObject * arg) { - char *fname = NULL; + const char *fname = NULL; if (PyUnicode_Check(arg)) { fname = PyUnicode_AsUTF8(arg); @@ -913,8 +1028,8 @@ static PyMethodDef DMIDataMethods[] = { {(char *)"pythonmap", dmidecode_set_pythonxmlmap, METH_O, (char *) "Use another python dict map definition. The default file is " PYTHON_XML_MAP}, - {(char *)"xmlapi", dmidecode_xmlapi, METH_VARARGS | METH_KEYWORDS, - (char *) "Internal API for retrieving data as raw XML data"}, + {(char *)"xmlapi", _PyCFunction_CAST(dmidecode_xmlapi_kw), METH_VARARGS | METH_KEYWORDS, + (char *) "Internal API for retrieving data as raw XML data"}, {(char *)"get_warnings", dmidecode_get_warnings, METH_NOARGS, @@ -926,12 +1041,16 @@ static PyMethodDef DMIDataMethods[] = { {NULL, NULL, 0, NULL} }; -void destruct_options(void *ptr) +void destruct_options(PyObject *ptr) { + void *actual_ptr = ptr; #ifdef IS_PY3K - ptr = PyCapsule_GetPointer(ptr, NULL); + actual_ptr = PyCapsule_GetPointer(ptr, NULL); #endif - options *opt = (options *) ptr; + if( actual_ptr == NULL ) { + return; + } + options *opt = (options *) actual_ptr; if( opt->mappingxml != NULL ) { xmlFreeDoc(opt->mappingxml); @@ -965,7 +1084,7 @@ void destruct_options(void *ptr) log_close(opt->logdata); } - free(ptr); + free(actual_ptr); } #ifdef IS_PY3K @@ -994,7 +1113,7 @@ initdmidecodemod(void) options *opt; xmlInitParser(); - xmlXPathInit(); + /* xmlXPathInit() is deprecated (no longer needed with libxml2 init) */ opt = (options *) malloc(sizeof(options)+2); if (opt == NULL) @@ -1023,7 +1142,6 @@ initdmidecodemod(void) // Assign this options struct to the module as well with a destructor, that way it will // clean up the memory for us. - // TODO: destructor has wrong type under py3? PyModule_AddObject(module, "options", PyCapsule_New(opt, NULL, destruct_options)); global_options = opt; #ifdef IS_PY3K diff --git a/src/dmidump.c b/src/dmidump.c index 07f24e9..2f971c9 100644 --- a/src/dmidump.c +++ b/src/dmidump.c @@ -152,7 +152,8 @@ static int legacy_decode(u8 *buf, const char *devmem, u32 flags, const char *du devmem, flags, dumpfile); memcpy(crafted, buf, 16); - overwrite_smbios3_address(crafted); + /* Legacy entry point is a DMI entry point, not SMBIOS3 */ + overwrite_dmi_address(crafted); write_dump(0, 0x0F, crafted, dumpfile, 1); return 1; diff --git a/src/dmixml.c b/src/dmixml.c index 682acc7..9fa4008 100644 --- a/src/dmixml.c +++ b/src/dmixml.c @@ -45,6 +45,28 @@ #include "dmilog.h" #include "dmixml.h" +static void dmixml_sanitize_xml_string(xmlChar *s) +{ + /* + * libxml2 expects UTF-8 strings. SMBIOS strings are often plain bytes + * with vendor-specific encodings; make output safe for XML parsers by + * restricting to XML 1.0-safe ASCII. + */ + if (s == NULL) { + return; + } + + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + if (c == '\t' || c == '\n' || c == '\r') { + continue; + } + if (c < 0x20 || c == 0x7F || c >= 0x80) { + *s = (xmlChar)'.'; + } + } +} + /** * Internal function for dmixml_* functions. The function will allocate a buffer and populate it * according to the format string @@ -55,18 +77,17 @@ * @return xmlChar* Pointer to the buffer of the string */ xmlChar *dmixml_buildstr(size_t len, const char *fmt, va_list ap) { - xmlChar *ret = NULL, *xmlfmt = NULL; + xmlChar *ret = NULL; xmlChar *ptr = NULL; ret = (xmlChar *) malloc(len+2); assert( ret != NULL ); memset(ret, 0, len+2); - xmlfmt = xmlCharStrdup(fmt); - assert( xmlfmt != NULL ); + /* xmlStrVPrintf expects a const char * format string */ + xmlStrVPrintf(ret, len, (const char *)fmt, ap); - xmlStrVPrintf(ret, len, xmlfmt, ap); - free(xmlfmt); + dmixml_sanitize_xml_string(ret); // Right trim the string ptr = ret + xmlStrlen(ret)-1; @@ -201,6 +222,7 @@ xmlNode *dmixml_AddDMIstring(xmlNode *node, const char *tagname, const struct dm xmlChar *ret = NULL; xmlChar *ptr = NULL; xmlChar *val_s = xmlCharStrdup(dmistr); + dmixml_sanitize_xml_string(val_s); // Right trim the string ret = val_s; ptr = ret + xmlStrlen(ret) - 1; diff --git a/src/setup_common.py b/src/setup_common.py index aec1f9b..94e2b6b 100644 --- a/src/setup_common.py +++ b/src/setup_common.py @@ -68,8 +68,9 @@ def libxml2_lib(libdir, libs): elif l.find('-l') == 0: libs.append(l.replace("-l", "", 1)) - # this library is not reported and we need it anyway - libs.append('xml2mod') + # Historically we linked against the Python libxml2 bindings helper library + # (xml2mod) to expose libxml2 objects to Python. The ElementTree port no + # longer relies on those bindings, so do not force-link against xml2mod. @@ -100,4 +101,3 @@ def get_macros(): if sys.byteorder == 'big': macros.append(("ALIGNMENT_WORKAROUND", None)) return macros - diff --git a/test_elementtree_port.py b/test_elementtree_port.py new file mode 100644 index 0000000..0aaa713 --- /dev/null +++ b/test_elementtree_port.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Test script to verify the ElementTree port works correctly. +This tests the Python API changes without requiring the C extension to be rebuilt. +""" + +import sys +import os + +# Add the current directory to Python path so we can import our modified dmidecode +sys.path.insert(0, '.') + +# Mock the dmidecodemod module before importing dmidecode +import mock_dmidecodemod +sys.modules['dmidecodemod'] = mock_dmidecodemod + +def test_imports(): + """Test that we can import the module and it uses ElementTree""" + try: + import xml.etree.ElementTree as ET + print("✅ Successfully imported xml.etree.ElementTree") + + # Try to import our modified dmidecode + import dmidecode + print("✅ Successfully imported dmidecode module") + + # Check that it's using our new classes + assert hasattr(dmidecode, 'XmlNode'), "XmlNode class not found" + assert hasattr(dmidecode, 'XmlDoc'), "XmlDoc class not found" + print("✅ Found XmlNode and XmlDoc classes") + + return True + + except ImportError as e: + print(f"❌ Import failed: {e}") + return False + except AssertionError as e: + print(f"❌ Assertion failed: {e}") + return False + +def test_wrapper_classes(): + """Test that our wrapper classes work correctly""" + try: + import dmidecode + import xml.etree.ElementTree as ET + + # Create a test XML element + test_xml = """ + + + Test Manufacturer + Test Product + + """ + + # Parse with ElementTree + root = ET.fromstring(test_xml) + + # Test XmlNode wrapper + xml_node = dmidecode.XmlNode(root) + assert hasattr(xml_node, 'element'), "XmlNode missing element attribute" + assert hasattr(xml_node, '_obj'), "XmlNode missing _obj attribute" + print("✅ XmlNode wrapper class works correctly") + + # Test XmlDoc wrapper + tree = ET.ElementTree(root) + xml_doc = dmidecode.XmlDoc(tree) + assert hasattr(xml_doc, 'element_tree'), "XmlDoc missing element_tree attribute" + assert hasattr(xml_doc, '_obj'), "XmlDoc missing _obj attribute" + assert hasattr(xml_doc, 'getroot'), "XmlDoc missing getroot method" + print("✅ XmlDoc wrapper class works correctly") + + return True + + except Exception as e: + print(f"❌ Wrapper class test failed: {e}") + return False + +def test_dmidecode_xml_class(): + """Test that the dmidecodeXML class works with our changes""" + try: + import dmidecode + + # Create instance + dmi_xml = dmidecode.dmidecodeXML() + assert dmi_xml.restype == dmidecode.DMIXML_NODE, "Default restype should be DMIXML_NODE" + print("✅ dmidecodeXML instance created successfully") + + # Test SetResultType + result = dmi_xml.SetResultType(dmidecode.DMIXML_NODE) + assert result is True, "SetResultType should return True" + assert dmi_xml.restype == dmidecode.DMIXML_NODE + + result = dmi_xml.SetResultType(dmidecode.DMIXML_DOC) + assert result is True, "SetResultType should return True" + assert dmi_xml.restype == dmidecode.DMIXML_DOC + print("✅ SetResultType method works correctly") + + # Test invalid type + try: + dmi_xml.SetResultType('invalid') + print("❌ SetResultType should have raised TypeError for invalid type") + return False + except TypeError: + print("✅ SetResultType correctly rejects invalid types") + + return True + + except Exception as e: + print(f"❌ dmidecodeXML class test failed: {e}") + return False + +def test_xml_parsing(): + """Test that our XML parsing method works""" + try: + import dmidecode + + # Create instance + dmi_xml = dmidecode.dmidecodeXML() + + # Test XML string parsing + test_xml = """ + + Test Manufacturer + Test Product + """ + + # Test with DMIXML_NODE + dmi_xml.SetResultType(dmidecode.DMIXML_NODE) + result = dmi_xml._create_xml_from_string(test_xml) + assert isinstance(result, dmidecode.XmlNode), f"Expected XmlNode, got {type(result)}" + assert hasattr(result, 'element'), "Result should have element attribute" + print("✅ XML parsing for DMIXML_NODE works correctly") + + # Test with DMIXML_DOC + dmi_xml.SetResultType(dmidecode.DMIXML_DOC) + result = dmi_xml._create_xml_from_string(test_xml) + assert isinstance(result, dmidecode.XmlDoc), f"Expected XmlDoc, got {type(result)}" + assert hasattr(result, 'element_tree'), "Result should have element_tree attribute" + print("✅ XML parsing for DMIXML_DOC works correctly") + + # Test invalid XML + try: + dmi_xml._create_xml_from_string("-- + + On newer Pythons the build directory often looks like: + ../build/lib.- + (e.g. lib.linux-x86_64-cpython-313) + """ + + here = os.path.dirname(os.path.abspath(__file__)) + repo_root = os.path.abspath(os.path.join(here, '..')) + build_root = os.path.join(repo_root, 'build') + + # Always allow importing from repo root (useful for in-place builds) + if repo_root not in sys.path: + sys.path.insert(0, repo_root) + + # Prefer any build/lib.* directory that contains dmidecode.py + candidates = sorted(glob.glob(os.path.join(build_root, 'lib.*'))) + for p in candidates: + if os.path.exists(os.path.join(p, 'dmidecode.py')) and p not in sys.path: + sys.path.insert(0, p) + + # Fallback: add all build/lib.* candidates (best-effort) + for p in candidates: + if p not in sys.path: + sys.path.insert(0, p) + + +_add_build_paths() + #. Let's ignore warnings from the module for the test units... err = open('/dev/null', 'a+', 1) os.dup2(err.fileno(), sys.stderr.fileno()) @@ -167,7 +198,7 @@ except: vwrite(LINE, 1) try: vwrite(" * Importing module...", 1) - import libxml2 + import xml.etree.ElementTree as ET import dmidecode if not root_user: vwrite("\n%s"%cyan("Not running as root, a warning above can be expected..."), 1) @@ -315,7 +346,7 @@ try: vwrite(" * XML: Testing bad type - dmidecodeXML::QueryTypeId(%s)..." % red(i), 1) try: output_node = dmixml.QueryTypeId(i) - test(not isinstance(output_node, libxml2.xmlNode)) + test(not isinstance(output_node, dmidecode.XmlNode)) except SystemError: vwrite("Accepted => ", 1) failed() @@ -330,7 +361,7 @@ try: vwrite(" * XML: Testing dmidecodeXML::QueryTypeId(%s)..." % red(i), 1) try: output_node = dmixml.QueryTypeId(i) - test(isinstance(output_node, libxml2.xmlNode)) + test(isinstance(output_node, dmidecode.XmlNode)) except Exception as e: failed(e, 1) except: @@ -347,7 +378,7 @@ try: ), 1) try: output_doc = dmixml.QuerySection(section) - test(isinstance(output_doc, libxml2.xmlDoc)) + test(isinstance(output_doc, dmidecode.XmlDoc)) except Exception as e: failed(e, 1) except: