diff --git a/.github/workflows/broken_links_checker.yml b/.github/workflows/broken_links_checker.yml index 5a71dc43..20b770df 100644 --- a/.github/workflows/broken_links_checker.yml +++ b/.github/workflows/broken_links_checker.yml @@ -15,7 +15,15 @@ jobs: - name: Configure broken links checker run: | mkdir -p ./target - echo '{ "aliveStatusCodes": [429, 200] }' > ./target/broken_links_checker.json + echo '{ + "aliveStatusCodes": [429, 200], + "ignorePatterns": [ + { + # The Autosar TLS certificate cannot be validated from GitHub Actions. + "pattern": "^https://www.autosar.org/" + } + ] + }' > ./target/broken_links_checker.json - uses: tcort/github-action-markdown-link-check@v1 with: use-quiet-mode: "yes" diff --git a/doc/changes/changes.md b/doc/changes/changes.md index eea7a754..571eef84 100644 --- a/doc/changes/changes.md +++ b/doc/changes/changes.md @@ -1,5 +1,6 @@ # Changes ++ [4.2.1](changes_4.2.1.md) * [4.2.0](changes_4.2.0.md) * [4.1.0](changes_4.1.0.md) * [4.0.2](changes_4.0.2.md) diff --git a/doc/changes/changes_4.2.1.md b/doc/changes/changes_4.2.1.md new file mode 100644 index 00000000..acba3192 --- /dev/null +++ b/doc/changes/changes_4.2.1.md @@ -0,0 +1,11 @@ +# OpenFastTrace 4.2.1, released 2025-09-14 + +Code name: Peek before you guess + +## Summary + +This is a bugfix release. It addresses the problem that ReqM2 uses a generic `.xml` file suffix which led OpenFastTrace to treat any XML file as a ReqM2 file based solely on the extension. To avoid false detections, this version adds a peek function to the file type detection so that we no longer rely only on the file type/extension (#429). + +## Bugfixes + +* #429: Add content peek to file type detection so `.xml` no longer implies ReqM2; prevents misclassification of arbitrary XML files diff --git a/doc/spec/design.md b/doc/spec/design.md index e8c34b87..b30bc7f6 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -7,7 +7,7 @@ ## Acknowledgments -This documents structure is derived from the "[arc42][bib.arc42]" architectural template by Dr. Gernot Starke, Dr. Peter Hruschka. +This document's structure is derived from the "[arc42][bib.arc42]" architectural template by Dr. Gernot Starke, Dr. Peter Hruschka. If you build your own modifications based on this document, please keep the attrbiutions. @@ -174,9 +174,25 @@ Needs: impl, itest ## Import -Depending on the source format a variety of [importers](#importers) takes care of reading the input [specification items](#specification-item). Each importer emits events which an [import event listener](#import-event-listener) consumes. +Depending on the source format, a variety of [importers](#importers) takes care of reading the input [specification items](#specification-item). Each importer emits events which an [import event listener](#import-event-listener) consumes. -Common parts of the import like filtering out unnecessary items or attributes are handled by the listener. +The listener handles Common parts of the import like filtering out unnecessary items or attributes. + +A factory for importers decides which importer to use. Usually, by file extension. + +### ReqM2 File Detection +`dsn~import.reqm2-file-detection~1` + +The `SpecobjectImporterFactory` detects ReqM2 files either + +1. via the file extension `.oreqm` or +2. via the file extension `.xml` and the presence of the string ` dsn~import.reqm2-file-detection~1] + @Override + public boolean supportsFile(final InputFile file) + { + final String path = file.getPath(); + final String lower = path.toLowerCase(Locale.ROOT); + if (lower.endsWith(".oreqm")) + { + return true; + } + else if (lower.endsWith(".xml")) + { + return doesFileContainOreqmHeader(file, path); + } else { + return false; + } + } + + private static boolean doesFileContainOreqmHeader(final InputFile file, final String path) { + try (BufferedReader reader = file.createReader()) + { + final char[] buf = new char[PEEK_CHARS]; + final int read = reader.read(buf); + if (read <= 0) + { + return false; + } + else { + final String header = new String(buf, 0, read); + return header.contains(" "Unable to peek XML file '" + path + "' trying to determine if it contains ReqM2 format: " + exception.getMessage()); + return false; + } + } + @Override public Importer createImporter(final InputFile file, final ImportEventListener listener) { diff --git a/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporterFactory.java b/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporterFactory.java index 6c78041a..7bd8661c 100644 --- a/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporterFactory.java +++ b/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporterFactory.java @@ -1,10 +1,21 @@ package org.itsallcode.openfasttrace.importer.specobject; import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.when; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; +import org.itsallcode.openfasttrace.api.importer.input.InputFile; +import org.itsallcode.openfasttrace.api.importer.input.RealFileInput; import org.itsallcode.openfasttrace.testutil.importer.ImporterFactoryTestBase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; /** * Tests for {@link SpecobjectImporterFactory} @@ -19,16 +30,73 @@ protected SpecobjectImporterFactory createFactory() return new SpecobjectImporterFactory(); } + /** + * Only the {@code .oreqm} extension is always supported. That is why we + * can't simply list {@code .xml} here. Whether {@code .xml} is supported + * depends on a successful peek that confirms the file is in ReqM2 format. + * + * @return list of files that need to pass the test for supported files + */ @Override protected List getSupportedFilenames() { - return asList("file.xml", "file.XML", "FILE.xml", "FILE.XML", "file.md.xml"); + return asList("file.oreqm", "file.OREQM"); } @Override protected List getUnsupportedFilenames() { return asList("file.md", "file.xm", "file.ml", "file.1xml", "file.xml1", "file.xml.md", - "file_xml", "filexml"); + "file_xml", "filexml", "file_oreqm", "fileoreqm"); + } + + // [utest ->dsn~import.reqm2-file-detection~1] + @Test + void supportsXmlIfContentLooksLikeReqM2(@TempDir final Path tempDir) throws IOException + { + final Path tempFile = tempDir.resolve("reqm2-specdocument.xml"); + Files.writeString(tempFile, """ + + + + + """); + final boolean supported = createFactory().supportsFile(RealFileInput.forPath(tempFile)); + assertThat(supported, equalTo(true)); + } + + // [utest ->dsn~import.reqm2-file-detection~1] + @Test + void doesNotSupportXmlIfContentIsGeneric(@TempDir final Path tempDir) throws IOException + { + final Path tempFile = tempDir.resolve("generic.xml"); + Files.writeString(tempFile, """ + + + + " + """); + final boolean supported = createFactory().supportsFile(RealFileInput.forPath(tempFile)); + assertThat(supported, equalTo(false)); + } + + // [utest ->dsn~import.reqm2-file-detection~1] + @Test + void givenEmptyFileWhenCheckingReqM2HeaderThenDoesNotClaimSupport(@TempDir final Path tempDir) throws IOException + { + final Path tempFile = tempDir.resolve("empty.xml"); + Files.writeString(tempFile, ""); + final boolean supported = createFactory().supportsFile(RealFileInput.forPath(tempFile)); + assertThat(supported, equalTo(false)); + } + + // [utest ->dsn~import.reqm2-file-detection~1] + @Test + void givenFileWhenIOExceptionOccursThenDoesNotClaimSupport(@Mock InputFile mockInputFile) throws IOException + { + when(mockInputFile.getPath()).thenReturn("/irrelevant/path/to/file.xml"); + when(mockInputFile.createReader()).thenThrow(new IOException("This is an expected test exception")); + final boolean supported = createFactory().supportsFile(mockInputFile); + assertThat(supported, equalTo(false)); } } diff --git a/parent/pom.xml b/parent/pom.xml index 06218e3b..b969f493 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -10,7 +10,7 @@ Free requirement tracking suite https://github.com/itsallcode/openfasttrace - 4.2.0 + 4.2.1 17 5.12.2 3.5.3