Skip to content

Commit

Permalink
feat: migrate urls when normalizing to JSON (#380)
Browse files Browse the repository at this point in the history
migrate/fixup URLs when normalizing to JSON

followup of #35 

---------

Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck committed Dec 2, 2023
1 parent 02d05eb commit 3b5482a
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 40 deletions.
5 changes: 5 additions & 0 deletions HISTORY.md
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.

## unreleased

* Added
* Migration/fixup of URL(`iri-reference`) when normalizing to JSON (via [#380])

[#380]: https://github.com/CycloneDX/cyclonedx-php-library/pull/380

## 3.0.2 - 2023-11-27

* Misc
Expand Down
Expand Up @@ -24,7 +24,7 @@
namespace CycloneDX\Core\Serialization\DOM\Normalizers;

use CycloneDX\Core\_helpers\SimpleDOM;
use CycloneDX\Core\_helpers\XML;
use CycloneDX\Core\_helpers\XML as XmlHelper;
use CycloneDX\Core\Collections\ExternalReferenceRepository;
use CycloneDX\Core\Collections\HashDictionary;
use CycloneDX\Core\Collections\LicenseRepository;
Expand Down Expand Up @@ -142,7 +142,7 @@ private function normalizePurl(?PackageUrl $purl): ?DOMElement
: SimpleDOM::makeSafeTextElement(
$this->getNormalizerFactory()->getDocument(),
'purl',
XML::encodeAnyUriBE((string) $purl)
XmlHelper::encodeAnyUriBE((string) $purl)
);
}

Expand Down
Expand Up @@ -24,7 +24,7 @@
namespace CycloneDX\Core\Serialization\DOM\Normalizers;

use CycloneDX\Core\_helpers\SimpleDOM;
use CycloneDX\Core\_helpers\XML;
use CycloneDX\Core\_helpers\XML as XmlHelper;
use CycloneDX\Core\Collections\HashDictionary;
use CycloneDX\Core\Enums\ExternalReferenceType;
use CycloneDX\Core\Models\ExternalReference;
Expand All @@ -45,7 +45,7 @@ class ExternalReferenceNormalizer extends _BaseNormalizer
public function normalize(ExternalReference $externalReference): DOMElement
{
$refURI = $externalReference->getUrl();
$anyURI = XML::encodeAnyUriBE($refURI)
$anyURI = XmlHelper::encodeAnyUriBE($refURI)
?? throw new UnexpectedValueException("unable to make 'anyURI' from: $refURI");

$factory = $this->getNormalizerFactory();
Expand Down
4 changes: 2 additions & 2 deletions src/Core/Serialization/DOM/Normalizers/LicenseNormalizer.php
Expand Up @@ -24,7 +24,7 @@
namespace CycloneDX\Core\Serialization\DOM\Normalizers;

use CycloneDX\Core\_helpers\SimpleDOM;
use CycloneDX\Core\_helpers\XML;
use CycloneDX\Core\_helpers\XML as XmlHelper;
use CycloneDX\Core\Models\License\LicenseExpression;
use CycloneDX\Core\Models\License\NamedLicense;
use CycloneDX\Core\Models\License\SpdxLicense;
Expand Down Expand Up @@ -80,7 +80,7 @@ private function normalizeDisjunctive(SpdxLicense|NamedLicense $license): DOMEle
[
SimpleDOM::makeSafeTextElement($document, 'id', $id),
SimpleDOM::makeSafeTextElement($document, 'name', $name),
SimpleDOM::makeSafeTextElement($document, 'url', XML::encodeAnyUriBE($license->getUrl())),
SimpleDOM::makeSafeTextElement($document, 'url', XmlHelper::encodeAnyUriBE($license->getUrl())),
]
);
}
Expand Down
Expand Up @@ -23,6 +23,7 @@

namespace CycloneDX\Core\Serialization\JSON\Normalizers;

use CycloneDX\Core\_helpers\JSON as JsonHelper;
use CycloneDX\Core\_helpers\Predicate;
use CycloneDX\Core\Collections\ExternalReferenceRepository;
use CycloneDX\Core\Collections\HashDictionary;
Expand Down Expand Up @@ -104,11 +105,12 @@ private function normalizeHashes(HashDictionary $hashes): ?array
: $this->getNormalizerFactory()->makeForHashDictionary()->normalize($hashes);
}

/** @SuppressWarnings(PHPMD.StaticAccess) */
private function normalizePurl(?PackageUrl $purl): ?string
{
return null === $purl
? null
: (string) $purl;
: JsonHelper::encodeIriReferenceBE((string) $purl);
}

private function normalizeExternalReferences(ExternalReferenceRepository $extRefs): ?array
Expand Down
Expand Up @@ -23,13 +23,13 @@

namespace CycloneDX\Core\Serialization\JSON\Normalizers;

use CycloneDX\Core\_helpers\JSON as JsonHelper;
use CycloneDX\Core\_helpers\Predicate;
use CycloneDX\Core\Collections\HashDictionary;
use CycloneDX\Core\Enums\ExternalReferenceType;
use CycloneDX\Core\Models\ExternalReference;
use CycloneDX\Core\Serialization\JSON\_BaseNormalizer;
use DomainException;
use Opis\JsonSchema\Formats\IriFormats;
use UnexpectedValueException;

/**
Expand All @@ -45,10 +45,9 @@ class ExternalReferenceNormalizer extends _BaseNormalizer
*/
public function normalize(ExternalReference $externalReference): array
{
$url = $externalReference->getUrl();
if (false === IriFormats::iriReference($url)) {
throw new UnexpectedValueException("invalid to format 'IriReference': $url");
}
$refURI = $externalReference->getUrl();
$refIRI = JsonHelper::encodeIriReferenceBE($refURI)
?? throw new UnexpectedValueException("invalid to format 'IriReference': $refURI");

$spec = $this->getNormalizerFactory()->getSpec();
$type = $externalReference->getType();
Expand All @@ -63,7 +62,7 @@ public function normalize(ExternalReference $externalReference): array
return array_filter(
[
'type' => $type->value,
'url' => $url,
'url' => $refIRI,
'comment' => $externalReference->getComment(),
'hashes' => $this->normalizeHashes($externalReference->getHashes()),
],
Expand Down
8 changes: 2 additions & 6 deletions src/Core/Serialization/JSON/Normalizers/LicenseNormalizer.php
Expand Up @@ -23,12 +23,12 @@

namespace CycloneDX\Core\Serialization\JSON\Normalizers;

use CycloneDX\Core\_helpers\JSON as JsonHelper;
use CycloneDX\Core\_helpers\Predicate;
use CycloneDX\Core\Models\License\LicenseExpression;
use CycloneDX\Core\Models\License\NamedLicense;
use CycloneDX\Core\Models\License\SpdxLicense;
use CycloneDX\Core\Serialization\JSON\_BaseNormalizer;
use Opis\JsonSchema\Formats\IriFormats;

/**
* @author jkowalleck
Expand Down Expand Up @@ -63,15 +63,11 @@ private function normalizeDisjunctive(SpdxLicense|NamedLicense $license): array
[$id, $name] = [null, $id];
}

$url = $license->getUrl();

return ['license' => array_filter(
[
'id' => $id,
'name' => $name,
'url' => null !== $url && IriFormats::iriReference($url)
? $url
: null,
'url' => JsonHelper::encodeIriReferenceBE($license->getUrl()),
],
Predicate::isNotNull(...)
)];
Expand Down
64 changes: 64 additions & 0 deletions src/Core/_helpers/JSON.php
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

/*
* This file is part of CycloneDX PHP Library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/

namespace CycloneDX\Core\_helpers;

use Opis\JsonSchema\Formats\IriFormats;

/**
* Namespace of functions related to JSON.
*
* @internal as this class may be affected by breaking changes without notice
*
* @author jkowalleck
*/
abstract class JSON
{
use UriTrait;

/**
* Make a string valid to JSON::iri-reference spec - best-effort.
*
* Complete and failsafe implementation is pretty context-dependent.
* Best-effort solution: replacement & drop every URI that is not well-formed already.
*
* @see UriTrait::fixUriBE
* @see filterIriReference
*
* @return string|null string on success; null if encoding failed, or input was null
*/
public static function encodeIriReferenceBE(?string $uri): ?string
{
$uri = self::fixUriBE($uri);

return null === $uri || self::filterIriReference($uri)
? $uri
: null; // @codeCoverageIgnore
}

/** @SuppressWarnings(PHPMD.StaticAccess) */
public static function filterIriReference(string $uri): bool
{
return IriFormats::iriReference($uri);
}
}
57 changes: 57 additions & 0 deletions src/Core/_helpers/UriTrait.php
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

/*
* This file is part of CycloneDX PHP Library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/

namespace CycloneDX\Core\_helpers;

/**
* Namespace of functions related to URI.
*
* @internal as this trait may be affected by breaking changes without notice
*
* @author jkowalleck
*/
trait UriTrait
{
/**
* Make a string valid to
* - XML::anyURI spec.
* - JSON::iri-reference spec.
*
* BEST EFFORT IMPLEMENTATION
*
* @see http://www.w3.org/TR/xmlschema-2/#anyURI
* @see http://www.datypic.com/sc/xsd/t-xsd_anyURI.html
* @see https://datatracker.ietf.org/doc/html/rfc2396
* @see https://datatracker.ietf.org/doc/html/rfc3987
*/
private static function fixUriBE(?string $uri): ?string
{
return null === $uri
? $uri
: str_replace(
[' ', '[', ']', '<', '>', '{', '}'],
['%20', '%5B', '%5D', '%3C', '%3E', '%7B', '%7D'],
$uri
);
}
}
18 changes: 6 additions & 12 deletions src/Core/_helpers/XML.php
Expand Up @@ -34,30 +34,24 @@
*/
abstract class XML
{
use UriTrait;

/**
* Make a string valid to XML::anyURI spec - best-effort.
*
* Complete and failsafe implementation is pretty context-dependent.
* Best-effort solution: replacement & drop every URI that is not well-formed already.
*
* @see http://www.w3.org/TR/xmlschema-2/#anyURI
* @see http://www.datypic.com/sc/xsd/t-xsd_anyURI.html
* @see UriTrait::fixUriBE
* @see filterAnyUri
*
* @return string|null string on success; null if encoding failed, or input was null
*/
public static function encodeAnyUriBE(?string $uri): ?string
{
if (null === $uri) {
return null;
}

$uri = str_replace(
[' ', '[', ']', '<', '>', '{', '}'],
['%20', '%5B', '%5D', '%3C', '%3E', '%7B', '%7D'],
$uri
);
$uri = self::fixUriBE($uri);

return self::filterAnyUri($uri)
return null === $uri || self::filterAnyUri($uri)
? $uri
: null; // @codeCoverageIgnore
}
Expand Down
Expand Up @@ -30,7 +30,7 @@
use CycloneDX\Core\Serialization\DOM\NormalizerFactory;
use CycloneDX\Core\Serialization\DOM\Normalizers;
use CycloneDX\Core\Spec\_SpecProtocol;
use CycloneDX\Tests\_data\XmlAnyUriData;
use CycloneDX\Tests\_data\AnyUriData;
use CycloneDX\Tests\_traits\DomNodeAssertionTrait;
use DomainException;
use DOMDocument;
Expand Down Expand Up @@ -291,7 +291,7 @@ public function testNormalizeHashesOmitIfNotSupported(): void

// endregion normalize hashes

#[DataProviderExternal(XmlAnyUriData::class, 'dpEncodeAnyUri')]
#[DataProviderExternal(AnyUriData::class, 'dpEncodeAnyUri')]
public function testNormalizeUrlEncodeAnyUri(string $rawUrl, string $encodedUrl): void
{
$spec = $this->createMock(_SpecProtocol::class);
Expand All @@ -302,20 +302,18 @@ public function testNormalizeUrlEncodeAnyUri(string $rawUrl, string $encodedUrl)
$normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory);
$extRef = $this->createConfiguredMock(ExternalReference::class, [
'getUrl' => $rawUrl,
'getType' => ExternalReferenceType::BOM,
'getType' => ExternalReferenceType::Other,
'getComment' => null,
'getHashes' => $this->createStub(HashDictionary::class),
]);

$spec->expects(self::atLeastOnce())
->method('isSupportedExternalReferenceType')
->with(ExternalReferenceType::BOM)
$spec->method('isSupportedExternalReferenceType')
->willReturn(true);

$actual = $normalizer->normalize($extRef);

self::assertStringEqualsDomNode(
'<reference type="bom"><url>'.htmlspecialchars($encodedUrl).'</url></reference>',
'<reference type="other"><url>'.htmlspecialchars($encodedUrl).'</url></reference>',
$actual
);
}
Expand Down

0 comments on commit 3b5482a

Please sign in to comment.