diff --git a/.gitignore b/.gitignore index 9a2c61ded12..434203fe2a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.bak *.gcno +*.gch *.o *.pyc /cppcheck diff --git a/AUTHORS b/AUTHORS index d77fa9dca46..45c61f3e45d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -238,6 +238,7 @@ Ludvig Gunne Lindström Luis Díaz Más Luís Pereira Lukas Grützmacher +Lukas Hiesmayr Lukasz Czajczyk Łukasz Jankowski Luxon Jean-Pierre diff --git a/addons/README.md b/addons/README.md index 563c0c793f6..2df77aff012 100644 --- a/addons/README.md +++ b/addons/README.md @@ -7,7 +7,7 @@ Addons are scripts that analyses Cppcheck dump files to check compatibility with + [misra.py](https://github.com/danmar/cppcheck/blob/main/addons/misra.py) Used to verify compliance with MISRA C 2012 - a proprietary set of guidelines to avoid such questionable code, developed for embedded systems. Since this standard is proprietary, cppcheck does not display error text by specifying only the number of violated rules (for example, [c2012-21.3]). If you want to display full texts for violated rules, you will need to create a text file containing MISRA rules, which you will have to pass when calling the script with `--rule-texts` key. Some examples of rule texts files available in [tests directory](https://github.com/danmar/cppcheck/blob/main/addons/test/misra/). + [y2038.py](https://github.com/danmar/cppcheck/blob/main/addons/y2038.py) - Checks Linux system for [year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) safety. This required [modified environment](https://github.com/3adev/y2038). See complete description [here](https://github.com/danmar/cppcheck/blob/main/addons/doc/y2038.txt). + Checks code for [year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) safety. Integrates with cppcheck's project parsing to automatically extract Y2038-related compiler flags from `compile_commands.json` and other build system configurations. See complete description [here](https://github.com/danmar/cppcheck/blob/main/addons/doc/y2038.md). + [threadsafety.py](https://github.com/danmar/cppcheck/blob/main/addons/threadsafety.py) Analyse Cppcheck dump files to locate threadsafety issues like static local objects used by multiple threads. + [naming.py](https://github.com/danmar/cppcheck/blob/main/addons/naming.py) @@ -50,6 +50,11 @@ Addons are scripts that analyses Cppcheck dump files to check compatibility with cppcheck --addon=misc src/test.c ``` +For project-wide analysis with compile_commands.json: +```bash +cppcheck --project=build/compile_commands.json --addon=y2038 +``` + It is also possible to call scripts as follows: ```bash cppcheck --dump --quiet src/test.c diff --git a/addons/doc/y2038.md b/addons/doc/y2038.md new file mode 100644 index 00000000000..3ac28a8bdc3 --- /dev/null +++ b/addons/doc/y2038.md @@ -0,0 +1,202 @@ +# README of the Y2038 cppcheck addon + +## Contents + +- [README of the Y2038 cppcheck addon](#readme-of-the-y2038-cppcheck-addon) + - [Contents](#contents) + - [What is Y2038?](#what-is-y2038) + - [What is the Y2038 cppcheck addon?](#what-is-the-y2038-cppcheck-addon) + - [How does the Y2038 cppcheck addon work?](#how-does-the-y2038-cppcheck-addon-work) + - [Primary Usage: Cppcheck Addon Integration (`y2038.py`)](#primary-usage-cppcheck-addon-integration-y2038py) + - [Implementation Details](#implementation-details) + - [Requirements](#requirements) + - [How to use the Y2038 cppcheck addon](#how-to-use-the-y2038-cppcheck-addon) + - [**Auditing Your Project for Y2038 Compliance**](#auditing-your-project-for-y2038-compliance) + - [**CI/CD Integration**](#cicd-integration) + - [Testing](#testing) + - [Running Y2038 Addon Tests](#running-y2038-addon-tests) + - [Test Coverage](#test-coverage) + - [Test Structure](#test-structure) + +--- + +## What is Y2038? + +In a few words: + +In Linux, the current date and time is kept as the number of seconds elapsed +since the Unix epoch, that is, since January 1st, 1970 at 00:00:00 GMT. + +Most of the time, this representation is stored as a 32-bit signed quantity. + +On January 19th, 2038 at 03:14:07 GMT, such 32-bit representations will reach +their maximum positive value. + +What happens then is unpredictable: system time might roll back to December +13th, 1901 at 19:55:13, or it might keep running on until February 7th, 2106 +at 06:28:15 GMT, or the computer may freeze, or just about anything you can +think of, plus a few ones you can't. + +The workaround for this is to switch to a 64-bit signed representation of time +as seconds from the Unix epoch. This representation will work for more than 250 +billion years. + +Working around Y2038 requires fixing the Linux kernel, the C libraries, and +any user code around which uses 32-bit epoch representations. + +There is Y2038-proofing work in progress on the Linux and GNU glibc front. + +## What is the Y2038 cppcheck addon? + +The Y2038 cppcheck addon is a tool to help detect code which might need fixing +because it is Y2038-unsafe. This may be because it uses types or functions from +GNU libc or from the Linux kernel which are known not to be Y2038-proof. + +## How does the Y2038 cppcheck addon work? + +The Y2038 addon is a comprehensive tool designed to audit your project for Y2038 compliance. It provides a streamlined, intelligent approach to Y2038 analysis. + +### Primary Usage: Cppcheck Integration with Project Files + +The Y2038 addon integrates seamlessly with cppcheck's core project parsing infrastructure. For optimal analysis, use the addon with project files: + +```bash +cppcheck --project=build/compile_commands.json --addon=y2038 +``` + +For single files, you can also use: +```bash +cppcheck --addon=y2038 source_file.c +``` + +#### Implementation Details + +The addon leverages cppcheck's built-in project parsing capabilities: + +- **Core Integration**: Y2038-related compiler flags are extracted by cppcheck core during project parsing and passed through dump file configuration +- **Automatic Flag Detection**: Cppcheck automatically detects Y2038-relevant flags (`-D_TIME_BITS=64`, `-D_FILE_OFFSET_BITS=64`, `-D_USE_TIME_BITS64`) from compilation commands +- **Clean Architecture**: No redundant file parsing - cppcheck handles project files once, addon focuses on analysis +- **Priority Logic**: Dump file configuration (from cppcheck's project parsing) takes precedence over source code `#define` statements +- **Source Fallback**: When no project configuration is available, the addon analyzes source code `#define` statements + +This architecture ensures optimal performance and maintains clean separation of concerns between cppcheck core (project parsing) and addon (analysis logic). + +The output is the standard Cppcheck analysis report, focused on Y2038-related issues. + +## Requirements + +The Y2038 addon works with any cppcheck installation and requires no additional dependencies beyond cppcheck itself. + +For optimal Y2038 analysis, ensure your project uses a supported build system that generates `compile_commands.json`: + +- **CMake**: Use `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` +- **Bear**: For Make/Autotools projects, use `bear` to generate compile commands +- **Ninja**: Use `ninja -t compdb` to generate compile commands +- **Bazel**: Use `bazel aquery` with appropriate flags + +If using `bear` for Make-based projects, install it via your package manager: + +```bash +# On Debian/Ubuntu +sudo apt-get install bear + +# On Fedora +sudo dnf install bear + +# On macOS (using Homebrew) +brew install bear +``` + +## How to use the Y2038 cppcheck addon + +### **Auditing Your Project for Y2038 Compliance** + +The Y2038 addon seamlessly integrates with your existing cppcheck workflow. + +**For projects with compile_commands.json (recommended):** + +```bash +cppcheck --project=build/compile_commands.json --addon=y2038 +``` + +**For single file analysis:** + +```bash +cppcheck --addon=y2038 source_file.c +``` + +**For project-wide analysis without compile_commands.json:** + +```bash +cppcheck --addon=y2038 src/ +``` + +The integration automatically: + +1. **Extracts Y2038 flags** from your project's compilation commands via cppcheck's project parsing +2. **Passes flag information** through dump file configuration to the addon +3. **Analyzes source code** with proper Y2038 context from both build system and source directives +4. **Reports Y2038 issues** using cppcheck's standard error reporting format + +### **CI/CD Integration** + +For CI/CD integration, use the Y2038 addon with your project's build configuration: + +```sh +# Example CI script with compile_commands.json +#!/bin/bash +# Generate compile_commands.json (if not already available) +cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build +# or: bear -- make + +# Run Y2038 analysis +cppcheck --project=build/compile_commands.json --addon=y2038 --error-exitcode=1 + +# The addon will return a non-zero exit code if Y2038 issues are found. +# The output is the standard Cppcheck report. +``` + +**For projects without compile_commands.json:** + +```sh +# Example CI script for source-only analysis +#!/bin/bash +cppcheck --addon=y2038 --error-exitcode=1 src/ +``` + +## Testing + +The Y2038 addon includes comprehensive test suites to ensure reliability and correctness: + +### Running Y2038 Addon Tests + +To run the Y2038 addon tests, execute: + +```bash +# Run the main Y2038 addon tests +python3 -m pytest addons/test/y2038_test.py -v + +# Run the build system integration tests +python3 -m pytest addons/test/test_y2038_buildsystem.py -v + +# Run all Y2038-related tests +python3 -m pytest addons/test/ -k y2038 -v +``` + +### Test Coverage + +The test suite covers: + +- **Core Y2038 detection logic**: Testing identification of Y2038-unsafe functions and types +- **Compiler flag parsing**: Validation of `_TIME_BITS`, `_FILE_OFFSET_BITS`, and `_USE_TIME_BITS64` detection +- **Build system integration**: Testing automatic build system detection and `compile_commands.json` generation +- **Priority-based flag resolution**: Ensuring build system flags take precedence over source directives +- **Warning suppression**: Verifying proper Y2038-safe configuration detection and warning suppression +- **Error reporting**: Testing accurate error messages and source attribution + +### Test Structure + +- `addons/test/y2038_test.py` - Core addon functionality tests +- `addons/test/test_y2038_buildsystem.py` - Build system integration tests + +The tests use mock objects and temporary directories to simulate various project configurations and build systems without requiring actual build tools to be installed. diff --git a/addons/doc/y2038.txt b/addons/doc/y2038.txt deleted file mode 100644 index 990b24bd483..00000000000 --- a/addons/doc/y2038.txt +++ /dev/null @@ -1,151 +0,0 @@ -README of the Y2038 cppcheck addon -================================== - -Contents - -1. What is Y2038? -2. What is the Y2038 cppcheck addon? -3. How does the Y2038 cppcheck addon work? -4. How to use the Y2038 cppcheck addon - ---- - -1. What is Y2038? - -In a few words: - -In Linux, the current date and time is kept as the number of seconds elapsed -since the Unix epoch, that is, since January 1st, 1970 at 00:00:00 GMT. - -Most of the time, this representation is stored as a 32-bit signed quantity. - -On January 19th, 2038 at 03:14:07 GMT, such 32-bit representations will reach -their maximum positive value. - -What happens then is unpredictable: system time might roll back to December -13th, 1901 at 19:55:13, or it might keep running on until February 7th, 2106 -at 06:28:15 GMT, or the computer may freeze, or just about anything you can -think of, plus a few ones you can't. - -The workaround for this is to switch to a 64-bit signed representation of time -as seconds from the Unix epoch. This representation will work for more than 250 -billion years. - -Working around Y2038 requires fixing the Linux kernel, the C libraries, and -any user code around which uses 32-bit epoch representations. - -There is Y2038-proofing work in progress on the Linux and GNU glibc front. - -2. What is the Y2038 cppcheck addon? - -The Y2038 cppcheck addon is a tool to help detect code which might need fixing -because it is Y2038-unsafe. This may be because it uses types or functions from -GNU libc or from the Linux kernel which are known not to be Y2038-proof. - -3. How does the Y2038 cppcheck addon work? - -The Y2038 cppcheck addon takes XML dumps produced by cppcheck from source code -files and looks for the names of types or functions which are known to be Y2038- -unsafe, and emits diagnostics whenever it finds one. - -Of course, this is of little use if your code uses a Y2038-proof glibc and -correctly configured Y2038-proof time support. - -This is why y2038.py takes into account two preprocessor directives: -_TIME_BITS and __USE_TIME_BITS64. - -_TIME_BITS is defined equal to 64 by user code when it wants 64-bit time -support from the GNU glibc. Code which does not define _TIME_BITS equal to 64 -(or defines it to something else than 64) runs a risk of not being Y2038-proof. - -__USE_TIME_BITS64 is defined by the GNU glibc when it actually provides 64-bit -time support. When this is defined, then all glibc symbols, barring bugs, are -Y2038-proof (but your code might have its own Y2038 bugs, if it handles signed -32-bit Unix epoch values). - -The Y2038 cppcheck performs the following checks: - - 1. Upon meeting a definition for _TIME_BITS, if that definition does not - set it equal to 64, this error diagnostic is emitted: - - Error: _TIME_BITS must be defined equal to 64 - - This case is very unlikely but might result from a typo, so pointing - it out is quite useful. Note that definitions of _TIME_BITS as an - expression evaluating to 64 will be flagged too. - - 2. Upon meeting a definition for _USE_TIME_BITS64, if _TIME_BITS is not - defined equal to 64, this information diagnostic is emitted: - - Warning: _USE_TIME_BITS64 is defined but _TIME_BITS was not - - This reflects the fact that even though the glibc checked default to - 64-bit time support, this was not requested by the user code, and - therefore the user code might fail Y2038 if built against a glibc - which defaults to 32-bit time support. - - 3. Upon meeting a symbol (type or function) which is known to be Y2038- - unsafe, if _USE_TIME_BITS64 is undefined or _TIME_BITS not properly - defined, this warning diagnostic is emitted: - - Warning: is Y2038-unsafe - - This reflects the fact that the user code is referring to a symbol - which, when glibc defaults to 32-bit time support, might fail Y2038. - -General note: y2038.py will handle multiple configurations, and will -emit diagnostics for each configuration in turn. - -4. How to use the Y2038 cppcheck addon - -The Y2038 cppcheck addon is used like any other cppcheck addon: - - cppcheck --dump file1.c [ file2.c [...]]] - y2038.py file1.c [ file2.c [...]]] - -Sample test C file is provided: - - test/y2038-test-1-bad-time-bits.c - test/y2038-test-2-no-time-bits.c - test/y2038-test-3-no-use-time-bits.c - test/y2038-test-4-good.c - -These cover the cases described above. You can run them through cppcheck -and y2038.py to see for yourself how the addon diagnostics look like. If -this README is not outdated (and if it is, feel free to submit a patch), -you can run cppcheck on these files as on any others: - - cppcheck --dump addons/y2038/test/y2038-*.c - y2038.py addons/y2038/test/y2038-*.dump - -If you have not installed cppcheck yet, you will have to run these -commands from the root of the cppcheck repository: - - make - sudo make install - ./cppcheck --dump addons/y2038/test/y2038-*.c - PYTHONPATH=addons python addons/y2038/y2038.py addons/y2038/test/y2038-*.c.dump - -In both cases, y2038.py execution should result in the following: - -Checking addons/y2038/test/y2038-test-1-bad-time-bits.c.dump... -Checking addons/y2038/test/y2038-test-1-bad-time-bits.c.dump, config ""... -Checking addons/y2038/test/y2038-test-2-no-time-bits.c.dump... -Checking addons/y2038/test/y2038-test-2-no-time-bits.c.dump, config ""... -Checking addons/y2038/test/y2038-test-3-no-use-time-bits.c.dump... -Checking addons/y2038/test/y2038-test-3-no-use-time-bits.c.dump, config ""... -Checking addons/y2038/test/y2038-test-4-good.c.dump... -Checking addons/y2038/test/y2038-test-4-good.c.dump, config ""... -# Configuration "": -# Configuration "": -[addons/y2038/test/y2038-test-1-bad-time-bits.c:8]: (error) _TIME_BITS must be defined equal to 64 -[addons/y2038/test/y2038-inc.h:9]: (warning) _USE_TIME_BITS64 is defined but _TIME_BITS was not -[addons/y2038/test/y2038-test-1-bad-time-bits.c:10]: (information) addons/y2038/test/y2038-inc.h was included from here -[addons/y2038/test/y2038-inc.h:9]: (warning) _USE_TIME_BITS64 is defined but _TIME_BITS was not -[addons/y2038/test/y2038-test-2-no-time-bits.c:8]: (information) addons/y2038/test/y2038-inc.h was included from here -[addons/y2038/test/y2038-test-3-no-use-time-bits.c:13]: (warning) timespec is Y2038-unsafe -[addons/y2038/test/y2038-test-3-no-use-time-bits.c:15]: (warning) clock_gettime is Y2038-unsafe - -Note: y2038.py recognizes option --template as cppcheck does, including -pre-defined templates 'gcc', 'vs' and 'edit'. The short form -t is also -recognized. diff --git a/addons/test/test_y2038_buildsystem.py b/addons/test/test_y2038_buildsystem.py new file mode 100644 index 00000000000..412a35c9fcb --- /dev/null +++ b/addons/test/test_y2038_buildsystem.py @@ -0,0 +1,170 @@ +import unittest +from unittest.mock import patch, call +import sys +import tempfile +import shutil +import json +import io +from contextlib import redirect_stdout +from pathlib import Path + +# Add addons and tools to sys.path to allow for imports +# Assuming the script is run from the root of the cppcheck repository +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +try: + from addons import y2038_buildsystem +except ImportError: + # Fallback for different directory structures + import y2038_buildsystem + +class TestY2038Buildsystem(unittest.TestCase): + + def setUp(self): + self.test_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def _create_dummy_project(self, build_system_file): + project_dir = Path(self.test_dir) + (project_dir / build_system_file).touch() + return project_dir + + def _create_dummy_compile_commands(self, directory): + content = [ + { + "directory": str(directory), + "command": "gcc -o test.o -c src/test.c", + "file": "src/test.c" + } + ] + compile_commands_path = directory / "compile_commands.json" + with open(compile_commands_path, 'w') as f: + json.dump(content, f) + return compile_commands_path + + @patch('addons.y2038_buildsystem.run_command') + def test_cmake_project(self, mock_run_command): + """Test detection and handling of a CMake project.""" + project_dir = self._create_dummy_project("CMakeLists.txt") + build_dir = project_dir / "build" + + def mock_cmake_run(*args, **kwargs): + self._create_dummy_compile_commands(build_dir) + # After creating the dummy file, we need to copy it to the project root + shutil.copy(build_dir / "compile_commands.json", project_dir) + return True + + mock_run_command.side_effect = mock_cmake_run + + # Test the library function directly + result = y2038_buildsystem.generate_compile_commands(str(project_dir)) + + # Check that the build command was called + mock_run_command.assert_called_once_with( + ["cmake", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", ".."], + cwd=build_dir.resolve() + ) + + # Verify the function returned success + self.assertTrue(result) + + + @patch('addons.y2038_buildsystem.run_command') + @patch('shutil.which', return_value='/usr/bin/bear') + def test_make_project(self, mock_shutil_which, mock_run_command): + """Test detection and handling of a Make project.""" + project_dir = self._create_dummy_project("Makefile") + + def mock_bear_run(*args, **kwargs): + self._create_dummy_compile_commands(project_dir) + return True + + mock_run_command.side_effect = mock_bear_run + + # Test the library function directly + result = y2038_buildsystem.generate_compile_commands(str(project_dir)) + + mock_run_command.assert_called_once_with( + ["bear", "--", "make"], + cwd=project_dir.resolve() + ) + + # Verify the function returned success + self.assertTrue(result) + + + @patch('addons.y2038_buildsystem.run_command') + @patch('shutil.which', return_value='/usr/bin/bear') + def test_autotools_project(self, mock_shutil_which, mock_run_command): + """Test detection and handling of an Autotools project.""" + project_dir = self._create_dummy_project("configure") + (project_dir / "Makefile").touch() + + def mock_autotools_run(cmd, cwd): + if cmd[0] == "./configure": + return True + if cmd[0] == "bear": + self._create_dummy_compile_commands(project_dir) + return True + return False + + mock_run_command.side_effect = mock_autotools_run + + # Test the library function directly + result = y2038_buildsystem.generate_compile_commands(str(project_dir)) + + expected_calls = [ + call(["./configure"], cwd=project_dir.resolve()), + call(["bear", "--", "make"], cwd=project_dir.resolve()) + ] + mock_run_command.assert_has_calls(expected_calls) + + # Verify the function returned success + self.assertTrue(result) + + @patch('addons.y2038_buildsystem.detect_build_system', return_value=None) + def test_no_build_system_detected(self, mock_detect): + """Test that the script exits gracefully if no build system is detected.""" + project_dir = Path(self.test_dir) + with redirect_stdout(io.StringIO()): + with patch.object(sys, 'argv', ['y2038_buildsystem.py', str(project_dir)]): + with self.assertRaises(SystemExit) as cm: + y2038_buildsystem.main() + self.assertEqual(cm.exception.code, 1) + + @patch('addons.y2038_buildsystem.generate_for_cmake', return_value=False) + @patch('addons.y2038_buildsystem.detect_build_system', return_value='cmake') + def test_generation_failure(self, mock_detect, mock_generate): + """Test that the script exits gracefully if compile_commands.json generation fails.""" + project_dir = Path(self.test_dir) + with redirect_stdout(io.StringIO()): + with patch.object(sys, 'argv', ['y2038_buildsystem.py', str(project_dir)]): + with self.assertRaises(SystemExit) as cm: + y2038_buildsystem.main() + self.assertEqual(cm.exception.code, 1) + + def test_library_usage(self): + """Test that y2038_buildsystem can be used as a library""" + project_dir = self._create_dummy_project("CMakeLists.txt") + + # Test generate_compile_commands function + result = y2038_buildsystem.generate_compile_commands(str(project_dir)) + # The function should return False since we don't have actual CMake installed + # but it should not raise an exception + self.assertIsInstance(result, bool) + + # Test detect_build_system function + build_system = y2038_buildsystem.detect_build_system(str(project_dir)) + self.assertEqual(build_system, "cmake") + + # Test with no build system + empty_project = Path(self.test_dir) / "empty" + empty_project.mkdir() + build_system = y2038_buildsystem.detect_build_system(str(empty_project)) + self.assertIsNone(build_system) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/addons/test/y2038/y2038-test-buildsystem.c b/addons/test/y2038/y2038-test-buildsystem.c new file mode 100644 index 00000000000..f2ca9b9827d --- /dev/null +++ b/addons/test/y2038/y2038-test-buildsystem.c @@ -0,0 +1,25 @@ +/* + * Test case for Y2038 addon build system integration + * + * This file tests build system integration scenarios: + * - Build system flags take precedence over source directives + * - Proper build system Y2038 configuration detection + * - Fallback to source code analysis when no build system is present + * + * The same source code is used for different scenarios - differentiation happens + * through the presence/absence of compile_commands.json and its contents. + */ + +#include + +int main(int argc, char **argv) +{ + time_t current_time; + struct timespec ts; + + current_time = time(NULL); + clock_gettime(CLOCK_REALTIME, &ts); + + return 0; +} + diff --git a/addons/test/y2038/y2038-test-compiler-flags.c b/addons/test/y2038/y2038-test-compiler-flags.c new file mode 100644 index 00000000000..29cabb3dd80 --- /dev/null +++ b/addons/test/y2038/y2038-test-compiler-flags.c @@ -0,0 +1,24 @@ +/* + * Shared test case for Y2038 addon compiler flag testing + * + * This file tests various compiler flag scenarios: + * - Proper Y2038 configuration: -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -D_USE_TIME_BITS64 + * - Incorrect _TIME_BITS value: -D_TIME_BITS=32 + * - Incomplete configuration: -D_USE_TIME_BITS64 (without _TIME_BITS) + * + * The same source code is used for all scenarios - differentiation happens + * through the compiler flags passed to cppcheck during dump creation. + */ + +#include + +int main(int argc, char **argv) +{ + time_t current_time; + struct timespec ts; + + current_time = time(NULL); + clock_gettime(CLOCK_REALTIME, &ts); + + return 0; +} \ No newline at end of file diff --git a/addons/test/y2038_test.py b/addons/test/y2038_test.py index 282e55650d7..861b006d8bf 100644 --- a/addons/test/y2038_test.py +++ b/addons/test/y2038_test.py @@ -21,18 +21,28 @@ './addons/test/y2038/y2038-test-4-good.c', './addons/test/y2038/y2038-test-5-good-no-time-used.c'] +# Build system test file (for testing build system integration) +BUILD_SYSTEM_TEST_FILE = './addons/test/y2038/y2038-test-buildsystem.c' + def setup_module(module): sys.argv.append("--cli") + + # Create dumps for regular test files for f in TEST_SOURCE_FILES: dump_create(f) + # For build system tests, we'll create dumps on-demand in each test + # to avoid conflicts from multiple dump_create calls on the same file + def teardown_module(module): sys.argv.remove("--cli") for f in TEST_SOURCE_FILES: dump_remove(f) + # Build system test dumps are cleaned up individually in each test method + def test_1_bad_time_bits(capsys): is_safe = check_y2038_safe('./addons/test/y2038/y2038-test-1-bad-time-bits.c.dump', quiet=True) @@ -107,6 +117,31 @@ def test_5_good(capsys): assert(len([c for c in unsafe_calls if c['file'].endswith('.c')]) == 0) +def test_build_system_integration(): + """Test that Y2038 flags are properly parsed from cppcheck dump file configuration""" + from addons.y2038 import parse_dump_config + + # Test Y2038-safe configuration string (as cppcheck would generate it) + config_string = "_TIME_BITS=64;_FILE_OFFSET_BITS=64;_USE_TIME_BITS64" + result = parse_dump_config(config_string) + # Y2038-safe flags should be detected + assert result['time_bits_defined'] is True + assert result['time_bits_value'] == 64 + assert result['use_time_bits64_defined'] is True + assert result['file_offset_bits_defined'] is True + assert result['file_offset_bits_value'] == 64 + + # Test partial Y2038 configuration + partial_config = "_TIME_BITS=32;_FILE_OFFSET_BITS=64" + result = parse_dump_config(partial_config) + # Should detect the flags with their actual values + assert result['time_bits_defined'] is True + assert result['time_bits_value'] == 32 # Not Y2038-safe value + assert result['use_time_bits64_defined'] is False + assert result['file_offset_bits_defined'] is True + assert result['file_offset_bits_value'] == 64 + + def test_arguments_regression(): args_ok = ["-t=foo", "--template=foo", "-q", "--quiet", @@ -137,4 +172,35 @@ def test_arguments_regression(): pytest.fail("Unexpected SystemExit with '%s'" % arg) sys.argv.remove(arg) finally: - sys.argv = sys_argv_old \ No newline at end of file + sys.argv = sys_argv_old + + +def test_parse_dump_config(): + """Test the parse_dump_config function for cppcheck dump file integration""" + from addons.y2038 import parse_dump_config + + # Test comprehensive Y2038 configuration + full_config = "_TIME_BITS=64;_FILE_OFFSET_BITS=64;_USE_TIME_BITS64;OTHER_FLAG=1" + result = parse_dump_config(full_config) + + assert result['time_bits_defined'] is True + assert result['time_bits_value'] == 64 + assert result['use_time_bits64_defined'] is True + assert result['file_offset_bits_defined'] is True + assert result['file_offset_bits_value'] == 64 + + # Test empty configuration + result = parse_dump_config("") + assert result['time_bits_defined'] is False + assert result['time_bits_value'] is None + assert result['use_time_bits64_defined'] is False + assert result['file_offset_bits_defined'] is False + assert result['file_offset_bits_value'] is None + # Test Y2038-unsafe configuration + unsafe_config = "_TIME_BITS=32;_FILE_OFFSET_BITS=32" + result = parse_dump_config(unsafe_config) + assert result['time_bits_defined'] is True + assert result['time_bits_value'] == 32 + assert result['use_time_bits64_defined'] is False + assert result['file_offset_bits_defined'] is True + assert result['file_offset_bits_value'] == 32 \ No newline at end of file diff --git a/addons/y2038.py b/addons/y2038.py index 93d237afd06..b17d41dd330 100755 --- a/addons/y2038.py +++ b/addons/y2038.py @@ -2,14 +2,33 @@ # # cppcheck addon for Y2038 safeness detection # +# This addon provides comprehensive Y2038 (Year 2038 Problem) detection for C/C++ code. +# It extracts compiler flags from cppcheck dump file configuration to determine +# Y2038 safety, suppressing warnings when proper configuration is detected. +# +# Key Features: +# - Extraction of Y2038-related flags from cppcheck dump file configuration +# - Compiler flag parsing and validation from cppcheck's project parsing +# - Warning suppression when proper Y2038 configuration is found +# - Support for both _TIME_BITS=64 and _FILE_OFFSET_BITS=64 requirements +# - Priority-based flag resolution (dump file configuration > source code directives) +# # Detects: # # 1. _TIME_BITS being defined to something else than 64 bits -# 2. _USE_TIME_BITS64 being defined when _TIME_BITS is not -# 3. Any Y2038-unsafe symbol when _USE_TIME_BITS64 is not defined. +# 2. _FILE_OFFSET_BITS being defined to something else than 64 bits +# 3. _USE_TIME_BITS64 being defined when _TIME_BITS is not +# 4. Any Y2038-unsafe symbol when proper Y2038 configuration is not present +# 5. Dump file configurations that affect Y2038 safety +# +# Warning Suppression: +# When both _TIME_BITS=64 AND _FILE_OFFSET_BITS=64 are detected (prioritizing dump file +# configuration over source code directives), Y2038 warnings are suppressed and an +# informational message is displayed instead. # # Example usage: # $ cppcheck --addon=y2038 path-to-src/test.c +# $ cppcheck --dump file.c && python3 y2038.py file.c.dump # from __future__ import print_function @@ -18,13 +37,23 @@ import sys import re +# Y2038 flags are extracted by cppcheck core during project parsing +# and passed through dump file configuration - no redundant parsing needed + +# -------------------------------- +# Y2038 safety constants +# -------------------------------- + +# Y2038-safe bit values +Y2038_SAFE_TIME_BITS = 64 +Y2038_SAFE_FILE_OFFSET_BITS = 64 # -------------------------------------------- # #define/#undef detection regular expressions # -------------------------------------------- # test for '#define _TIME_BITS 64' -re_define_time_bits_64 = re.compile(r'^\s*#\s*define\s+_TIME_BITS\s+64\s*$') +re_define_time_bits_64 = re.compile(rf'^\s*#\s*define\s+_TIME_BITS\s+{Y2038_SAFE_TIME_BITS}\s*$') # test for '#define _TIME_BITS ...' (combine w/ above to test for 'not 64') re_define_time_bits = re.compile(r'^\s*#\s*define\s+_TIME_BITS\s+.*$') @@ -38,6 +67,34 @@ # test for '#undef _USE_TIME_BITS64' (if it ever happens) re_undef_use_time_bits64 = re.compile(r'^\s*#\s*undef\s+_USE_TIME_BITS64\s*$') +# test for '#define _FILE_OFFSET_BITS 64' +re_define_file_offset_bits_64 = re.compile(rf'^\s*#\s*define\s+_FILE_OFFSET_BITS\s+{Y2038_SAFE_FILE_OFFSET_BITS}\s*$') + +# test for '#define _FILE_OFFSET_BITS ...' (combine w/ above to test for 'not 64') +re_define_file_offset_bits = re.compile(r'^\s*#\s*define\s+_FILE_OFFSET_BITS\s+.*$') + +# test for '#undef _FILE_OFFSET_BITS' (if it ever happens) +re_undef_file_offset_bits = re.compile(r'^\s*#\s*undef\s+_FILE_OFFSET_BITS\s*$') + +# -------------------------------------------- +# Compiler flag parsing regular expressions +# -------------------------------------------- + +# test for '_TIME_BITS=64' in compiler flags +re_flag_time_bits_64 = re.compile(rf'_TIME_BITS={Y2038_SAFE_TIME_BITS}(?:\s|$|;)') + +# test for '_TIME_BITS=...' in compiler flags (combine w/ above to test for 'not 64') +re_flag_time_bits = re.compile(r'_TIME_BITS=(\d+)') + +# test for '_USE_TIME_BITS64' in compiler flags +re_flag_use_time_bits64 = re.compile(r'_USE_TIME_BITS64(?:\s|$|=|;)') + +# test for '_FILE_OFFSET_BITS=64' in compiler flags +re_flag_file_offset_bits_64 = re.compile(rf'_FILE_OFFSET_BITS={Y2038_SAFE_FILE_OFFSET_BITS}(?:\s|$|;)') + +# test for '_FILE_OFFSET_BITS=...' in compiler flags +re_flag_file_offset_bits = re.compile(r'_FILE_OFFSET_BITS=(\d+)') + # -------------------------------- # List of Y2038-unsafe identifiers # -------------------------------- @@ -147,13 +204,142 @@ } + + + + + + + +def parse_dump_config(config_name): + """ + Parse Y2038-related flags from cppcheck dump file configuration name. + + This function analyzes the cppcheck dump file configuration name (which contains + preprocessor definitions extracted by cppcheck from project files like compile_commands.json) + to extract Y2038-related definitions. It looks for _TIME_BITS, _USE_TIME_BITS64, and + _FILE_OFFSET_BITS definitions and validates their values. + + Args: + config_name (str): The cppcheck configuration name from dump file + (e.g., "_TIME_BITS=64;_FILE_OFFSET_BITS=64") + + Returns: + dict: Dictionary containing Y2038-related flag information with keys: + - 'time_bits_defined' (bool): Whether _TIME_BITS is defined + - 'time_bits_value' (int|None): Value of _TIME_BITS (None if not defined) + - 'use_time_bits64_defined' (bool): Whether _USE_TIME_BITS64 is defined + - 'file_offset_bits_defined' (bool): Whether _FILE_OFFSET_BITS is defined + - 'file_offset_bits_value' (int|None): Value of _FILE_OFFSET_BITS (None if not defined) + + Example: + >>> parse_dump_config("_TIME_BITS=64;_FILE_OFFSET_BITS=64") + { + 'time_bits_defined': True, + 'time_bits_value': 64, + 'use_time_bits64_defined': False, + 'file_offset_bits_defined': True, + 'file_offset_bits_value': 64 + } + """ + result = { + 'time_bits_defined': False, + 'time_bits_value': None, + 'use_time_bits64_defined': False, + 'file_offset_bits_defined': False, + 'file_offset_bits_value': None + } + + if not config_name: + return result + + try: + # Check for _TIME_BITS=64 (correct value) + if re_flag_time_bits_64.search(config_name): + result['time_bits_defined'] = True + result['time_bits_value'] = Y2038_SAFE_TIME_BITS + else: + # Check for _TIME_BITS=other_value + match = re_flag_time_bits.search(config_name) + if match: + result['time_bits_defined'] = True + try: + result['time_bits_value'] = int(match.group(1)) + except (ValueError, IndexError): + # Malformed _TIME_BITS value, treat as undefined + result['time_bits_defined'] = False + result['time_bits_value'] = None + + # Check for _USE_TIME_BITS64 + if re_flag_use_time_bits64.search(config_name): + result['use_time_bits64_defined'] = True + + # Check for _FILE_OFFSET_BITS=64 (correct value) + if re_flag_file_offset_bits_64.search(config_name): + result['file_offset_bits_defined'] = True + result['file_offset_bits_value'] = Y2038_SAFE_FILE_OFFSET_BITS + else: + # Check for _FILE_OFFSET_BITS=other_value + match = re_flag_file_offset_bits.search(config_name) + if match: + result['file_offset_bits_defined'] = True + try: + result['file_offset_bits_value'] = int(match.group(1)) + except (ValueError, IndexError): + # Malformed _FILE_OFFSET_BITS value, treat as undefined + result['file_offset_bits_defined'] = False + result['file_offset_bits_value'] = None + + except (AttributeError, TypeError, ValueError): + # If any unexpected error occurs during parsing, return empty result + # This ensures the addon continues to work even with malformed configurations + # Note: We catch specific exceptions rather than broad Exception for better debugging + pass + + return result + + def check_y2038_safe(dumpfile, quiet=False): + """ + Main function to check Y2038 safety of C/C++ code from cppcheck dump files. + + This function performs comprehensive Y2038 analysis including: + 1. Extraction of Y2038-related compiler flags from cppcheck dump file configuration + 2. Analysis of source code preprocessor directives + 3. Warning suppression when proper Y2038 configuration is detected + 4. Reporting of Y2038-unsafe symbols and configurations + + The function implements a priority-based approach for Y2038 flag detection: + - Dump file configuration (from cppcheck's project parsing - highest priority) + - Source code #define directives (fallback) + + Warning suppression occurs when both _TIME_BITS=64 AND _FILE_OFFSET_BITS=64 + are detected from any source. When warnings are suppressed, an informational + message is displayed indicating the configuration source and suppression count. + + Args: + dumpfile (str): Path to the cppcheck XML dump file (.dump extension) + quiet (bool, optional): If True, suppress informational messages. Defaults to False. + + Returns: + bool: True if code is Y2038-safe, False if Y2038 issues were detected + + Raises: + Exception: May raise exceptions from cppcheckdata parsing or file I/O operations + + Example: + >>> check_y2038_safe("test.c.dump") # Normal operation + True + >>> check_y2038_safe("test.c.dump", quiet=True) # Suppress info messages + False + """ # Assume that the code is Y2038 safe until proven otherwise y2038safe = True # load XML from .dump file data = cppcheckdata.CppcheckData(dumpfile) srcfile = data.files[0] + for cfg in data.iterconfigurations(): if not quiet: print('Checking %s, config %s...' % (srcfile, cfg.name)) @@ -162,56 +348,231 @@ def check_y2038_safe(dumpfile, quiet=False): time_bits_defined = False srclinenr = 0 + # Priority-based flag detection: dump file configuration > source code directives + # 1. Check dump file configuration (from cppcheck's project parsing - highest priority) + dump_config_flags = parse_dump_config(cfg.name) + # Initialize effective flags with dump file configuration + effective_flags = { + 'time_bits_defined': dump_config_flags['time_bits_defined'], + 'time_bits_value': dump_config_flags['time_bits_value'], + 'use_time_bits64_defined': dump_config_flags['use_time_bits64_defined'], + 'file_offset_bits_defined': dump_config_flags['file_offset_bits_defined'], + 'file_offset_bits_value': dump_config_flags['file_offset_bits_value'] + } + + # Determine configuration source for reporting + config_source = None + has_dump_config = (dump_config_flags['time_bits_defined'] or + dump_config_flags['file_offset_bits_defined'] or + dump_config_flags['use_time_bits64_defined']) + if has_dump_config: + config_source = "cppcheck configuration" + + # Track time_bits_defined for _USE_TIME_BITS64 validation + time_bits_defined = effective_flags['time_bits_defined'] + + # Check effective _TIME_BITS value (from dump file configuration) + if effective_flags['time_bits_defined']: + if effective_flags['time_bits_value'] != Y2038_SAFE_TIME_BITS: + fake_directive = type('FakeDirective', (), { + 'file': srcfile, 'linenr': 0, 'column': 0, + 'str': 'cppcheck configuration: _TIME_BITS=%s' % effective_flags['time_bits_value'] + })() + cppcheckdata.reportError(fake_directive, 'error', + '_TIME_BITS must be defined equal to 64 (found in cppcheck configuration: _TIME_BITS=%s)' % effective_flags['time_bits_value'], + 'y2038', + 'type-bits-not-64') + y2038safe = False + + # Check effective _FILE_OFFSET_BITS value (from dump file configuration) + if effective_flags['file_offset_bits_defined']: + if effective_flags['file_offset_bits_value'] != Y2038_SAFE_FILE_OFFSET_BITS: + fake_directive = type('FakeDirective', (), { + 'file': srcfile, 'linenr': 0, 'column': 0, + 'str': 'cppcheck configuration: _FILE_OFFSET_BITS=%s' % effective_flags['file_offset_bits_value'] + })() + cppcheckdata.reportError(fake_directive, 'error', + '_FILE_OFFSET_BITS must be defined equal to 64 (found in cppcheck configuration: _FILE_OFFSET_BITS=%s)' % effective_flags['file_offset_bits_value'], + 'y2038', + 'file-offset-bits-not-64') + y2038safe = False + + # Check effective _USE_TIME_BITS64 (from dump file configuration) + if effective_flags['use_time_bits64_defined']: + if not time_bits_defined: + # _USE_TIME_BITS64 defined without _TIME_BITS is problematic + fake_directive = type('FakeDirective', (), { + 'file': srcfile, 'linenr': 0, 'column': 0, + 'str': 'cppcheck configuration: _USE_TIME_BITS64' + })() + cppcheckdata.reportError(fake_directive, 'warning', + '_USE_TIME_BITS64 is defined in cppcheck configuration but _TIME_BITS was not', + 'y2038', + 'type-bits-undef') + y2038safe = False + else: + # _USE_TIME_BITS64 defined WITH _TIME_BITS - this is correct + safe = 0 # Start of file is safe + + # 2. Fallback to source code directives when dump file configuration is not available + source_time_bits_defined = False # pylint: disable=unused-variable + source_file_offset_bits_defined = False # pylint: disable=unused-variable + source_file_offset_bits_value = None # pylint: disable=unused-variable + source_use_time_bits64_defined = False # pylint: disable=unused-variable + + # Track which flags came from source code for mixed scenario reporting + source_flags_used = { + 'time_bits': False, + 'file_offset_bits': False, + 'use_time_bits64': False + } for directive in cfg.directives: # track source line number if directive.file == srcfile: srclinenr = directive.linenr + + # Process source code directives as fallback when dump config is not available # check for correct _TIME_BITS if present if re_define_time_bits_64.match(directive.str): - time_bits_defined = True + source_time_bits_defined = True + # Only use source directive if dump config doesn't define _TIME_BITS + if not effective_flags['time_bits_defined']: + effective_flags['time_bits_defined'] = True + effective_flags['time_bits_value'] = Y2038_SAFE_TIME_BITS + time_bits_defined = True + source_flags_used['time_bits'] = True elif re_define_time_bits.match(directive.str): - cppcheckdata.reportError(directive, 'error', - '_TIME_BITS must be defined equal to 64', - 'y2038', - 'type-bits-not-64') - time_bits_defined = False - y2038safe = False + source_time_bits_defined = False + # Only use source directive if dump config doesn't define _TIME_BITS + if not effective_flags['time_bits_defined']: + source_flags_used['time_bits'] = True + cppcheckdata.reportError(directive, 'error', + '_TIME_BITS must be defined equal to 64', + 'y2038', + 'type-bits-not-64') + y2038safe = False elif re_undef_time_bits.match(directive.str): - time_bits_defined = False + source_time_bits_defined = False + # Only use source directive if dump config doesn't define _TIME_BITS + if not effective_flags['time_bits_defined']: + time_bits_defined = False + source_flags_used['time_bits'] = True + # check for correct _FILE_OFFSET_BITS if present + if re_define_file_offset_bits_64.match(directive.str): + source_file_offset_bits_defined = True + source_file_offset_bits_value = Y2038_SAFE_FILE_OFFSET_BITS + # Only use source directive if dump config doesn't define _FILE_OFFSET_BITS + if not effective_flags['file_offset_bits_defined']: + effective_flags['file_offset_bits_defined'] = True + effective_flags['file_offset_bits_value'] = Y2038_SAFE_FILE_OFFSET_BITS + source_flags_used['file_offset_bits'] = True + elif re_define_file_offset_bits.match(directive.str): + source_file_offset_bits_defined = False + # Only use source directive if dump config doesn't define _FILE_OFFSET_BITS + if not effective_flags['file_offset_bits_defined']: + source_flags_used['file_offset_bits'] = True + cppcheckdata.reportError(directive, 'error', + '_FILE_OFFSET_BITS must be defined equal to 64', + 'y2038', + 'file-offset-bits-not-64') + y2038safe = False + elif re_undef_file_offset_bits.match(directive.str): + source_file_offset_bits_defined = False + source_file_offset_bits_value = None + # Only use source directive if dump config doesn't define _FILE_OFFSET_BITS + if not effective_flags['file_offset_bits_defined']: + effective_flags['file_offset_bits_defined'] = False + effective_flags['file_offset_bits_value'] = None + source_flags_used['file_offset_bits'] = True + # check for _USE_TIME_BITS64 (un)definition if re_define_use_time_bits64.match(directive.str): safe = int(srclinenr) - # warn about _TIME_BITS not being defined - if not time_bits_defined: - cppcheckdata.reportError(directive, 'warning', - '_USE_TIME_BITS64 is defined but _TIME_BITS was not', - 'y2038', - 'type-bits-undef') + source_use_time_bits64_defined = True + # Only use source directive if dump config doesn't define _USE_TIME_BITS64 + if not effective_flags['use_time_bits64_defined']: + effective_flags['use_time_bits64_defined'] = True + source_flags_used['use_time_bits64'] = True + # warn about _TIME_BITS not being defined (check effective flags) + if not time_bits_defined: + cppcheckdata.reportError(directive, 'warning', + '_USE_TIME_BITS64 is defined but _TIME_BITS was not', + 'y2038', + 'type-bits-undef') elif re_undef_use_time_bits64.match(directive.str): unsafe = int(srclinenr) + source_use_time_bits64_defined = False + # Only use source directive if dump config doesn't define _USE_TIME_BITS64 + if not effective_flags['use_time_bits64_defined']: + source_flags_used['use_time_bits64'] = True # do we have a safe..unsafe area? - if unsafe > safe > 0: + if unsafe > safe >= 0: safe_ranges.append((safe, unsafe)) safe = -1 # check end of source beyond last directive if len(cfg.tokenlist) > 0: unsafe = int(cfg.tokenlist[-1].linenr) - if unsafe > safe > 0: + if unsafe > safe >= 0: safe_ranges.append((safe, unsafe)) + # Determine if Y2038 warnings should be suppressed + # Require BOTH _TIME_BITS=64 AND _FILE_OFFSET_BITS=64 for complete Y2038 safety + y2038_safe_config = ( + effective_flags['time_bits_defined'] and + effective_flags['time_bits_value'] == Y2038_SAFE_TIME_BITS and + effective_flags['file_offset_bits_defined'] and + effective_flags['file_offset_bits_value'] == Y2038_SAFE_FILE_OFFSET_BITS + ) + + # Update config_source for suppression reporting based on mixed scenarios + if y2038_safe_config: + # Determine configuration source for mixed scenarios + dump_flags_count = sum([ + dump_config_flags['time_bits_defined'], + dump_config_flags['file_offset_bits_defined'], + dump_config_flags['use_time_bits64_defined'] + ]) + source_flags_count = sum(source_flags_used.values()) + + if dump_flags_count > 0 and source_flags_count > 0: + # Mixed scenario: both dump config and source directives used + config_source = "mixed configuration (cppcheck configuration and source code directives)" + elif dump_flags_count > 0: + # Only dump config used + config_source = "cppcheck configuration" + elif source_flags_count > 0: + # Only source directives used + config_source = "source code directives" + else: + # Fallback (shouldn't happen if y2038_safe_config is True) + config_source = "configuration" + # go through all tokens + warnings_suppressed = 0 for token in cfg.tokenlist: if token.str in id_Y2038: - if not any(lower <= int(token.linenr) <= upper - for (lower, upper) in safe_ranges): - cppcheckdata.reportError(token, 'warning', - token.str + ' is Y2038-unsafe', - 'y2038', - 'unsafe-call') - y2038safe = False + is_in_safe_range = any(lower <= int(token.linenr) <= upper + for (lower, upper) in safe_ranges) + + if not is_in_safe_range: + if y2038_safe_config: + # Count suppressed warnings but don't report them + warnings_suppressed += 1 + else: + # Report the warning as before + cppcheckdata.reportError(token, 'warning', + token.str + ' is Y2038-unsafe', + 'y2038', + 'unsafe-call') + y2038safe = False token = token.next + # Print suppression message if warnings were suppressed + if warnings_suppressed > 0 and config_source and not quiet: + print('Y2038 warnings suppressed: Found proper Y2038 configuration in %s (_TIME_BITS=%d and _FILE_OFFSET_BITS=%d)' % (config_source, Y2038_SAFE_TIME_BITS, Y2038_SAFE_FILE_OFFSET_BITS)) + print('Suppressed %d Y2038-unsafe function warning(s)' % warnings_suppressed) + return y2038safe diff --git a/addons/y2038_buildsystem.py b/addons/y2038_buildsystem.py new file mode 100644 index 00000000000..7162e9d1856 --- /dev/null +++ b/addons/y2038_buildsystem.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# +# Library for build system detection and compile_commands.json generation. +# +# This module provides functions to detect build systems in project directories +# and generate compile_commands.json files. It is designed to be imported and +# used by other scripts, particularly the y2038 addon. +# + +import shutil +import subprocess +import sys +from pathlib import Path + +def print_error(message): + """Prints an error message to stderr.""" + print(f"Error: {message}", file=sys.stderr) + +def run_command(cmd, cwd): + """Runs a command in a specified directory and handles errors.""" + try: + # Ensure cwd is a resolved Path for security + if isinstance(cwd, str): + cwd = Path(cwd).resolve() + elif isinstance(cwd, Path): + cwd = cwd.resolve() + else: + raise ValueError(f"Invalid cwd type: {type(cwd)}") + print(f"Running command: {' '.join(cmd)} in {cwd}") + result = subprocess.run(cmd, cwd=cwd, check=True, capture_output=True, text=True) + print(result.stdout) + if result.stderr: + print(result.stderr, file=sys.stderr) + return True + except FileNotFoundError: + print_error(f"Command '{cmd[0]}' not found. Please ensure it is in your PATH.") + return False + except ValueError as e: + print_error(f"Invalid working directory: {e}") + return False + except OSError as e: + print_error(f"Invalid working directory: {e}") + return False + except subprocess.CalledProcessError as e: + print_error(f"Command failed with exit code {e.returncode}") + print(e.stdout) + print(e.stderr, file=sys.stderr) + return False + +def detect_build_system(project_dir): + """ + Detects the build system by looking for characteristic files. + The order of checks determines the precedence. + """ + project_path = Path(project_dir) + if (project_path / "CMakeLists.txt").exists(): + return "cmake" + if (project_path / "meson.build").exists(): + return "meson" + if (project_path / "configure").exists(): + return "autotools" + if (project_path / "Makefile").exists(): + return "make" + if (project_path / "BUILD.bazel").exists() or (project_path / "BUILD").exists(): + return "bazel" + if (project_path / "Cargo.toml").exists(): + return "cargo" + return None + +def generate_for_cmake(project_dir): + """Generates compile_commands.json for CMake projects.""" + print("CMake project detected.") + build_dir = project_dir / "build" + build_dir.mkdir(exist_ok=True) + + cmd = ["cmake", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", ".."] + if not run_command(cmd, cwd=build_dir): + return False + + generated_file = build_dir / "compile_commands.json" + if generated_file.exists(): + shutil.copy(generated_file, project_dir) + print(f"Successfully generated and copied compile_commands.json to {project_dir}") + return True + print_error("CMake did not generate compile_commands.json.") + return False + +def generate_for_meson(project_dir): + """Generates compile_commands.json for Meson projects.""" + print("Meson project detected.") + build_dir = project_dir / "build" + + # Meson requires the build directory not to exist for setup + if build_dir.exists(): + print(f"Build directory {build_dir} already exists. Meson setup will not be run again.") + else: + cmd_setup = ["meson", "setup", "build"] + if not run_command(cmd_setup, cwd=project_dir): + return False + + generated_file = build_dir / "compile_commands.json" + if generated_file.exists(): + shutil.copy(generated_file, project_dir) + print(f"Successfully generated and copied compile_commands.json to {project_dir}") + return True + print_error("Meson did not generate compile_commands.json.") + return False + +def generate_for_make_autotools(project_dir, is_autotools): + """Generates compile_commands.json for Make/Autotools projects using bear.""" + if is_autotools: + print("Autotools project detected.") + else: + print("Make project detected.") + + if not shutil.which("bear"): + print_error("'bear' is required for Make/Autotools projects but is not found in your PATH.") + print("Please install it (e.g., 'sudo apt-get install bear' or 'brew install bear') and try again.") + return False + + if is_autotools: + if not run_command(["./configure"], cwd=project_dir): + print_error("'./configure' script failed. Cannot proceed with make.") + return False + + if not run_command(["bear", "--", "make"], cwd=project_dir): + return False + + generated_file = project_dir / "compile_commands.json" + if generated_file.exists(): + print(f"Successfully generated compile_commands.json in {project_dir}") + return True + print_error("bear did not generate compile_commands.json.") + return False + +def generate_compile_commands(project_dir): + """ + Generate compile_commands.json for the given project directory. + + This function detects the build system and generates compile_commands.json + using the appropriate method for the detected build system. + + Args: + project_dir (Path): Path to the project directory + + Returns: + bool: True if generation was successful, False otherwise + """ + try: + project_dir = Path(project_dir).resolve() + except (OSError, ValueError) as e: + print_error(f"Invalid project directory path: {e}") + return False + if not project_dir.exists(): + print_error(f"Project directory does not exist: {project_dir}") + return False + if not project_dir.is_dir(): + print_error(f"Path is not a directory: {project_dir}") + return False + # Security check: prevent path traversal attacks + try: + # Check if the resolved path contains suspicious patterns + path_str = str(project_dir) + if ".." in path_str or path_str.startswith("/"): + # Allow absolute paths but validate they're reasonable + if not path_str.startswith(("/home", "/opt", "/usr/local", "/tmp", "/private/var/folders", str(Path.cwd()))): + print_error(f"Path appears to be outside expected directories: {project_dir}") + return False + except Exception as e: + print_error(f"Path validation failed: {e}") + return False + + print(f"Analyzing project in: {project_dir}") + + build_system = detect_build_system(project_dir) + + generated_successfully = False + if build_system == "cmake": + generated_successfully = generate_for_cmake(project_dir) + elif build_system == "meson": + generated_successfully = generate_for_meson(project_dir) + elif build_system == "autotools": + generated_successfully = generate_for_make_autotools(project_dir, is_autotools=True) + elif build_system == "make": + generated_successfully = generate_for_make_autotools(project_dir, is_autotools=False) + elif build_system in ["bazel", "cargo"]: + print(f"{build_system.capitalize()} project detected. Automatic generation of 'compile_commands.json' is not supported.") + print(f"Please consult the {build_system.capitalize()} documentation to generate it manually.") + return False + else: + print_error("Could not detect a supported build system (CMake, Meson, Make, Autotools).") + return False + + if not generated_successfully: + print_error("Failed to generate compile_commands.json.") + return False + + compile_commands_path = project_dir / "compile_commands.json" + if not compile_commands_path.exists(): + print_error(f"compile_commands.json not found at {compile_commands_path} after generation step.") + return False + return True + + +def main(): + """Main function for standalone script usage (deprecated - use as library instead).""" + import argparse + + parser = argparse.ArgumentParser( + description="Generate a compile_commands.json file for various build systems." + ) + parser.add_argument( + "project_directory", + nargs="?", + default=".", + help="The path to the project's root directory (default: current directory)." + ) + args = parser.parse_args() + + project_dir = Path(args.project_directory).resolve() + success = generate_compile_commands(project_dir) + if not success: + sys.exit(1) + print(f"Successfully generated compile_commands.json in {project_dir}") + + +if __name__ == "__main__": + main() diff --git a/lib/filesettings.h b/lib/filesettings.h index 2c125264bbc..3149421c629 100644 --- a/lib/filesettings.h +++ b/lib/filesettings.h @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -95,7 +96,28 @@ struct CPPCHECKLIB FileSettings { std::string defines; // TODO: handle differently std::string cppcheckDefines() const { - return defines + (msc ? ";_MSC_VER=1900" : "") + (useMfc ? ";__AFXWIN_H__=1" : ""); + std::ostringstream oss; + oss << defines; + + if (msc) { + oss << ";_MSC_VER=1900"; + } + if (useMfc) { + oss << ";__AFXWIN_H__=1"; + } + + // Add Y2038 specific flags to configuration + if (timeBitsDefined) { + oss << ";_TIME_BITS=" << timeBitsValue; + } + if (fileOffsetBitsDefined) { + oss << ";_FILE_OFFSET_BITS=" << fileOffsetBitsValue; + } + if (useTimeBits64Defined) { + oss << ";_USE_TIME_BITS64"; + } + + return oss.str(); } std::set undefs; std::list includePaths; @@ -106,6 +128,13 @@ struct CPPCHECKLIB FileSettings { // TODO: get rid of these bool msc{}; bool useMfc{}; + + // Y2038 specific configuration flags + bool timeBitsDefined{}; + int timeBitsValue{}; + bool useTimeBits64Defined{}; + bool fileOffsetBitsDefined{}; + int fileOffsetBitsValue{}; }; #endif // fileSettingsH diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 1af563ed439..e1f590f0c68 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -306,6 +306,29 @@ void ImportProject::fsParseCommand(FileSettings& fs, const std::string& command) if (!defval.empty()) defs += defval; defs += ';'; + + // Extract Y2038-specific flags + if (fval == "_TIME_BITS" && !defval.empty() && defval[0] == '=') { + const std::string valueStr = defval.substr(1); + char* endptr = nullptr; + const long value = std::strtol(valueStr.c_str(), &endptr, 10); + if (*endptr == '\0' && value >= INT_MIN && value <= INT_MAX) { + fs.timeBitsDefined = true; + fs.timeBitsValue = static_cast(value); + } + // If parsing fails, leave timeBitsDefined as false + } else if (fval == "_FILE_OFFSET_BITS" && !defval.empty() && defval[0] == '=') { + const std::string valueStr = defval.substr(1); + char* endptr = nullptr; + const long value = std::strtol(valueStr.c_str(), &endptr, 10); + if (*endptr == '\0' && value >= INT_MIN && value <= INT_MAX) { + fs.fileOffsetBitsDefined = true; + fs.fileOffsetBitsValue = static_cast(value); + } + // If parsing fails, leave fileOffsetBitsDefined as false + } else if (fval == "_USE_TIME_BITS64") { + fs.useTimeBits64Defined = true; + } } else if (F=='U') fs.undefs.insert(std::move(fval)); else if (F=='I') { diff --git a/man/manual.md b/man/manual.md index cdc692552d0..5fa03ad92f6 100644 --- a/man/manual.md +++ b/man/manual.md @@ -1037,7 +1037,7 @@ Example configuration of naming conventions: ### y2038.py -[y2038.py](https://github.com/danmar/cppcheck/blob/main/addons/y2038.py) checks Linux systems for [year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) safety. This required [modified environment](https://github.com/3adev/y2038). See complete description [here](https://github.com/danmar/cppcheck/blob/main/addons/doc/y2038.txt). +[y2038.py](../addons/doc/y2038.md) checks Linux systems for [year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) safety. ## Running Addons diff --git a/win_installer/cppcheck.wxs b/win_installer/cppcheck.wxs index 6215077384d..e2e1acdaa25 100644 --- a/win_installer/cppcheck.wxs +++ b/win_installer/cppcheck.wxs @@ -170,6 +170,7 @@ +