diff options
author | 2020-09-17 14:46:47 +0200 | |
---|---|---|
committer | 2020-09-24 14:07:06 +0200 | |
commit | 6767596c5b15b75a3f1ae43e169aa88e0de56c3a (patch) | |
tree | 2b64ad7df6a076fb8315208ef3f32e355ace4536 /csarvalidation/src/test/java/org/onap/cvc | |
parent | e88eed4a3e6d7b9bf299d95fe2534d9f3bdbafb4 (diff) |
Fixing R130206 certificate searching mechanism
Issue-ID: VNFSDK-595
Signed-off-by: Bartosz Gardziejewski <bartosz.gardziejewski@nokia.com>
Change-Id: I8dacd924b16812378356b05291229f2097dfcbe1
Diffstat (limited to 'csarvalidation/src/test/java/org/onap/cvc')
-rw-r--r-- | csarvalidation/src/test/java/org/onap/cvc/csar/CsarValidatorTest.java | 48 | ||||
-rw-r--r-- | csarvalidation/src/test/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR130206IntegrationTest.java | 329 |
2 files changed, 324 insertions, 53 deletions
diff --git a/csarvalidation/src/test/java/org/onap/cvc/csar/CsarValidatorTest.java b/csarvalidation/src/test/java/org/onap/cvc/csar/CsarValidatorTest.java index 299aff2..491b20a 100644 --- a/csarvalidation/src/test/java/org/onap/cvc/csar/CsarValidatorTest.java +++ b/csarvalidation/src/test/java/org/onap/cvc/csar/CsarValidatorTest.java @@ -35,79 +35,83 @@ import static org.onap.cvc.csar.cc.sol004.IntegrationTestUtils.absoluteFilePath; public class CsarValidatorTest { - private static final String NO_CERTIFICATE_RULE = "r130206"; + private static final String CERTIFICATION_RULE = "r130206"; private static final String OPERATION_STATUS_FAILED = "FAILED"; + private static final String OPERATION_STATUS_PASS = "PASS"; @Test - public void shouldReportErrorAsWarningWhenErrorIsIgnored() throws URISyntaxException { + public void shouldReportThanVnfValidationFailed() throws URISyntaxException { // given OnapCliWrapper cli = new OnapCliWrapper(new String[]{ "--product", "onap-dublin", "csar-validate", "--format", "json", - "--pnf", - "--csar", absoluteFilePath("pnf/r130206/csar-option1-warning-2.csar")}); + "--csar", absoluteFilePath("VoLTE.csar")}); // when cli.handle(); // then final OnapCommandResult onapCommandResult = cli.getCommandResult(); - assertTrue(onapCommandResult.getOutput().toString().contains( - "\"warnings\":[{\"vnfreqNo\":\"R130206\",\"code\":\"0x1006\",\"message\":\"Warning. Consider adding package " - + "integrity and authenticity assurance according to ETSI NFV-SOL 004 Security Option 1\",\"file\":\"\",\"lineNumber\":-1}]}")); + verifyThatOperation(onapCommandResult, OPERATION_STATUS_FAILED); + verifyThatXRulesFails(onapCommandResult, 7); + verifyThatOperationFinishedWithoutAnyError(cli); } + @Test - public void shouldReportThanVnfValidationFailed() throws URISyntaxException { + public void shouldReportOnlyWarningWhenCsarDoNotHaveCertificateAndHashesInManifest() throws URISyntaxException { // given OnapCliWrapper cli = new OnapCliWrapper(new String[]{ "--product", "onap-dublin", "csar-validate", "--format", "json", - "--csar", absoluteFilePath("VoLTE.csar")}); - + "--pnf", + "--csar", absoluteFilePath("pnf/validFile.csar")}); // when cli.handle(); // then final OnapCommandResult onapCommandResult = cli.getCommandResult(); - verifyThatOperation(onapCommandResult, OPERATION_STATUS_FAILED); - verifyThatXRulesFails(onapCommandResult, 7); + verifyThatOperation(onapCommandResult, OPERATION_STATUS_PASS); + assertTrue(onapCommandResult.getOutput().toString().contains( + "\"warnings\":[{" + + "\"vnfreqNo\":\"R130206\"," + + "\"code\":\"0x1006\"," + + "\"message\":\"Warning. Consider adding package integrity and authenticity assurance according to ETSI NFV-SOL 004 Security Option 1\"," + + "\"file\":\"\"," + + "\"lineNumber\":-1}]")); verifyThatOperationFinishedWithoutAnyError(cli); } - @Test - public void shouldReportThatPnfValidationFailedWhenCsarDoNotHaveCertificate_allOtherRulesShouldPass() throws URISyntaxException { + public void shouldNotReportThatPnfValidationFailedWhenZipDoNotHaveCertificatesAndHashesInManifest() throws URISyntaxException { // given OnapCliWrapper cli = new OnapCliWrapper(new String[]{ "--product", "onap-dublin", "csar-validate", "--format", "json", "--pnf", - "--csar", absoluteFilePath("pnf/r972082/validFile.csar")}); + "--csar", absoluteFilePath("pnf/signed-package-valid-signature.zip")}); + // when cli.handle(); // then final OnapCommandResult onapCommandResult = cli.getCommandResult(); - verifyThatOperation(onapCommandResult, OPERATION_STATUS_FAILED); - verifyThatXRulesFails(onapCommandResult, 1); - verifyThatRuleFails(onapCommandResult, NO_CERTIFICATE_RULE); + verifyThatOperation(onapCommandResult, OPERATION_STATUS_PASS); verifyThatOperationFinishedWithoutAnyError(cli); } @Test - public void shouldReportThatPnfValidationFailedWhenZipDoNotHaveCertificate_allOtherRulesShouldPass() throws URISyntaxException { + public void shouldReportThatPnfValidationFailedWhenCsarContainsCertificateInCmsAndInToscaAndInRootAndHashIsIncorrect_allOtherRulesShouldPass() throws URISyntaxException { // given OnapCliWrapper cli = new OnapCliWrapper(new String[]{ "--product", "onap-dublin", "csar-validate", "--format", "json", "--pnf", - "--csar", absoluteFilePath("pnf/signed-package-valid-signature.zip")}); - + "--csar", absoluteFilePath("pnf/r130206/cert-in-cms-and-root-and-tosca-incorrect-hash.csar")}); // when cli.handle(); @@ -115,7 +119,7 @@ public class CsarValidatorTest { final OnapCommandResult onapCommandResult = cli.getCommandResult(); verifyThatOperation(onapCommandResult, OPERATION_STATUS_FAILED); verifyThatXRulesFails(onapCommandResult, 1); - verifyThatRuleFails(onapCommandResult, NO_CERTIFICATE_RULE); + verifyThatRuleFails(onapCommandResult, CERTIFICATION_RULE); verifyThatOperationFinishedWithoutAnyError(cli); } diff --git a/csarvalidation/src/test/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR130206IntegrationTest.java b/csarvalidation/src/test/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR130206IntegrationTest.java index 3eed6c6..cdaef79 100644 --- a/csarvalidation/src/test/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR130206IntegrationTest.java +++ b/csarvalidation/src/test/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR130206IntegrationTest.java @@ -49,10 +49,10 @@ public class VTPValidateCSARR130206IntegrationTest { "To verify signed package please please follow instructions from test/resources/README.txt file and comment @Ignore tag. " + "Use instructions for option 1. Test was created for manual verification." ) - public void manual_shouldValidateProperCsar() throws Exception { + public void manual_shouldValidateProperCsarWithCms() throws Exception { // given - configureTestCase(testCase, "pnf/r130206/csar-option1-valid.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-valid.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); // when testCase.execute(); @@ -64,13 +64,31 @@ public class VTPValidateCSARR130206IntegrationTest { @Test @Ignore("It is impossible to write test which will always pass, because certificate used to sign the file has time validity." + - "To verify signed package please please follow instructions from test/resources/README.txt file and comment @Ignore tag. " + - "Use instructions for option 1. Test was created for manual verification." + "To verify signed package please please follow instructions from test/resources/README.txt file and comment @Ignore tag. " + + "Use instructions for option 1. Test was created for manual verification." + ) + public void manual_shouldValidateCsarWithCertificateInToscaEtsiWithValidSignature() throws Exception { + + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-tosca-valid.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isZero(); + } + + @Test + @Ignore("It is impossible to write test which will always pass, because certificate used to sign the file has time validity." + + "To verify signed package please please follow instructions from test/resources/README.txt file and comment @Ignore tag. " + + "Use instructions for option 1. Test was created for manual verification." ) - public void manual_shouldValidateCsarWithCertificateInEtsiAndMissingInCMS() throws Exception { + public void manual_shouldValidateCsarWithCertificateInRootWithValidSignature() throws Exception { // given - configureTestCase(testCase, "pnf/r130206/csar-with-etsi-cert-without-cert-in-cms.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + configureTestCase(testCase, "pnf/r130206/csar-cert-in-root-valid.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); // when testCase.execute(); @@ -81,9 +99,10 @@ public class VTPValidateCSARR130206IntegrationTest { } @Test - public void shouldReportWarningForMissingCMSAndHashCodes() throws Exception{ + public void shouldReportWarningForMissingCertInCmsToscaMetaAndRootCatalogAndMissingHashCodesInManifest() + throws Exception{ // given - configureTestCase(testCase, "pnf/r130206/csar-option1-warning.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + configureTestCase(testCase, "pnf/r130206/csar-not-secure-warning.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); // when testCase.execute(); @@ -92,16 +111,102 @@ public class VTPValidateCSARR130206IntegrationTest { List<CSARArchive.CSARError> errors = testCase.getErrors(); assertThat(errors.size()).isEqualTo(1); assertThat(convertToMessagesList(errors)).contains( - "Warning. Consider adding package integrity and authenticity assurance according to ETSI NFV-SOL 004 Security Option 1" + "Warning. Consider adding package integrity and authenticity assurance according to ETSI NFV-SOL 004 Security Option 1" ); } + @Test + public void shouldReturnNoErrorWhenCertIsOnlyInCmsAndAlgorithmAndHashesAreCorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(1); + assertThat(convertToMessagesList(errors)).contains( + "File has invalid signature!" + ); + } @Test - public void shouldReportThatOnlySignatureIsInvalid() throws Exception { + public void shouldReturnNoErrorWhenCertIsOnlyInToscaAndAlgorithmAndHashesAreCorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-tosca.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(1); + assertThat(convertToMessagesList(errors)).contains( + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnNoErrorWhenCertIsOnlyInRootDirectoryAndAlgorithmAndHashesAreCorrect() + throws Exception{ // given - configureTestCase(testCase, "pnf/r130206/csar-option1-validSection.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + configureTestCase(testCase, "pnf/r130206/csar-cert-in-root.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(1); + assertThat(convertToMessagesList(errors)).contains( + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenCertIsOnlyInCmsHoweverHashesAreIncorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-incorrect-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(2); + assertThat(convertToMessagesList(errors)).contains( + "Source 'Artifacts/Other/my_script.csh' has wrong hash!", + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenCertIsOnlyInToscaHoweverHashesAreIncorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-tosca-incorrect-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(2); + assertThat(convertToMessagesList(errors)).contains( + "Source 'Artifacts/Deployment/Measurements/PM_Dictionary.yml' has wrong hash!", + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenCertIsOnlyInRootDirectoryHoweverHashesAreIncorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-root-incorrect-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); // when testCase.execute(); @@ -110,70 +215,232 @@ public class VTPValidateCSARR130206IntegrationTest { List<CSARArchive.CSARError> errors = testCase.getErrors(); assertThat(errors.size()).isEqualTo(2); assertThat(convertToMessagesList(errors)).contains( - "File has invalid CMS signature!", - "Mismatch between contents of non-mano-artifact-sets and source files of the package" + "Source 'Artifacts/Deployment/Events/RadioNode_Pnf_v1.yaml' has wrong hash!", + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenToscaEtsiEntryCertificatePointToNotExistingFile() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-with-tosca-cert-pointing-non-existing-cert.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(2); + assertThat(convertToMessagesList(errors)).contains( + "Unable to find cert file defined by ETSI-Entry-Certificate!", + "Invalid value. Entry [Entry-Certificate]. Artifacts/sample-pnf.cert does not exist" + ); + } + + @Test + public void shouldReturnErrorWhenCertificateIsLocatedInCmsAndInTosca() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-and-tosca.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(3); + assertThat(convertToMessagesList(errors)).contains( + "ETSI-Entry-Certificate entry in Tosca.meta is defined despite the certificate is included in the signature container", + "ETSI-Entry-Certificate certificate present despite the certificate is included in the signature container", + "File has invalid signature!" ); } @Test - public void shouldReportErrorsForInvalidCsar() throws Exception { + public void shouldReturnErrorWhenCertificateIsLocatedInCmsAndInToscaAndHashIsIncorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-and-tosca-incorrect-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(4); + assertThat(convertToMessagesList(errors)).contains( + "ETSI-Entry-Certificate entry in Tosca.meta is defined despite the certificate is included in the signature container", + "ETSI-Entry-Certificate certificate present despite the certificate is included in the signature container", + "Source 'Artifacts/Informational/user_guide.txt' has wrong hash!", + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenCertificateIsLocatedInCmsAndInToscaAndInRootDirectory() + throws Exception{ // given - configureTestCase(testCase, "pnf/r130206/csar-option1-invalid.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-and-root-and-tosca.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); // when testCase.execute(); // then List<CSARArchive.CSARError> errors = testCase.getErrors(); - assertThat(errors.size()).isEqualTo(6); + assertThat(errors.size()).isEqualTo(4); assertThat(convertToMessagesList(errors)).contains( - "Unable to find CMS section in manifest!", - "Source 'Definitions/MainServiceTemplate.yaml' has wrong hash!", - "Source 'Artifacts/Other/my_script.csh' has hash, but unable to find algorithm tag!", - "Unable to calculate digest - file missing: Artifacts/NonExisting2.txt", - "Mismatch between contents of non-mano-artifact-sets and source files of the package", - "File has invalid CMS signature!" + "ETSI-Entry-Certificate entry in Tosca.meta is defined despite the certificate is included in the signature container", + "ETSI-Entry-Certificate certificate present despite the certificate is included in the signature container", + "Certificate present in root catalog despite the certificate is included in the signature container", + "File has invalid signature!" ); } @Test - public void shouldReportThanInVnfPackageCertFileWasNotDefined() throws Exception { + public void shouldReturnErrorWhenCertificateIsLocatedInCmsAndInToscaAndInRootDirectoryAndHashIsIncorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-and-root-and-tosca-incorrect-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(5); + assertThat(convertToMessagesList(errors)).contains( + "ETSI-Entry-Certificate entry in Tosca.meta is defined despite the certificate is included in the signature container", + "ETSI-Entry-Certificate certificate present despite the certificate is included in the signature container", + "Certificate present in root catalog despite the certificate is included in the signature container", + "Source 'Artifacts/Informational/user_guide.txt' has wrong hash!", + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenCertificateIsLocatedInCmsAndInRootDirectory() + throws Exception{ // given - configureTestCase(testCase, "sample2.csar", "vtp-validate-csar-r130206.yaml", false); + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-and-root.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); // when testCase.execute(); // then List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(2); assertThat(convertToMessagesList(errors)).contains( - "Unable to find cert file defined by Entry-Certificate!", - "Warning. Consider adding package integrity and authenticity assurance according to ETSI NFV-SOL 004 Security Option 1", - "Missing. Entry [tosca_definitions_version]" + "Certificate present in root catalog despite the certificate is included in the signature container", + "File has invalid signature!" ); } + @Test + public void shouldReturnErrorWhenCertificateIsLocatedInCmsAndInRootDirectoryAndHashIsIncorrect() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-cms-and-root-incorrect-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(3); + assertThat(convertToMessagesList(errors)).contains( + "Certificate present in root catalog despite the certificate is included in the signature container", + "Source 'Artifacts/Informational/user_guide.txt' has wrong hash!", + "File has invalid signature!" + ); + } @Test - public void shouldReportThanInVnfPackageETSIFileIsMissing() throws Exception { + public void shouldReturnErrorWhenCertificateIsLocatedInToscaAndInRootDirectory() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-root-and-tosca.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(2); + assertThat(convertToMessagesList(errors)).contains( + "Certificate present in root catalog despite the certificate is included in ETSI-Entry-Certificate", + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenCertificateIsLocatedInToscaAndInRootDirectoryAdnHashIsIncorrect() + throws Exception{ // given - configureTestCase(testCase, "pnf/r130206/csar-with-no-certificate.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + configureTestCase(testCase, "pnf/r130206/csar-cert-in-root-and-tosca-incorrect-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); // when testCase.execute(); // then List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(3); + assertThat(convertToMessagesList(errors)).contains( + "Certificate present in root catalog despite the certificate is included in ETSI-Entry-Certificate", + "Source 'Artifacts/Deployment/Yang_module/yang-module1.yang' has wrong hash!", + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnNoErrorWhenCertificateIsLocatedInToscaAndInRootDirectoryHoweverEtsiEntryIsPointingCertificateInRoot() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-root-pointed-by-tosca.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(1); assertThat(convertToMessagesList(errors)).contains( - "Unable to find cert file defined by ETSI-Entry-Certificate!", - "Warning. Consider adding package integrity and authenticity assurance according to ETSI NFV-SOL 004 Security Option 1" + "File has invalid signature!" + ); + } + + @Test + public void shouldReturnErrorWhenCertificateIsLocatedInToscaHoweverManifestDoesNotContainsCms() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-cert-in-tosca-no-cms.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(1); + assertThat(convertToMessagesList(errors)).contains( + "Unable to find cms signature!" ); } + @Test + public void shouldReturnErrorWhenCsarDoesNotContainsCmsAndCertsHoweverManifestContainsHash() + throws Exception{ + // given + configureTestCase(testCase, "pnf/r130206/csar-no-cms-no-cert-with-hash.csar", "vtp-validate-csar-r130206.yaml", IS_PNF); + + // when + testCase.execute(); + // then + List<CSARArchive.CSARError> errors = testCase.getErrors(); + assertThat(errors.size()).isEqualTo(1); + assertThat(convertToMessagesList(errors)).contains( + "Unable to find cms signature!" + ); + } } |