From 7d2c49af641840b208c012a582f9caa976972b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Sat, 7 Oct 2017 13:14:33 +0200 Subject: [PATCH] initial commit --- .gitignore | 26 + LICENSE-bouncycastle.txt | 18 + LICENSE.txt | 203 ++ build.gradle | 108 + config/checkstyle/checkstyle.xml | 323 +++ config/maven/repo-settings.xml | 24 + gradle-plugin-rpm/NOTICE.txt | 5 + gradle-plugin-rpm/build.gradle | 50 + .../config/checkstyle/checkstyle.xml | 323 +++ .../plugin/rpm/CopySpecEnhancement.groovy | 77 + .../xbib/gradle/plugin/rpm/Dependency.groovy | 55 + .../xbib/gradle/plugin/rpm/Directory.groovy | 14 + .../rpm/FromConfigurationFactory.groovy | 21 + .../org/xbib/gradle/plugin/rpm/Link.groovy | 13 + .../rpm/ProjectPackagingExtension.groovy | 257 +++ .../org/xbib/gradle/plugin/rpm/Rpm.groovy | 390 ++++ .../gradle/plugin/rpm/RpmCopyAction.groovy | 317 +++ .../plugin/rpm/RpmFileVisitorStrategy.groovy | 63 + .../xbib/gradle/plugin/rpm/RpmPlugin.groovy | 65 + .../rpm/SystemPackagingExtension.groovy | 490 +++++ .../RpmPackageNameAttributeValidator.groovy | 18 + .../RpmTaskPropertiesValidator.groovy | 16 + .../SystemPackagingAttributeValidator.groovy | 8 + ...temPackagingTaskPropertiesValidator.groovy | 8 + .../org.xbib.gradle.plugin.rpm.properties | 1 + .../plugin/rpm/AbstractProjectSpec.groovy | 52 + .../plugin/rpm/BaseIntegrationSpec.groovy | 120 ++ .../BuildLauncherBackedGradleHandle.groovy | 96 + .../ClasspathAddingInitScriptBuilder.groovy | 58 + ...asspathInjectingGradleHandleFactory.groovy | 35 + .../xbib/gradle/plugin/rpm/Coordinate.groovy | 18 + .../plugin/rpm/CopySpecEnhancementTest.groovy | 107 + .../plugin/rpm/DefaultExecutionResult.groovy | 90 + .../plugin/rpm/DefaultGradleRunner.groovy | 25 + .../gradle/plugin/rpm/DependencyGraph.groovy | 37 + .../plugin/rpm/DependencyGraphNode.groovy | 16 + .../gradle/plugin/rpm/ExecutedTask.groovy | 10 + .../gradle/plugin/rpm/ExecutionResult.groovy | 20 + .../rpm/GradleDependencyGenerator.groovy | 151 ++ .../gradle/plugin/rpm/GradleHandle.groovy | 10 + .../rpm/GradleHandleBuildListener.groovy | 8 + .../plugin/rpm/GradleHandleFactory.groovy | 8 + .../gradle/plugin/rpm/GradleRunner.groovy | 78 + .../plugin/rpm/GradleRunnerFactory.groovy | 26 + .../gradle/plugin/rpm/IntegrationSpec.groovy | 185 ++ .../plugin/rpm/MinimalExecutedTask.groovy | 20 + .../plugin/rpm/MultiProjectHelper.groovy | 43 + .../gradle/plugin/rpm/MultiProjectInfo.groovy | 14 + .../plugin/rpm/PreExecutionAction.groovy | 6 + .../xbib/gradle/plugin/rpm/ProjectSpec.groovy | 8 + .../plugin/rpm/RpmCopySpecVisitorTest.groovy | 50 + .../rpm/RpmPluginIntegrationTest.groovy | 26 + .../gradle/plugin/rpm/RpmPluginTest.groovy | 1316 +++++++++++++ .../xbib/gradle/plugin/rpm/RpmReader.groovy | 131 ++ .../rpm/SystemPackagingExtensionTest.groovy | 77 + .../rpm/ToolingApiGradleHandleFactory.groovy | 113 ++ .../plugin/rpm/ToolingExecutionResult.groovy | 12 + ...pmPackageNameAttributeValidatorTest.groovy | 35 + ...kPropertiesValidatorIntegrationTest.groovy | 41 + .../src/test/resources/pgp/test-secring.gpg | Bin 0 -> 2551 bytes gradle.properties | 15 + gradle/ext.gradle | 8 + gradle/publish.gradle | 67 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++ gradlew.bat | 84 + maven-plugin-rpm/NOTICE.txt | 13 + maven-plugin-rpm/build.gradle | 72 + .../config/checkstyle/checkstyle.xml | 323 +++ .../xbib/maven/plugin/rpm/RpmBaseObject.java | 95 + .../org/xbib/maven/plugin/rpm/RpmLink.java | 68 + .../org/xbib/maven/plugin/rpm/RpmPackage.java | 1336 +++++++++++++ .../plugin/rpm/RpmPackageAssociation.java | 124 ++ .../xbib/maven/plugin/rpm/RpmPackageRule.java | 259 +++ .../plugin/rpm/RpmScriptTemplateRenderer.java | 53 + .../org/xbib/maven/plugin/rpm/RpmTrigger.java | 217 ++ .../plugin/rpm/mojo/AbstractRpmMojo.java | 407 ++++ .../plugin/rpm/mojo/ListFilesRpmMojo.java | 43 + .../maven/plugin/rpm/mojo/PackageRpmMojo.java | 54 + .../xbib/maven/plugin/rpm/mojo/RpmMojo.java | 122 ++ .../maven/plugin/rpm/mojo/package-info.java | 4 + .../xbib/maven/plugin/rpm/package-info.java | 4 + .../resources/META-INF/plexus/components.xml | 19 + .../xbib/maven/plugin/rpm/MockBuilder.java | 21 + .../org/xbib/maven/plugin/rpm/MockMojo.java | 49 + .../maven/plugin/rpm/RpmBaseObjectTest.java | 46 + .../xbib/maven/plugin/rpm/RpmLinkTest.java | 42 + .../plugin/rpm/RpmPackageAssociationTest.java | 81 + .../rpm/RpmPackageRuleDirectiveTest.java | 136 ++ .../maven/plugin/rpm/RpmPackageRuleTest.java | 153 ++ .../xbib/maven/plugin/rpm/RpmPackageTest.java | 410 ++++ .../rpm/RpmScriptTemplateRendererTest.java | 62 + .../xbib/maven/plugin/rpm/RpmTriggerTest.java | 50 + .../plugin/rpm/mojo/AbstractRpmMojoTest.java | 172 ++ .../plugin/rpm/mojo/ListFilesRpmMojoTest.java | 63 + .../plugin/rpm/mojo/PackageRpmMojoTest.java | 112 ++ .../maven/plugin/rpm/mojo/package-info.java | 4 + .../xbib/maven/plugin/rpm/package-info.java | 4 + .../resources/mojo/AbstractRpmMojo-template | 2 + .../mojo/AbstractRpmMojo-template-expected | 2 + .../src/test/resources/rpm/RpmPackage.sh | 3 + .../rpm/RpmScriptTemplateRenderer-template | 412 ++++ ...pmScriptTemplateRenderer-template-expected | 412 ++++ rpm-ant/NOTICE.txt | 13 + rpm-ant/build.gradle | 4 + rpm-ant/config/checkstyle/checkstyle.xml | 323 +++ .../main/java/org/xbib/rpm/ant/BuiltIn.java | 26 + .../main/java/org/xbib/rpm/ant/Conflicts.java | 66 + .../main/java/org/xbib/rpm/ant/Depends.java | 66 + .../main/java/org/xbib/rpm/ant/Obsoletes.java | 66 + .../main/java/org/xbib/rpm/ant/Provides.java | 28 + .../java/org/xbib/rpm/ant/RpmFileSet.java | 147 ++ .../main/java/org/xbib/rpm/ant/RpmTask.java | 476 +++++ .../java/org/xbib/rpm/ant/package-info.java | 4 + .../main/resources/org/xbib/rpm/antlib.xml | 5 + .../java/org/xbib/rpm/ant/RpmTaskTest.java | 474 +++++ .../java/org/xbib/rpm/ant/package-info.java | 4 + .../org/xbib/rpm/changelog/changelog | 24 + .../src/test/resources/pgp/test-secring.gpg | Bin 0 -> 2551 bytes rpm-ant/src/test/resources/postin.sh | 3 + rpm-ant/src/test/resources/postun.sh | 3 + rpm-ant/src/test/resources/prein.sh | 3 + rpm-ant/src/test/resources/preun.sh | 3 + rpm-core/NOTICE.txt | 13 + rpm-core/build.gradle | 11 + rpm-core/config/checkstyle/checkstyle.xml | 323 +++ .../main/java/org/xbib/rpm/RpmBuilder.java | 1754 +++++++++++++++++ .../src/main/java/org/xbib/rpm/RpmReader.java | 134 ++ .../xbib/rpm/changelog/ChangelogEntry.java | 63 + .../xbib/rpm/changelog/ChangelogHandler.java | 69 + .../xbib/rpm/changelog/ChangelogParser.java | 148 ++ .../org/xbib/rpm/changelog/ParsingState.java | 7 + .../org/xbib/rpm/changelog/package-info.java | 4 + .../exception/ChangelogParseException.java | 21 + .../DatesOutOfSequenceException.java | 15 + .../IncompleteChangelogEntryException.java | 14 + .../InvalidChangelogDateException.java | 18 + .../exception/InvalidDirectiveException.java | 18 + .../rpm/exception/InvalidPathException.java | 18 + .../exception/NoInitialAsteriskException.java | 13 + .../PathOutsideBuildPathException.java | 18 + .../org/xbib/rpm/exception/RpmException.java | 26 + .../SigningKeyNotFoundException.java | 18 + .../UnknownArchitectureException.java | 28 + .../UnknownOperatingSystemException.java | 28 + .../org/xbib/rpm/exception/package-info.java | 4 + .../main/java/org/xbib/rpm/format/Flags.java | 39 + .../main/java/org/xbib/rpm/format/Format.java | 49 + .../org/xbib/rpm/format/package-info.java | 4 + .../org/xbib/rpm/header/AbstractHeader.java | 380 ++++ .../java/org/xbib/rpm/header/EntryType.java | 29 + .../main/java/org/xbib/rpm/header/Header.java | 18 + .../java/org/xbib/rpm/header/HeaderTag.java | 141 ++ .../main/java/org/xbib/rpm/header/Tags.java | 31 + .../rpm/header/entry/AbstractSpecEntry.java | 107 + .../xbib/rpm/header/entry/BinSpecEntry.java | 33 + .../rpm/header/entry/I18NStringSpecEntry.java | 14 + .../xbib/rpm/header/entry/Int16SpecEntry.java | 52 + .../xbib/rpm/header/entry/Int32SpecEntry.java | 58 + .../xbib/rpm/header/entry/Int64SpecEntry.java | 52 + .../xbib/rpm/header/entry/Int8SpecEntry.java | 47 + .../org/xbib/rpm/header/entry/SpecEntry.java | 41 + .../header/entry/StringArraySpecEntry.java | 14 + .../rpm/header/entry/StringSpecEntry.java | 63 + .../xbib/rpm/header/entry/package-info.java | 4 + .../org/xbib/rpm/header/package-info.java | 4 + .../java/org/xbib/rpm/io/ChannelWrapper.java | 166 ++ .../xbib/rpm/io/ReadableChannelWrapper.java | 55 + .../xbib/rpm/io/WritableChannelWrapper.java | 54 + .../java/org/xbib/rpm/io/package-info.java | 4 + .../java/org/xbib/rpm/lead/Architecture.java | 29 + .../src/main/java/org/xbib/rpm/lead/Lead.java | 143 ++ .../src/main/java/org/xbib/rpm/lead/Os.java | 31 + .../java/org/xbib/rpm/lead/PackageType.java | 10 + .../java/org/xbib/rpm/lead/package-info.java | 4 + .../main/java/org/xbib/rpm/package-info.java | 4 + .../org/xbib/rpm/payload/CompressionType.java | 9 + .../java/org/xbib/rpm/payload/Contents.java | 957 +++++++++ .../java/org/xbib/rpm/payload/CpioHeader.java | 373 ++++ .../java/org/xbib/rpm/payload/Directive.java | 104 + .../java/org/xbib/rpm/payload/EmptyDir.java | 61 + .../main/java/org/xbib/rpm/payload/Ghost.java | 80 + .../main/java/org/xbib/rpm/payload/Link.java | 37 + .../org/xbib/rpm/payload/package-info.java | 4 + .../java/org/xbib/rpm/security/HashAlgo.java | 39 + .../java/org/xbib/rpm/security/KeyDumper.java | 51 + .../org/xbib/rpm/security/KeyGenerator.java | 130 ++ .../xbib/rpm/security/SignatureGenerator.java | 266 +++ .../org/xbib/rpm/security/package-info.java | 4 + .../xbib/rpm/signature/SignatureHeader.java | 21 + .../org/xbib/rpm/signature/SignatureTag.java | 63 + .../org/xbib/rpm/signature/package-info.java | 4 + .../org/xbib/rpm/trigger/AbstractTrigger.java | 44 + .../java/org/xbib/rpm/trigger/Depends.java | 42 + .../java/org/xbib/rpm/trigger/Trigger.java | 52 + .../java/org/xbib/rpm/trigger/TriggerIn.java | 14 + .../org/xbib/rpm/trigger/TriggerPostUn.java | 14 + .../org/xbib/rpm/trigger/TriggerPreIn.java | 14 + .../java/org/xbib/rpm/trigger/TriggerUn.java | 14 + .../org/xbib/rpm/trigger/package-info.java | 4 + .../java/org/xbib/rpm/RpmBuilderTest.java | 255 +++ .../test/java/org/xbib/rpm/RpmReaderTest.java | 53 + .../test/java/org/xbib/rpm/SimpleRpmTest.java | 33 + .../rpm/changelog/ChangelogHandlerTest.java | 65 + .../rpm/changelog/ChangelogParserTest.java | 214 ++ .../org/xbib/rpm/changelog/ChangelogTest.java | 66 + .../org/xbib/rpm/changelog/package-info.java | 4 + .../InvalidDirectiveExceptionTest.java | 17 + .../exception/InvalidPathExceptionTest.java | 19 + .../PathOutsideBuildPathExceptionTest.java | 18 + .../SigningKeyNotFoundExceptionTest.java | 17 + .../UnknownArchitectureExceptionTest.java | 25 + .../UnknownOperatingSystemExceptionTest.java | 26 + .../org/xbib/rpm/exception/package-info.java | 4 + .../java/org/xbib/rpm/header/HeaderTest.java | 292 +++ .../org/xbib/rpm/header/package-info.java | 4 + .../test/java/org/xbib/rpm/package-info.java | 4 + .../org/xbib/rpm/payload/ContentsTest.java | 64 + .../org/xbib/rpm/payload/package-info.java | 4 + .../xbib/rpm/security/PrintPublicKeyTest.java | 24 + .../rpm/security/SignatureGeneratorTest.java | 87 + .../rpm/security/SignatureReaderTest.java | 35 + .../org/xbib/rpm/security/package-info.java | 4 + .../org/xbib/rpm/changelog/bad.changelog | 2 + .../org/xbib/rpm/changelog/changelog | 24 + .../rpm/changelog/changelog.with.comments | 6 + .../src/test/resources/pgp/test-pubring.gpg | Bin 0 -> 1157 bytes .../src/test/resources/pgp/test-secring.gpg | Bin 0 -> 2551 bytes rpm-core/src/test/resources/postin.sh | 3 + rpm-core/src/test/resources/postun.sh | 3 + rpm-core/src/test/resources/prein.sh | 3 + rpm-core/src/test/resources/preun.sh | 3 + .../src/test/resources/rpm-1-1.0-1.noarch.rpm | Bin 0 -> 34397 bytes .../test/resources/rpm-3-1.0-1.somearch.rpm | Bin 0 -> 128435 bytes .../signature-my-ring-test-1.0-1.noarch.rpm | Bin 0 -> 1752 bytes .../resources/signing-test-1.0-1.noarch.rpm | Bin 0 -> 1736 bytes rpm-core/src/test/resources/test.txt | 0 settings.gradle | 6 + 239 files changed, 21704 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE-bouncycastle.txt create mode 100644 LICENSE.txt create mode 100644 build.gradle create mode 100644 config/checkstyle/checkstyle.xml create mode 100644 config/maven/repo-settings.xml create mode 100644 gradle-plugin-rpm/NOTICE.txt create mode 100644 gradle-plugin-rpm/build.gradle create mode 100644 gradle-plugin-rpm/config/checkstyle/checkstyle.xml create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancement.groovy create mode 100755 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Dependency.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Directory.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/FromConfigurationFactory.groovy create mode 100755 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Link.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/ProjectPackagingExtension.groovy create mode 100755 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Rpm.groovy create mode 100755 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmCopyAction.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmFileVisitorStrategy.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmPlugin.groovy create mode 100755 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtension.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidator.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidator.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingAttributeValidator.groovy create mode 100644 gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingTaskPropertiesValidator.groovy create mode 100644 gradle-plugin-rpm/src/main/resources/META-INF/gradle-plugins/org.xbib.gradle.plugin.rpm.properties create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/AbstractProjectSpec.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BaseIntegrationSpec.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BuildLauncherBackedGradleHandle.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathAddingInitScriptBuilder.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathInjectingGradleHandleFactory.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/Coordinate.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancementTest.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultExecutionResult.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultGradleRunner.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraph.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraphNode.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutedTask.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutionResult.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleDependencyGenerator.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandle.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleBuildListener.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleFactory.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunner.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunnerFactory.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/IntegrationSpec.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MinimalExecutedTask.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectHelper.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectInfo.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/PreExecutionAction.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ProjectSpec.groovy create mode 100755 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmCopySpecVisitorTest.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginIntegrationTest.groovy create mode 100755 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginTest.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmReader.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtensionTest.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingApiGradleHandleFactory.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingExecutionResult.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidatorTest.groovy create mode 100644 gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidatorIntegrationTest.groovy create mode 100644 gradle-plugin-rpm/src/test/resources/pgp/test-secring.gpg create mode 100644 gradle.properties create mode 100644 gradle/ext.gradle create mode 100644 gradle/publish.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 maven-plugin-rpm/NOTICE.txt create mode 100644 maven-plugin-rpm/build.gradle create mode 100644 maven-plugin-rpm/config/checkstyle/checkstyle.xml create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmBaseObject.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmLink.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackage.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageAssociation.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageRule.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRenderer.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmTrigger.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojo.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojo.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojo.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/RpmMojo.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/package-info.java create mode 100644 maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/package-info.java create mode 100644 maven-plugin-rpm/src/main/resources/META-INF/plexus/components.xml create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockBuilder.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockMojo.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmBaseObjectTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmLinkTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageAssociationTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleDirectiveTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRendererTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmTriggerTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojoTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojoTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojoTest.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/package-info.java create mode 100644 maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/package-info.java create mode 100644 maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template create mode 100644 maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template-expected create mode 100644 maven-plugin-rpm/src/test/resources/rpm/RpmPackage.sh create mode 100644 maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template create mode 100644 maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template-expected create mode 100644 rpm-ant/NOTICE.txt create mode 100644 rpm-ant/build.gradle create mode 100644 rpm-ant/config/checkstyle/checkstyle.xml create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/BuiltIn.java create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/Conflicts.java create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/Depends.java create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/Obsoletes.java create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/Provides.java create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/RpmFileSet.java create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/RpmTask.java create mode 100644 rpm-ant/src/main/java/org/xbib/rpm/ant/package-info.java create mode 100644 rpm-ant/src/main/resources/org/xbib/rpm/antlib.xml create mode 100644 rpm-ant/src/test/java/org/xbib/rpm/ant/RpmTaskTest.java create mode 100644 rpm-ant/src/test/java/org/xbib/rpm/ant/package-info.java create mode 100644 rpm-ant/src/test/resources/org/xbib/rpm/changelog/changelog create mode 100644 rpm-ant/src/test/resources/pgp/test-secring.gpg create mode 100644 rpm-ant/src/test/resources/postin.sh create mode 100644 rpm-ant/src/test/resources/postun.sh create mode 100644 rpm-ant/src/test/resources/prein.sh create mode 100644 rpm-ant/src/test/resources/preun.sh create mode 100644 rpm-core/NOTICE.txt create mode 100644 rpm-core/build.gradle create mode 100644 rpm-core/config/checkstyle/checkstyle.xml create mode 100644 rpm-core/src/main/java/org/xbib/rpm/RpmBuilder.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/RpmReader.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogHandler.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogParser.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/changelog/ParsingState.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/changelog/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/ChangelogParseException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/DatesOutOfSequenceException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/IncompleteChangelogEntryException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/InvalidChangelogDateException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/InvalidDirectiveException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/InvalidPathException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/NoInitialAsteriskException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/PathOutsideBuildPathException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/RpmException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/SigningKeyNotFoundException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/UnknownArchitectureException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/UnknownOperatingSystemException.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/exception/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/format/Flags.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/format/Format.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/format/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/AbstractHeader.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/EntryType.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/Header.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/HeaderTag.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/Tags.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/AbstractSpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/BinSpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/I18NStringSpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/Int16SpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/Int32SpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/Int64SpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/Int8SpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/SpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/StringArraySpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/StringSpecEntry.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/entry/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/header/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/io/ChannelWrapper.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/io/ReadableChannelWrapper.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/io/WritableChannelWrapper.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/io/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/lead/Architecture.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/lead/Lead.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/lead/Os.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/lead/PackageType.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/lead/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/CompressionType.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/Contents.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/CpioHeader.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/Directive.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/EmptyDir.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/Ghost.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/Link.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/payload/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/security/HashAlgo.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/security/KeyDumper.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/security/KeyGenerator.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/security/SignatureGenerator.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/security/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/signature/SignatureHeader.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/signature/SignatureTag.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/signature/package-info.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/AbstractTrigger.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/Depends.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/Trigger.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerIn.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPostUn.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPreIn.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerUn.java create mode 100644 rpm-core/src/main/java/org/xbib/rpm/trigger/package-info.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/RpmBuilderTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/RpmReaderTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/SimpleRpmTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogHandlerTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogParserTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/changelog/package-info.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/exception/InvalidDirectiveExceptionTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/exception/InvalidPathExceptionTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/exception/PathOutsideBuildPathExceptionTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/exception/SigningKeyNotFoundExceptionTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/exception/UnknownArchitectureExceptionTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/exception/UnknownOperatingSystemExceptionTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/exception/package-info.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/header/HeaderTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/header/package-info.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/package-info.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/payload/ContentsTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/payload/package-info.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/security/PrintPublicKeyTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/security/SignatureGeneratorTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/security/SignatureReaderTest.java create mode 100644 rpm-core/src/test/java/org/xbib/rpm/security/package-info.java create mode 100644 rpm-core/src/test/resources/org/xbib/rpm/changelog/bad.changelog create mode 100644 rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog create mode 100644 rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog.with.comments create mode 100644 rpm-core/src/test/resources/pgp/test-pubring.gpg create mode 100644 rpm-core/src/test/resources/pgp/test-secring.gpg create mode 100644 rpm-core/src/test/resources/postin.sh create mode 100644 rpm-core/src/test/resources/postun.sh create mode 100644 rpm-core/src/test/resources/prein.sh create mode 100644 rpm-core/src/test/resources/preun.sh create mode 100644 rpm-core/src/test/resources/rpm-1-1.0-1.noarch.rpm create mode 100644 rpm-core/src/test/resources/rpm-3-1.0-1.somearch.rpm create mode 100644 rpm-core/src/test/resources/signature-my-ring-test-1.0-1.noarch.rpm create mode 100644 rpm-core/src/test/resources/signing-test-1.0-1.noarch.rpm create mode 100644 rpm-core/src/test/resources/test.txt create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5b797d --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +data +work +out +logs +/.idea +/target +/.settings +/.classpath +/.project +/.gradle +/plugins +/sessions +.DS_Store +*.iml +*~ +.secret +build +**/alkmene.json +**/alkmene*.options +**/herakles.json +**/herakles*.options +**/prod.json +**/prod*.options +**/*.crt +**/*.pkcs8 +**/*.gz diff --git a/LICENSE-bouncycastle.txt b/LICENSE-bouncycastle.txt new file mode 100644 index 0000000..2f4a24f --- /dev/null +++ b/LICENSE-bouncycastle.txt @@ -0,0 +1,18 @@ +Copyright (c) 2000 - 2017 The Legion Of The Bouncy Castle (http://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..984f944 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,203 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..44c2b3c --- /dev/null +++ b/build.gradle @@ -0,0 +1,108 @@ +plugins { + id "org.sonarqube" version "2.5" + id "io.codearte.nexus-staging" version "0.7.0" +} + +allprojects { + + apply plugin: 'java' + apply plugin: 'maven' + apply plugin: 'signing' + apply plugin: 'findbugs' + apply plugin: 'pmd' + apply plugin: 'checkstyle' + apply plugin: "jacoco" + + repositories { + mavenLocal() + mavenCentral() + } + + configurations { + wagon + } + + dependencies { + testCompile 'junit:junit:4.12' + wagon 'org.apache.maven.wagon:wagon-ssh:2.12' + } + + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-profile" << "compact1" + } + + test { + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + } + } + + clean { + delete 'out' + } + + tasks.withType(FindBugs) { + ignoreFailures = true + reports { + xml.enabled = false + html.enabled = true + } + } + tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } + } + tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } + } + jacocoTestReport { + reports { + xml.enabled = true + csv.enabled = false + } + } + + sonarqube { + properties { + property "sonar.projectName", "xbib RPM" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.java.coveragePlugin", "jacoco" + property "sonar.junit.reportsPath", "build/test-results/test/" + } + } + + task sourcesJar(type: Jar, dependsOn: classes) { + classifier 'sources' + from sourceSets.main.allSource + } + task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' + } + artifacts { + archives sourcesJar, javadocJar + } + if (project.hasProperty('signing.keyId')) { + signing { + sign configurations.archives + } + } + + apply from: "${rootProject.projectDir}/gradle/ext.gradle" + apply from: "${rootProject.projectDir}/gradle/publish.gradle" + +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..55e59d2 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/maven/repo-settings.xml b/config/maven/repo-settings.xml new file mode 100644 index 0000000..5968882 --- /dev/null +++ b/config/maven/repo-settings.xml @@ -0,0 +1,24 @@ + + ~/.m2/repository + + xbib + + + + xbib + + + xbib + http://xbib.org/repository + + true + always + + + true + + + + + + \ No newline at end of file diff --git a/gradle-plugin-rpm/NOTICE.txt b/gradle-plugin-rpm/NOTICE.txt new file mode 100644 index 0000000..f188c12 --- /dev/null +++ b/gradle-plugin-rpm/NOTICE.txt @@ -0,0 +1,5 @@ +This is a derived work of + +https://github.com/nebula-plugins/gradle-ospackage-plugin + +licensed under Apache Software License 2.0. diff --git a/gradle-plugin-rpm/build.gradle b/gradle-plugin-rpm/build.gradle new file mode 100644 index 0000000..ecd89db --- /dev/null +++ b/gradle-plugin-rpm/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'java-gradle-plugin' + id 'com.gradle.plugin-publish' version '0.9.7' +} + +apply plugin: 'groovy' +apply plugin: 'maven' +apply plugin: 'signing' +apply plugin: 'com.gradle.plugin-publish' + +dependencies { + compile gradleApi() + compile project(':rpm-core') + compileOnly "org.codehaus.groovy:groovy-all:${project.property('groovy.version')}" + testCompile "junit:junit:${project.property('junit.version')}" + testCompile("org.spockframework:spock-core:${project.property('spock-core.version')}") { + exclude module: 'groovy-all' + exclude module: 'junit' + } + testCompile "org.xbib:guice:${project.property('xbib-guice.version')}" + +} + +compileGroovy { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +test { + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } +} + +if (project.hasProperty('gradle.publish.key')) { + pluginBundle { + website = 'https://github.com/xbib/gradle-plugin-rpm' + vcsUrl = 'https://github.com/xbib/gradle-plugin-rpm' + plugins { + rpmPlugin { + id = 'org.xbib.gradle.plugin.rpm' + version = project.version + description = projectDescription + displayName = projectDescription + tags = ['gradle', 'plugin', 'rpm'] + } + } + } +} \ No newline at end of file diff --git a/gradle-plugin-rpm/config/checkstyle/checkstyle.xml b/gradle-plugin-rpm/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..55e59d2 --- /dev/null +++ b/gradle-plugin-rpm/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancement.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancement.groovy new file mode 100644 index 0000000..7d3323d --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancement.groovy @@ -0,0 +1,77 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.file.CopySpec +import org.gradle.api.internal.file.copy.CopySpecWrapper +import org.gradle.internal.impldep.org.apache.commons.lang.reflect.FieldUtils + +/** + * + */ +@Category(CopySpec) +class CopySpecEnhancement { + + static void appendFieldToCopySpec(CopySpec spec, String fieldName, Object value) { + def directSpec = spec + if (spec instanceof CopySpecWrapper) { + def delegateField = FieldUtils.getField(CopySpecWrapper, 'delegate', true) + directSpec = delegateField.get(spec) + } + directSpec.metaClass["get${fieldName.capitalize()}"] = { value } + } + + static void user(CopySpec spec, String user) { + appendFieldToCopySpec(spec, 'user', user) + } + + static void setUser(CopySpec spec, String userArg) { + user(spec, userArg) + } + + static void permissionGroup(CopySpec spec, String permissionGroup) { + appendFieldToCopySpec(spec, 'permissionGroup', permissionGroup) + } + + static void setPermissionGroup(CopySpec spec, String permissionGroupArg) { + permissionGroup(spec, permissionGroupArg) + } + + static void setFileType(CopySpec spec, List fileTypeArg) { + fileType(spec, fileTypeArg) + } + + static void fileType(CopySpec spec, List fileType) { + appendFieldToCopySpec(spec, 'fileType', fileType) + } + + static void addParentDirs(CopySpec spec, boolean addParentDirs) { + appendFieldToCopySpec(spec, 'addParentDirs', addParentDirs) + } + + static void setAddParentDirs(CopySpec spec, boolean addParentDirsArg) { + addParentDirs(spec, addParentDirsArg) + } + + static void createDirectoryEntry(CopySpec spec, boolean createDirectoryEntry) { + appendFieldToCopySpec(spec, 'createDirectoryEntry', createDirectoryEntry) + } + + static void setCreateDirectoryEntry(CopySpec spec, boolean createDirectoryEntryArg) { + createDirectoryEntry(spec, createDirectoryEntryArg) + } + + static void uid(CopySpec spec, int uid) { + appendFieldToCopySpec(spec, 'uid', uid) + } + + static void setUid(CopySpec spec, int uidArg) { + uid(spec, uidArg) + } + + static void gid(CopySpec spec, int gid) { + appendFieldToCopySpec(spec, 'gid', gid) + } + + static void setGid(CopySpec spec, int gidArg) { + gid(spec, gidArg) + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Dependency.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Dependency.groovy new file mode 100755 index 0000000..c3eaead --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Dependency.groovy @@ -0,0 +1,55 @@ +package org.xbib.gradle.plugin.rpm + +import groovy.transform.EqualsAndHashCode +import org.xbib.rpm.format.Flags + +@EqualsAndHashCode +class Dependency implements Serializable { + + String packageName + + String version + + int flag = 0 + + Dependency alternative = null + + Dependency(String packageName, String version, int flag=0) { + if (packageName.contains(',')) { + throw new IllegalArgumentException('package name can not contain comma') + } + this.packageName = packageName + this.version = version + this.flag = flag + } + + Dependency or(String packageName, String version='', int flag=0) { + alternative = new Dependency(packageName, version, flag) + alternative + } + + String toDebString() { + def signMap = [ + (Flags.GREATER|Flags.EQUAL): '>=', + (Flags.LESS|Flags.EQUAL): '<=', + (Flags.EQUAL): '=', + (Flags.GREATER): '>>', + (Flags.LESS): '<<' + ] + + def depStr = this.packageName + if (this.flag && this.version) { + def sign = signMap[this.flag] + if (sign==null) { + throw new IllegalArgumentException() + } + depStr += " (${sign} ${this.version})" + } else if (this.version) { + depStr += " (${this.version})" + } + if (alternative) { + depStr += " | " + alternative.toDebString() + } + depStr + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Directory.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Directory.groovy new file mode 100644 index 0000000..b3c1c1f --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Directory.groovy @@ -0,0 +1,14 @@ +package org.xbib.gradle.plugin.rpm + +class Directory { + + String path + + int permissions = -1 + + String user = null + + String permissionGroup = null + + boolean addParents = false +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/FromConfigurationFactory.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/FromConfigurationFactory.groovy new file mode 100644 index 0000000..b591594 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/FromConfigurationFactory.groovy @@ -0,0 +1,21 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.file.FileCopyDetails +import org.gradle.api.tasks.AbstractCopyTask + +import java.nio.file.Files + +class FromConfigurationFactory { + + static Closure preserveSymlinks(delegate) { + return { + delegate.eachFile { FileCopyDetails details -> + if (Files.isSymbolicLink(details.file.toPath())) { + details.exclude() + def toFile = Files.readSymbolicLink(details.file.toPath()).toFile() + delegate.link(details.relativePath.toString(), toFile.toString()) + } + } + } + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Link.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Link.groovy new file mode 100755 index 0000000..93ff8a4 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Link.groovy @@ -0,0 +1,13 @@ +package org.xbib.gradle.plugin.rpm + +import groovy.transform.EqualsAndHashCode + +@EqualsAndHashCode +class Link implements Serializable { + + String path + + String target + + int permissions = -1 +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/ProjectPackagingExtension.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/ProjectPackagingExtension.groovy new file mode 100644 index 0000000..03cc747 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/ProjectPackagingExtension.groovy @@ -0,0 +1,257 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.file.CopyProcessingSpec +import org.gradle.api.file.CopySpec +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.FileCopyDetails +import org.gradle.api.file.FileTree +import org.gradle.api.file.FileTreeElement +import org.gradle.api.file.RelativePath +import org.gradle.api.internal.file.FileResolver +import org.gradle.api.internal.file.copy.CopySpecInternal +import org.gradle.api.internal.file.copy.DefaultCopySpec +import org.gradle.api.internal.project.ProjectInternal +import org.gradle.api.specs.Spec +import org.gradle.internal.reflect.Instantiator + +import java.util.regex.Pattern + +/** + * An extension which can be attached to the project. + * This is a superset of SystemPackagingExtension because we don't + * want the @Delegate to inherit the copy spec parts. + * + * We can't extends DefaultCopySpec, since it's @NotExtensible, meaning that we won't get any convention + * mappings. If we extend DelegatingCopySpec we get groovy compilation errors around the return types between + * CopySourceSpec's methods and the ones overriden in DelegatingCopySpec, even though that's perfectly valid + * Java code. The theory is that it's some bug in groovyc. + */ +class ProjectPackagingExtension extends SystemPackagingExtension { + + CopySpecInternal delegateCopySpec + + ProjectPackagingExtension(Project project) { + FileResolver resolver = ((ProjectInternal) project).getFileResolver() + Instantiator instantiator = ((ProjectInternal) project).getServices().get(Instantiator.class) + delegateCopySpec = new DefaultCopySpec( resolver, instantiator) + } + + /* + * Special Use cases that involve Closure's which we want to wrap. + */ + CopySpec from(Object sourcePath, Closure c) { + def preserveSymlinks = FromConfigurationFactory.preserveSymlinks(this) + use(CopySpecEnhancement) { + return getDelegateCopySpec().from(sourcePath, c << preserveSymlinks) + } + } + + CopySpec from(Object... sourcePaths) { + def spec = null + for (Object sourcePath : sourcePaths) { + spec = from(sourcePath, {}) + } + spec + } + + CopySpec into(Object destPath, Closure configureClosure) { + use(CopySpecEnhancement) { + return getDelegateCopySpec().into(destPath, configureClosure) + } + } + + CopySpec include(Closure includeSpec) { + use(CopySpecEnhancement) { + return getDelegateCopySpec().include(includeSpec) + } + } + + CopySpec exclude(Closure excludeSpec) { + use(CopySpecEnhancement) { + return getDelegateCopySpec().exclude(excludeSpec) + } + } + + CopySpec filter(Closure closure) { + use(CopySpecEnhancement) { + return getDelegateCopySpec().filter(closure) + } + } + + CopySpec rename(Closure closure) { + use(CopySpecEnhancement) { + return getDelegateCopySpec().rename(closure) + } + } + + CopySpec eachFile(Closure closure) { + use(CopySpecEnhancement) { + return getDelegateCopySpec().eachFile(closure) + } + } + + /* + * Copy and Paste from org.gradle.api.internal.file.copy.DelegatingCopySpec, since extending it causes + * compilation problems. The methods above are special cases and are commented out below. + */ + RelativePath getDestPath() { + getDelegateCopySpec().getDestPath() + } + + FileTree getSource() { + getDelegateCopySpec().getSource() + } + + boolean hasSource() { + getDelegateCopySpec().hasSource() + } + + Collection> getAllCopyActions() { + getDelegateCopySpec().getAllCopyActions() + } + + boolean isCaseSensitive() { + getDelegateCopySpec().isCaseSensitive() + } + + void setCaseSensitive(boolean caseSensitive) { + getDelegateCopySpec().setCaseSensitive(caseSensitive) + } + + boolean getIncludeEmptyDirs() { + getDelegateCopySpec().getIncludeEmptyDirs() + } + + void setIncludeEmptyDirs(boolean includeEmptyDirs) { + getDelegateCopySpec().setIncludeEmptyDirs(includeEmptyDirs) + } + + DuplicatesStrategy getDuplicatesStrategy() { + getDelegateCopySpec().getDuplicatesStrategy() + } + + void setDuplicatesStrategy(DuplicatesStrategy strategy) { + getDelegateCopySpec().setDuplicatesStrategy(strategy) + } + + CopySpec filesMatching(String pattern, Action action) { + getDelegateCopySpec().filesMatching(pattern, action) + } + + CopySpec filesNotMatching(String pattern, Action action) { + getDelegateCopySpec().filesNotMatching(pattern, action) + } + + CopySpec with(CopySpec... sourceSpecs) { + getDelegateCopySpec().with(sourceSpecs) + } + + CopySpec setIncludes(Iterable includes) { + getDelegateCopySpec().setIncludes(includes) + } + + CopySpec setExcludes(Iterable excludes) { + getDelegateCopySpec().setExcludes(excludes) + } + + CopySpec include(String... includes) { + getDelegateCopySpec().include(includes) + } + + CopySpec include(Iterable includes) { + getDelegateCopySpec().include(includes) + } + + CopySpec include(Spec includeSpec) { + getDelegateCopySpec().include(includeSpec) + } + + CopySpec exclude(String... excludes) { + getDelegateCopySpec().exclude(excludes) + } + + CopySpec exclude(Iterable excludes) { + getDelegateCopySpec().exclude(excludes) + } + + CopySpec exclude(Spec excludeSpec) { + getDelegateCopySpec().exclude(excludeSpec) + } + + CopySpec into(Object destPath) { + getDelegateCopySpec().into(destPath) + } + + CopySpec rename(String sourceRegEx, String replaceWith) { + getDelegateCopySpec().rename(sourceRegEx, replaceWith) + } + + CopyProcessingSpec rename(Pattern sourceRegEx, String replaceWith) { + getDelegateCopySpec().rename(sourceRegEx, replaceWith) + } + + CopySpec filter(Map properties, Class filterType) { + getDelegateCopySpec().filter(properties, filterType) + } + + CopySpec filter(Class filterType) { + getDelegateCopySpec().filter(filterType) + } + + CopySpec expand(Map properties) { + getDelegateCopySpec().expand(properties) + } + + CopySpec eachFile(Action action) { + getDelegateCopySpec().eachFile(action) + } + + Integer getFileMode() { + getDelegateCopySpec().getFileMode() + } + + CopyProcessingSpec setFileMode(Integer mode) { + getDelegateCopySpec().setFileMode(mode) + } + + Integer getDirMode() { + getDelegateCopySpec().getDirMode() + } + + CopyProcessingSpec setDirMode(Integer mode) { + getDelegateCopySpec().setDirMode(mode) + } + + Set getIncludes() { + getDelegateCopySpec().getIncludes() + } + + Set getExcludes() { + getDelegateCopySpec().getExcludes() + } + + Iterable getChildren() { + getDelegateCopySpec().getChildren() + } + + FileTree getAllSource() { + getDelegateCopySpec().getAllSource() + } + + DefaultCopySpec addChild() { + getDelegateCopySpec().addChild() + } + + DefaultCopySpec addFirst() { + getDelegateCopySpec().addFirst() + } + + void walk(Action action) { + action.execute(this) + for (CopySpecInternal child : getChildren()) { + child.walk(action) + } + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Rpm.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Rpm.groovy new file mode 100755 index 0000000..7c710bc --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/Rpm.groovy @@ -0,0 +1,390 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.ConventionMapping +import org.gradle.api.internal.IConventionAware +import org.gradle.api.tasks.AbstractCopyTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.bundling.AbstractArchiveTask +import org.xbib.rpm.lead.Architecture +import org.xbib.rpm.lead.Os +import org.xbib.rpm.lead.PackageType + +import java.nio.file.Path + +/** + * + */ +class Rpm extends AbstractArchiveTask { + + @Input + @Optional + Path changeLogFile + + @Delegate + @Nested + SystemPackagingExtension systemPackagingExtension + + ProjectPackagingExtension projectPackagingExtension + + Rpm() { + super() + systemPackagingExtension = new SystemPackagingExtension() + projectPackagingExtension = project.extensions.findByType(ProjectPackagingExtension) + if (projectPackagingExtension) { + getRootSpec().with(projectPackagingExtension.delegateCopySpec) + } + extension = 'rpm' + } + + @Override + @TaskAction + protected void copy() { + use(CopySpecEnhancement) { + super.copy() + } + } + + @Override + AbstractCopyTask from(Object... sourcePaths) { + for (Object sourcePath : sourcePaths) { + from(sourcePath, {}) + } + this + } + + @Override + AbstractCopyTask from(Object sourcePath, Closure c) { + def preserveSymlinks = FromConfigurationFactory.preserveSymlinks(this) + use(CopySpecEnhancement) { + getMainSpec().from(sourcePath, c << preserveSymlinks) + } + this + } + + @Override + AbstractArchiveTask into(Object destPath, Closure configureClosure) { + use(CopySpecEnhancement) { + getMainSpec().into(destPath, configureClosure) + } + this + } + + @Override + AbstractCopyTask exclude(Closure excludeSpec) { + use(CopySpecEnhancement) { + getMainSpec().exclude(excludeSpec) + } + this + } + + @Override + AbstractCopyTask filter(Closure closure) { + use(CopySpecEnhancement) { + getMainSpec().filter(closure) + } + this + } + + @Override + AbstractCopyTask rename(Closure closure) { + use(CopySpecEnhancement) { + getMainSpec().rename(closure) + } + this + } + + @Override + RpmCopyAction createCopyAction() { + new RpmCopyAction(this) + } + + @Input + @Optional + void setArch(Object arch) { + setArchStr((arch instanceof Architecture)?arch.name():arch.toString()) + } + + @Input + @Optional + List getAllConfigurationPaths() { + return getConfigurationPaths() + (projectPackagingExtension?.getConfigurationPaths()?: []) + } + + @Input + @Optional + List getAllPreInstallCommands() { + return getPreInstallCommands() + (projectPackagingExtension?.getPreInstallCommands() ?: []) + } + + @Input + @Optional + List getAllPostInstallCommands() { + return getPostInstallCommands() + (projectPackagingExtension?.getPostInstallCommands() ?: []) + } + + @Input + @Optional + List getAllPreUninstallCommands() { + return getPreUninstallCommands() + (projectPackagingExtension?.getPreUninstallCommands() ?: []) + } + + @Input + @Optional + List getAllPostUninstallCommands() { + return getPostUninstallCommands() + (projectPackagingExtension?.getPostUninstallCommands() ?: []) + } + + @Input + @Optional + List getAllPreTransCommands() { + return getPreTransCommands() + projectPackagingExtension?.getPreTransCommands() + } + + @Input + @Optional + List getAllPostTransCommands() { + return getPostTransCommands() + projectPackagingExtension?.getPostTransCommands() + } + + @Input + @Optional + List getAllCommonCommands() { + return getCommonCommands() + projectPackagingExtension?.getCommonCommands() + } + + @Input + @Optional + List getAllSupplementaryControlFiles() { + return getSupplementaryControlFiles() + (projectPackagingExtension?.getSupplementaryControlFiles() ?: []) + } + + @Input + @Optional + List getAllLinks() { + if (projectPackagingExtension) { + return getLinks() + projectPackagingExtension.getLinks() + } else { + return getLinks() + } + } + + @Input + @Optional + List getAllDependencies() { + if (projectPackagingExtension) { + return getDependencies() + projectPackagingExtension.getDependencies() + } else { + return getDependencies() + } + } + + @Input + @Optional + def getAllPrefixes() { + if (projectPackagingExtension) { + return (getPrefixes() + projectPackagingExtension.getPrefixes()).unique() + } else { + return getPrefixes() + } + } + + @Input + @Optional + List getAllProvides() { + if (projectPackagingExtension) { + return projectPackagingExtension.getProvides() + getProvides() + } else { + return getProvides() + } + } + + @Input + @Optional + List getAllObsoletes() { + if (projectPackagingExtension) { + return getObsoletes() + projectPackagingExtension.getObsoletes() + } else { + return getObsoletes() + } + } + + @Input + @Optional + List getAllConflicts() { + if (projectPackagingExtension) { + return getConflicts() + projectPackagingExtension.getConflicts() + } else { + return getConflicts() + } + } + + /** + * Defines input files annotation with @SkipWhenEmpty as a workaround to force building the archive even if no + * from clause is declared. Without this method the task would be marked UP-TO-DATE - the actual archive creation + * would be skipped. + * + * The provided file collection is not supposed to be used or modified anywhere else in the task. + * + * @return Collection of files + */ + @InputFiles + @SkipWhenEmpty + private final FileCollection getFakeFiles() { + project.files('fake') + } + + void applyConventions() { + ConventionMapping mapping = ((IConventionAware) this).getConventionMapping() + + mapping.map('packageName', { + projectPackagingExtension?.getPackageName()?:getBaseName() + }) + mapping.map('release', { + projectPackagingExtension?.getRelease()?:getClassifier() + }) + mapping.map('version', { + sanitizeVersion(projectPackagingExtension?.getVersion()?:project.getVersion().toString()) + }) + mapping.map('epoch', { + projectPackagingExtension?.getEpoch()?:0 + }) + mapping.map('signingKeyId', { + projectPackagingExtension?.getSigningKeyId() + }) + mapping.map('signingKeyPassphrase', { + projectPackagingExtension?.getSigningKeyPassphrase() + }) + mapping.map('signingKeyRing', { + projectPackagingExtension?.getSigningKeyRing() + }) + mapping.map('user', { + projectPackagingExtension?.getUser()?:getPackager() + }) + mapping.map('maintainer', { + projectPackagingExtension?.getMaintainer()?:getPackager() + }) + mapping.map('uploaders', { + projectPackagingExtension?.getUploaders()?:getPackager() + }) + mapping.map('permissionGroup', { + projectPackagingExtension?.getPermissionGroup()?:'' + }) + mapping.map('packageGroup', { + projectPackagingExtension?.getPackageGroup() + }) + mapping.map('buildHost', { + projectPackagingExtension?.getBuildHost()?: getLocalHostName() + }) + mapping.map('summary', { + projectPackagingExtension?.getSummary()?:getPackageName() + }) + mapping.map('packageDescription', { + String packageDescription = projectPackagingExtension?.getPackageDescription()?:project.getDescription() + packageDescription ?: '' + }) + mapping.map('license', { + projectPackagingExtension?.getLicense()?:'' + }) + mapping.map('packager', { + projectPackagingExtension?.getPackager()?:System.getProperty('user.name', '') + }) + mapping.map('distribution', { + projectPackagingExtension?.getDistribution()?:'' + }) + mapping.map('vendor', { + projectPackagingExtension?.getVendor()?:'' + }) + mapping.map('url', { + projectPackagingExtension?.getUrl()?:'' + }) + mapping.map('sourcePackage', { + projectPackagingExtension?.getSourcePackage()?:'' + }) + mapping.map('createDirectoryEntry', { + projectPackagingExtension?.getCreateDirectoryEntry()?:false + }) + mapping.map('priority', { + projectPackagingExtension?.getPriority()?:'optional' + }) + mapping.map('preInstall', { + projectPackagingExtension?.getPreInstall() + }) + mapping.map('postInstall', { + projectPackagingExtension?.getPostInstall() + }) + mapping.map('preUninstall', { + projectPackagingExtension?.getPreUninstall() + }) + mapping.map('postUninstall', { + projectPackagingExtension?.getPostUninstall() + }) + mapping.map('archiveName', { + assembleArchiveName() + }) + mapping.map('fileType', { + projectPackagingExtension?.getFileType() + }) + mapping.map('addParentDirs', { + projectPackagingExtension?.getAddParentDirs()?:true + }) + mapping.map('archStr', { + projectPackagingExtension?.getArchStr()?:Architecture.NOARCH.name() + }) + mapping.map('os', { + projectPackagingExtension?.getOs()?:Os.UNKNOWN + }) + mapping.map('type', { + projectPackagingExtension?.getType()?:PackageType.BINARY + }) + mapping.map('prefixes', { + projectPackagingExtension?.getPrefixes()?:[] + }) + } + + String assembleArchiveName() { + String name = getPackageName() + name += getVersion() ? "-${getVersion()}" : '' + name += getRelease() ? "-${getRelease()}" : '' + name += getArchString() ? ".${getArchString()}" : '' + name += getExtension() ? ".${getExtension()}" : '' + name + } + + String getArchString() { + getArchStr()?.toLowerCase() + } + + void prefixes(String... addPrefixes) { + systemPackagingExtension.prefixes.addAll(addPrefixes) + } + + List getPrefixes() { + systemPackagingExtension.prefixes + } + + void setChangeLogFile(Path changeLogFile) { + this.changeLogFile = changeLogFile + } + + Path getChangeLogFile() { + changeLogFile + } + + static String sanitizeVersion(String version) { + version.replaceAll(/\+.*/, '').replaceAll(/-/, '~') + } + + static String getLocalHostName() { + try { + return InetAddress.localHost.hostName + } catch (UnknownHostException ignore) { + return "unknown" + } + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmCopyAction.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmCopyAction.groovy new file mode 100755 index 0000000..c319230 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmCopyAction.groovy @@ -0,0 +1,317 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.internal.file.CopyActionProcessingStreamAction +import org.gradle.api.internal.file.copy.CopyAction +import org.gradle.api.internal.file.copy.CopyActionProcessingStream +import org.gradle.api.internal.file.copy.CopySpecInternal +import org.gradle.api.internal.file.copy.CopySpecResolver +import org.gradle.api.internal.file.copy.DefaultCopySpec +import org.gradle.api.internal.file.copy.DefaultFileCopyDetails +import org.gradle.api.internal.file.copy.FileCopyDetailsInternal +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.gradle.api.tasks.WorkResult +import org.gradle.api.tasks.WorkResults +import org.gradle.internal.UncheckedException +import org.xbib.gradle.plugin.rpm.validation.RpmTaskPropertiesValidator +import org.xbib.rpm.RpmBuilder +import org.xbib.rpm.lead.Architecture +import org.xbib.rpm.header.HeaderTag +import org.xbib.rpm.payload.Directive + +import java.lang.reflect.Field +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption + +/** + * + */ +class RpmCopyAction implements CopyAction { + + private static final Logger logger = Logging.getLogger(RpmCopyAction.class) + + Rpm task + + Path tempDir + + RpmBuilder builder + + boolean includeStandardDefines = true + + private final RpmTaskPropertiesValidator rpmTaskPropertiesValidator = new RpmTaskPropertiesValidator() + + private RpmFileVisitorStrategy rpmFileVisitorStrategy + + RpmCopyAction(Rpm task) { + this.task = task + rpmTaskPropertiesValidator.validate(task) + } + + WorkResult execute(CopyActionProcessingStream stream) { + try { + startVisit(this) + stream.process(new StreamAction()) + endVisit() + } catch (Exception e) { + UncheckedException.throwAsUncheckedException(e) + } + WorkResults.didWork(true) + } + + private class StreamAction implements CopyActionProcessingStreamAction { + + @Override + void processFile(FileCopyDetailsInternal details) { + def ourSpec = extractSpec(details) + if (details.isDirectory()) { + visitDir(details, ourSpec) + } else { + visitFile(details, ourSpec) + } + } + } + + void endVisit() { + for (Link link : task.getAllLinks()) { + logger.debug "adding link {} -> {}", link.path, link.target + addLink link + } + for (Dependency dep : task.getAllDependencies()) { + logger.debug "adding dependency on {} {}", dep.packageName, dep.version + addDependency dep + } + for (Dependency obsolete: task.getAllObsoletes()) { + logger.debug "adding obsoletes on {} {}", obsolete.packageName, obsolete.version + addObsolete obsolete + } + for (Dependency conflict : task.getAllConflicts()) { + logger.debug "adding conflicts on {} {}", conflict.packageName, conflict.version + addConflict conflict + } + for (Dependency provides : task.getAllProvides()) { + logger.debug "adding provides on {} {}", provides.packageName, provides.version + addProvides(provides) + } + task.directories.each { directory -> + logger.debug "adding directory {}", directory.path + addDirectory(directory) + } + end() + } + + static String concat(Collection scripts) { + String shebang + StringBuilder result = new StringBuilder() + scripts.each { script -> + script?.eachLine { line -> + if (line.matches('^#!.*$')) { + if (!shebang) { + shebang = line + } else if (line != shebang) { + throw new IllegalArgumentException("mismatching #! script lines") + } + } else { + result.append line + result.append "\n" + } + } + } + if (shebang) { + result.insert(0, shebang + "\n") + } + result.toString() + } + + static CopySpecInternal extractSpec(FileCopyDetailsInternal fileDetails) { + if (fileDetails instanceof DefaultFileCopyDetails) { + def startingClass = fileDetails.getClass() + while( startingClass != null && startingClass != DefaultFileCopyDetails) { + startingClass = startingClass.superclass + } + Field specField = startingClass.getDeclaredField('specResolver') + specField.setAccessible(true) + CopySpecResolver specResolver = specField.get(fileDetails) + Field field = DefaultCopySpec.DefaultCopySpecResolver.class.getDeclaredField('this$0') + field.setAccessible(true) + CopySpecInternal spec = field.get(specResolver) + return spec + } else { + return null + } + } + + Path extractPath(FileCopyDetailsInternal fileDetails) { + Path path + try { + path = fileDetails.getFile().toPath() + } catch (UnsupportedOperationException uoe) { + path = tempDir.resolve(fileDetails.path) + fileDetails.copyTo(path.toFile()) + } + path + } + + void startVisit(CopyAction action) { + tempDir = task.getTemporaryDir().toPath() + if (!task.getVersion()) { + throw new IllegalArgumentException('RPM requires a version string') + } + if ([task.preInstall, task.postInstall, task.preUninstall, task.postUninstall].any()) { + logger.warn('at least one of (preInstall|postInstall|preUninstall|postUninstall) is defined ' + + 'and will be ignored for RPM builds') + } + builder = new RpmBuilder() + builder.setPackage task.packageName, task.version, task.release, task.epoch + builder.setType task.type + builder.setPlatform Architecture.valueOf(task.archStr.toUpperCase()), task.os + builder.setGroup task.packageGroup + builder.setBuildHost task.buildHost + builder.setSummary task.summary + builder.setDescription task.packageDescription ?: '' + builder.setLicense task.license + builder.setPackager task.packager + builder.setDistribution task.distribution + builder.setVendor task.vendor + builder.setUrl task.url + if (task.allPrefixes) { + builder.setPrefixes(task.allPrefixes as String[]) + } + if (task.getSigningKeyId() && task.getSigningKeyPassphrase() && task.getSigningKeyRing()) { + builder.setPrivateKeyId task.getSigningKeyId() + builder.setPrivateKeyPassphrase task.getSigningKeyPassphrase() + builder.setPrivateKeyRing task.getSigningKeyRing() + } + String sourcePackage = task.sourcePackage + if (!sourcePackage) { + sourcePackage = builder.defaultSourcePackage + } + builder.addHeaderEntry HeaderTag.SOURCERPM, sourcePackage + builder.setPreInstall task.getPreInstall() + builder.setPostInstall task.getPostInstall() + builder.setPreUninstall task.getPreUninstall() + builder.setPostUninstall task.getPostUninstall() + builder.setPreTrans task.getPreTrans() + builder.setPostTrans task.getPostTrans() + builder.setPreInstallValue(scriptWithUtils(task.allCommonCommands, task.allPreInstallCommands)) + builder.setPostInstallValue(scriptWithUtils(task.allCommonCommands, task.allPostInstallCommands)) + builder.setPreUninstallValue(scriptWithUtils(task.allCommonCommands, task.allPreUninstallCommands)) + builder.setPostUninstallValue(scriptWithUtils(task.allCommonCommands, task.allPostUninstallCommands)) + builder.setPreTransValue(scriptWithUtils(task.allCommonCommands, task.allPreTransCommands)) + builder.setPostTransValue(scriptWithUtils(task.allCommonCommands, task.allPostTransCommands)) + if (((Rpm) task).changeLogFile != null) { + builder.addChangelog(((Rpm) task).changeLogFile) + } + rpmFileVisitorStrategy = new RpmFileVisitorStrategy(builder) + } + + void visitFile(FileCopyDetailsInternal fileDetails, def specToLookAt) { + logger.debug "adding file {}", fileDetails.relativePath.pathString + def inputFile = extractPath(fileDetails) + EnumSet fileType = lookup(specToLookAt, 'fileType') + String user = lookup(specToLookAt, 'user') ?: task.user + String group = lookup(specToLookAt, 'permissionGroup') ?: task.permissionGroup + int fileMode = lookup(specToLookAt, 'fileMode') ?: fileDetails.mode + def specAddParentsDir = lookup(specToLookAt, 'addParentDirs') + boolean addParentsDir = specAddParentsDir!=null ? specAddParentsDir : task.addParentDirs + rpmFileVisitorStrategy.addFile(fileDetails, inputFile, fileMode, -1, fileType, user, group, addParentsDir) + } + + void visitDir(FileCopyDetailsInternal dirDetails, def specToLookAt) { + if (specToLookAt == null) { + logger.info("Got an empty spec from ${dirDetails.class.name} for ${dirDetails.path}/${dirDetails.name}") + return + } + // Have to take booleans specially, since they would fail an elvis operator if set to false + def specCreateDirectoryEntry = lookup(specToLookAt, 'createDirectoryEntry') + boolean createDirectoryEntry = specCreateDirectoryEntry!=null ? specCreateDirectoryEntry : task.createDirectoryEntry + def specAddParentsDir = lookup(specToLookAt, 'addParentDirs') + boolean addParentsDir = specAddParentsDir != null ? specAddParentsDir : task.addParentDirs + if (createDirectoryEntry) { + logger.debug 'adding directory {}', dirDetails.relativePath.pathString + int dirMode = lookup(specToLookAt, 'dirMode') ?: dirDetails.mode + List directiveList = (lookup(specToLookAt, 'fileType') ?: task.fileType) as List + EnumSet directive = makeDirective(directiveList) + String user = lookup(specToLookAt, 'user') ?: task.user + String group = lookup(specToLookAt, 'permissionGroup') ?: task.permissionGroup + rpmFileVisitorStrategy.addDirectory(dirDetails, dirMode, directive, user, group, addParentsDir) + } + } + + protected void addLink(Link link) { + builder.addLink link.path, link.target, link.permissions + } + + protected void addDependency(Dependency dep) { + builder.addDependency(dep.packageName, dep.flag, dep.version) + } + + protected void addConflict(Dependency dep) { + builder.addConflicts(dep.packageName, dep.flag, dep.version) + } + + protected void addObsolete(Dependency dep) { + builder.addObsoletes(dep.packageName, dep.flag, dep.version) + } + + protected void addProvides(Dependency dep) { + builder.addProvides(dep.packageName, dep.version, dep.flag) + } + + protected void addDirectory(Directory directory) { + def user = directory.user ? directory.user : task.user + def permissionGroup = directory.permissionGroup ? directory.permissionGroup : task.permissionGroup + builder.addDirectory(directory.path, directory.permissions, null, user, permissionGroup, directory.addParents) + } + + protected void end() { + Path path = task.getArchivePath().toPath() + Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING).withCloseable { ch -> + builder.build(ch) + } + logger.info 'Created RPM archive {}', path + } + + String standardScriptDefines() { + includeStandardDefines ? + String.format(" RPM_ARCH=%s \n RPM_OS=%s \n RPM_PACKAGE_NAME=%s \n RPM_PACKAGE_VERSION=%s \n RPM_PACKAGE_RELEASE=%s \n\n", + task.getArchString(), + task.os?.toString()?.toLowerCase() ?: '', + task.getPackageName(), + task.getVersion(), + task.getRelease()) : null + } + + String scriptWithUtils(List utils, List scripts) { + def list = [] + def stdDefines = standardScriptDefines() + if (stdDefines) { + list.add(stdDefines) + } + list.addAll(utils) + list.addAll(scripts) + concat(list) + } + + static T lookup(def specToLookAt, String propertyName) { + if (specToLookAt?.metaClass?.hasProperty(specToLookAt, propertyName) != null) { + def prop = specToLookAt.metaClass.getProperty(specToLookAt, propertyName) + if (prop instanceof MetaBeanProperty) { + return prop?.getProperty(specToLookAt) as T + } else { + return prop as T + } + } else { + return null + } + } + + static EnumSet makeDirective(List strings) { + EnumSet set = EnumSet.of(Directive.NONE) + for (String string : strings) { + set.add(Directive.valueOf(string)) + } + set + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmFileVisitorStrategy.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmFileVisitorStrategy.groovy new file mode 100644 index 0000000..43c64ce --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmFileVisitorStrategy.groovy @@ -0,0 +1,63 @@ +package org.xbib.gradle.plugin.rpm + +import org.xbib.rpm.RpmBuilder +import org.xbib.rpm.payload.Directive +import org.gradle.api.file.FileCopyDetails + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +class RpmFileVisitorStrategy { + + protected final RpmBuilder builder + + RpmFileVisitorStrategy(RpmBuilder builder) { + this.builder = builder + } + + void addFile(FileCopyDetails details, Path source, int mode, int dirmode, EnumSet directive, String uname, String gname, boolean addParents) { + try { + if (!Files.isSymbolicLink(Paths.get(details.file.parentFile.path))) { + addFileToBuilder(details, source, mode, dirmode, directive, uname, gname, addParents) + } + } + catch (UnsupportedOperationException e) { + // For file details that have filters, accessing the file throws this exception + addFileToBuilder(details, source, mode, dirmode, directive, uname, gname, addParents) + } + } + + void addDirectory(FileCopyDetails details, int permissions, EnumSet directive, String uname, + String gname, boolean addParents) { + try { + if (Files.isSymbolicLink(Paths.get(details.file.path))) { + addLinkToBuilder(details) + } + else { + addDirectoryToBuilder(details, permissions, directive, uname, gname, addParents) + } + } catch (UnsupportedOperationException e) { + // For file details that have filters, accessing the directory throws this exception + addDirectoryToBuilder(details, permissions, directive, uname, gname, addParents) + } + } + + protected void addFileToBuilder(FileCopyDetails details, Path source, int mode, int dirmode, EnumSet directive, String uname, String gname, boolean addParents) { + builder.addFile(getRootPath(details), source, mode, dirmode, directive, uname, gname, addParents) + } + + protected void addDirectoryToBuilder(FileCopyDetails details, int permissions, EnumSet directive, String uname, String gname, boolean addParents) { + builder.addDirectory(getRootPath(details), permissions, directive, uname, gname, addParents) + } + + private void addLinkToBuilder(FileCopyDetails details) { + Path path = Paths.get(details.file.path) + Path target = Files.readSymbolicLink(path) + builder.addLink(getRootPath(details), target.toFile().path) + } + + private static String getRootPath(FileCopyDetails details) { + "/${details.path}".toString() + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmPlugin.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmPlugin.groovy new file mode 100644 index 0000000..e3d11f3 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/RpmPlugin.groovy @@ -0,0 +1,65 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.plugins.BasePlugin +import org.xbib.rpm.RpmBuilder +import org.xbib.rpm.lead.Architecture +import org.xbib.rpm.format.Flags +import org.xbib.rpm.lead.Os +import org.xbib.rpm.lead.PackageType +import org.xbib.rpm.payload.Directive +import org.gradle.api.Plugin +import org.gradle.api.Project + +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +/** + * + */ +class RpmPlugin implements Plugin { + + void apply(Project project) { + project.plugins.apply(BasePlugin) + + project.ext.Rpm = Rpm.class + + RpmBuilder.metaClass.getDefaultSourcePackage() { + format.getLead().getName() + "-src.rpm" + } + + project.tasks.withType(Rpm) { Rpm task -> + applyAliases(task) + task.applyConventions() + } + } + + def static applyAliases(def dynamicObjectAware) { + aliasEnumValues(Architecture.values(), dynamicObjectAware) + aliasEnumValues(Os.values(), dynamicObjectAware) + aliasEnumValues(PackageType.values(), dynamicObjectAware) + aliasStaticInstances(Directive.class, dynamicObjectAware) + aliasStaticInstances(Flags.class, int.class, dynamicObjectAware) + } + + private static > void aliasEnumValues(T[] values, dynAware) { + for (T value : values) { + dynAware.metaClass."${value.name()}" = value + } + } + + private static void aliasStaticInstances(Class forClass, dynAware) { + aliasStaticInstances(forClass, forClass, dynAware) + } + + private static void aliasStaticInstances(Class forClass, Class ofClass, dynAware) { + for (Field field : forClass.fields) { + if (field.type == ofClass && hasModifier(field, Modifier.STATIC)) { + dynAware.metaClass."${field.name}" = field.get(null) + } + } + } + + private static boolean hasModifier(Field field, int modifier) { + (field.modifiers & modifier) == modifier + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtension.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtension.groovy new file mode 100755 index 0000000..0d5faf5 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtension.groovy @@ -0,0 +1,490 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.xbib.rpm.lead.Architecture +import org.xbib.rpm.lead.Os +import org.xbib.rpm.lead.PackageType + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Extension that can be used to configure RPM. + */ +class SystemPackagingExtension { + + @Input @Optional + String packageName + + @Input @Optional + String release + + @Input @Optional + String version + + @Input @Optional + Integer epoch + + @Input @Optional + String signingKeyPassphrase + + @Input @Optional + String signingKeyRing + + @Input @Optional + String signingKeyId + + @Input @Optional + String user + + @Input @Optional + String permissionGroup + + @Input @Optional + String packageGroup + + @Input @Optional + String buildHost + + @Input @Optional + String summary + + @Input @Optional + String packageDescription + + @Input @Optional + String license + + @Input @Optional + String packager + + @Input @Optional + String distribution + + @Input @Optional + String vendor + + @Input @Optional + String url + + @Input @Optional + String sourcePackage + + String archStr + + @Input @Optional + void setArch(Object arch) { + archStr = (arch instanceof Architecture) ? arch.name() : arch.toString() + } + + @Input @Optional + List fileType + + @Input @Optional + Boolean createDirectoryEntry + + @Input @Optional + Boolean addParentDirs + + @Input @Optional + Os os + + @Input @Optional + PackageType type + + List prefixes = new ArrayList() + + def prefix(String prefixStr) { + prefixes << prefixStr + return this + } + + @Input @Optional + Integer uid + + @Input @Optional + Integer gid + + @Input @Optional + String maintainer + + @Input @Optional + String uploaders + + @Input @Optional + String priority + + @Input @Optional + final List supplementaryControlFiles = [] + + def supplementaryControl(Object file) { + supplementaryControlFiles << file + return this + } + + @Input @Optional + String preInstall + + @Input @Optional + String postInstall + + @Input @Optional + String preUninstall + + @Input @Optional + String postUninstall + + @Input @Optional + String preTrans + + @Input @Optional + String postTrans + + final List configurationPaths = [] + + final List preInstallCommands = [] + + final List postInstallCommands = [] + + final List preUninstallCommands = [] + + final List postUninstallCommands = [] + + final List preTransCommands = [] + + final List postTransCommands = [] + + final List commonCommands = [] + + def setInstallUtils(Path script) { + installUtils(script) + } + + def installUtils(String script) { + commonCommands << script + return this + } + + def installUtils(Path script) { + commonCommands << script + return this + } + + def setConfigurationPath(String script) { + configurationPath(script) + } + + def configurationPath(String path) { + configurationPaths << path + this + } + + def setPreInstall(String script) { + preInstall(script) + } + def setPreInstall(Path script) { + preInstall(script) + } + def preInstall(String script) { + preInstall(Paths.get(script)) + } + def preInstall(Path script) { + if (Files.exists(script)) { + preInstallValue(script.text) + } + this + } + def preInstallValue(String content) { + preInstallCommands << content + this + } + + def setPostInstall(String script) { + postInstall(script) + } + def setPostInstall(Path script) { + postInstall(script) + } + def postInstall(String script) { + postInstall(Paths.get(script)) + } + def postInstall(Path script) { + if (Files.exists(script)) { + postInstallValue(script.text) + } + this + } + def postInstallValue(String content) { + postInstallCommands << content + this + } + + def setPreUninstall(String script) { + preUninstall(script) + } + def setPreUninstall(Path script) { + preUninstall(script) + } + def preUninstall(String script) { + preUninstall(Paths.get(script)) + } + def preUninstall(Path script) { + if (Files.exists(script)) { + preUninstallValue(script.text) + } + this + } + def preUninstallValue(String script) { + preUninstallCommands << script + this + } + + def setPostUninstall(String script) { + postUninstall(script) + } + def setPostUninstall(Path script) { + postUninstall(script) + } + def postUninstall(String script) { + postUninstall(Paths.get(script)) + } + def postUninstall(Path script) { + if (Files.exists(script)) { + postUninstallValue(script.text) + } + this + } + def postUninstallValue(String content) { + postUninstallCommands << content + this + } + + def setPreTrans(String script) { + preTrans(script) + } + def setPreTrans(Path script) { + preTrans(script) + } + def preTrans(String script) { + preTrans(Paths.get(script)) + } + def preTrans(Path script) { + if (Files.exists(script)) { + preTransValue(script.text) + } + this + } + def preTransValue(String script) { + preTransCommands << script + this + } + + def setPostTrans(String script) { + postTrans(script) + } + def setPostTrans(Path script) { + postTrans(script) + } + def postTrans(String script) { + postTrans(Paths.get(script)) + } + def postTrans(Path script) { + if (Files.exists(script)) { + postTransValue(script.text) + } + return this + } + def postTransValue(String script) { + postTransCommands << script + return this + } + + List links = [] + + Link link(String path, String target) { + link(path, target, -1) + } + + Link link(String path, String target, int permissions) { + Link link = new Link() + link.path = path + link.target = target + link.permissions = permissions + links.add(link) + link + } + + List dependencies = [] + + List obsoletes = [] + + List conflicts = [] + + List recommends = [] + + List suggests = [] + + List enhances = [] + + List preDepends = [] + + List breaks = [] + + List replaces = [] + + List provides = [] + + Dependency requires(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + dependencies.add(dep) + dep + } + + Dependency requires(String packageName, String version){ + requires(packageName, version, 0) + } + + Dependency requires(String packageName) { + requires(packageName, '', 0) + } + + Dependency obsoletes(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + obsoletes.add(dep) + dep + } + + Dependency obsoletes(String packageName) { + obsoletes(packageName, '', 0) + } + + Dependency conflicts(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + conflicts.add(dep) + dep + } + + Dependency conflicts(String packageName) { + conflicts(packageName, '', 0) + } + + Dependency recommends(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + recommends.add(dep) + dep + } + + Dependency recommends(String packageName) { + recommends(packageName, '', 0) + } + + Dependency suggests(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + suggests.add(dep) + dep + } + + Dependency suggests(String packageName) { + suggests(packageName, '', 0) + } + + Dependency enhances(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + enhances.add(dep) + dep + } + + Dependency enhances(String packageName) { + enhances(packageName, '', 0) + } + + Dependency preDepends(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + preDepends.add(dep) + dep + } + + Dependency preDepends(String packageName) { + preDepends(packageName, '', 0) + } + + Dependency breaks(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + breaks.add(dep) + dep + } + + Dependency breaks(String packageName) { + breaks(packageName, '', 0) + } + + Dependency replaces(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + replaces.add(dep) + dep + } + + Dependency replaces(String packageName) { + replaces(packageName, '', 0) + } + + Dependency provides(String packageName, String version, int flag) { + def dep = new Dependency(packageName, version, flag) + provides.add(dep) + dep + } + + Dependency provides(String packageName) { + provides(packageName, '', 0) + } + + List directories = [] + + Directory directory(String path) { + Directory directory = directory(path, -1) + directories << directory + directory + } + + Directory directory(String path, boolean addParents) { + Directory directory = new Directory(path: path, addParents: addParents) + directories << directory + directory + } + + Directory directory(String path, int permissions) { + Directory directory = new Directory(path: path, permissions: permissions) + directories << directory + directory + } + + Directory directory(String path, int permissions, boolean addParents) { + Directory directory = new Directory(path: path, permissions: permissions, addParents: addParents) + directories << directory + directory + } + + Directory directory(String path, int permissions, String user, String permissionGroup) { + Directory directory = new Directory(path: path, permissions: permissions, user: user, + permissionGroup: permissionGroup) + directories << directory + directory + } + + Directory directory(String path, int permissions, String user, String permissionGroup, boolean addParents) { + Directory directory = new Directory(path: path, permissions: permissions, user: user, + permissionGroup: permissionGroup, addParents: addParents) + directories << directory + directory + } + + private static IllegalStateException multipleFilesDefined(String fileName) { + new IllegalStateException("Cannot specify more than one $fileName File") + } + + private static IllegalStateException conflictingDefinitions(String type) { + new IllegalStateException("Cannot specify $type File and $type Commands") + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidator.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidator.groovy new file mode 100644 index 0000000..f06c09e --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidator.groovy @@ -0,0 +1,18 @@ +package org.xbib.gradle.plugin.rpm.validation + +class RpmPackageNameAttributeValidator implements SystemPackagingAttributeValidator { + + @Override + boolean validate(String packageName) { + matchesExpectedCharacters(packageName) + } + + private static boolean matchesExpectedCharacters(String packageName) { + packageName ==~ /[a-zA-Z0-9-._+]+/ + } + + @Override + String getErrorMessage(String attribute) { + "Invalid package name '$attribute' - a valid package name must only contain [a-zA-Z0-9-._+]" + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidator.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidator.groovy new file mode 100644 index 0000000..f756523 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidator.groovy @@ -0,0 +1,16 @@ +package org.xbib.gradle.plugin.rpm.validation + +import org.xbib.gradle.plugin.rpm.Rpm +import org.gradle.api.InvalidUserDataException + +class RpmTaskPropertiesValidator implements SystemPackagingTaskPropertiesValidator { + + private final SystemPackagingAttributeValidator packageNameValidator = new RpmPackageNameAttributeValidator() + + @Override + void validate(Rpm task) { + if (!packageNameValidator.validate(task.getPackageName())) { + throw new InvalidUserDataException(packageNameValidator.getErrorMessage(task.getPackageName())) + } + } +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingAttributeValidator.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingAttributeValidator.groovy new file mode 100644 index 0000000..e5d3909 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingAttributeValidator.groovy @@ -0,0 +1,8 @@ +package org.xbib.gradle.plugin.rpm.validation + +interface SystemPackagingAttributeValidator { + + boolean validate(String attribute) + + String getErrorMessage(String attribute) +} diff --git a/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingTaskPropertiesValidator.groovy b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingTaskPropertiesValidator.groovy new file mode 100644 index 0000000..0f3cea3 --- /dev/null +++ b/gradle-plugin-rpm/src/main/groovy/org/xbib/gradle/plugin/rpm/validation/SystemPackagingTaskPropertiesValidator.groovy @@ -0,0 +1,8 @@ +package org.xbib.gradle.plugin.rpm.validation + +import org.gradle.api.Task + +interface SystemPackagingTaskPropertiesValidator { + + void validate(T task) +} diff --git a/gradle-plugin-rpm/src/main/resources/META-INF/gradle-plugins/org.xbib.gradle.plugin.rpm.properties b/gradle-plugin-rpm/src/main/resources/META-INF/gradle-plugins/org.xbib.gradle.plugin.rpm.properties new file mode 100644 index 0000000..677797c --- /dev/null +++ b/gradle-plugin-rpm/src/main/resources/META-INF/gradle-plugins/org.xbib.gradle.plugin.rpm.properties @@ -0,0 +1 @@ +implementation-class=org.xbib.gradle.plugin.rpm.RpmPlugin \ No newline at end of file diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/AbstractProjectSpec.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/AbstractProjectSpec.groovy new file mode 100644 index 0000000..76eba9f --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/AbstractProjectSpec.groovy @@ -0,0 +1,52 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.rules.TestName +import spock.lang.Specification + +abstract class AbstractProjectSpec extends Specification { + + static final String CLEAN_PROJECT_DIR_SYS_PROP = 'cleanProjectDir' + + File ourProjectDir + + @Rule TestName testName = new TestName() + + String canonicalName + + Project project + + MultiProjectHelper helper + + void setup() { + ourProjectDir = new File("build/nebulatest/${this.class.canonicalName}/${testName.methodName.replaceAll(/\W+/, '-')}").absoluteFile + if (ourProjectDir.exists()) { + ourProjectDir.deleteDir() + } + ourProjectDir.mkdirs() + canonicalName = testName.getMethodName().replaceAll(' ', '-') + project = ProjectBuilder.builder().withName(canonicalName).withProjectDir(ourProjectDir).build() + helper = new MultiProjectHelper(project) + } + + void cleanup() { + if (deleteProjectDir()) { + ourProjectDir.deleteDir() + } + } + + boolean deleteProjectDir() { + String cleanProjectDirSystemProperty = System.getProperty(CLEAN_PROJECT_DIR_SYS_PROP) + cleanProjectDirSystemProperty ? cleanProjectDirSystemProperty.toBoolean() : true + } + + Project addSubproject(String subprojectName) { + helper.addSubproject(subprojectName) + } + + Project addSubprojectWithDirectory(String subprojectName) { + helper.addSubprojectWithDirectory(subprojectName) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BaseIntegrationSpec.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BaseIntegrationSpec.groovy new file mode 100644 index 0000000..0d7cecf --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BaseIntegrationSpec.groovy @@ -0,0 +1,120 @@ +package org.xbib.gradle.plugin.rpm + +import org.junit.Rule +import org.junit.rules.TestName +import spock.lang.Specification + +abstract class BaseIntegrationSpec extends Specification { + + @Rule + TestName testName = new TestName() + + File projectDir + + def setup() { + projectDir = new File("build/xbibtest/${this.class.canonicalName}/${testName.methodName.replaceAll(/\W+/, '-')}").absoluteFile + if (projectDir.exists()) { + projectDir.deleteDir() + } + projectDir.mkdirs() + } + + protected File directory(String path, File baseDir = getProjectDir()) { + new File(baseDir, path).with { + mkdirs() + it + } + } + + protected File file(String path, File baseDir = getProjectDir()) { + def splitted = path.split('/') + def directory = splitted.size() > 1 ? directory(splitted[0..-2].join('/'), baseDir) : baseDir + def file = new File(directory, splitted[-1]) + file.createNewFile() + file + } + + protected File createFile(String path, File baseDir = getProjectDir()) { + File file = file(path, baseDir) + if (!file.exists()) { + assert file.parentFile.mkdirs() || file.parentFile.exists() + file.createNewFile() + } + file + } + + protected static void checkForDeprecations(String output) { + def deprecations = output.readLines().findAll { + it.contains("has been deprecated and is scheduled to be removed in Gradle") + } + if (!System.getProperty("ignoreDeprecations") && !deprecations.isEmpty()) { + throw new IllegalArgumentException("Deprecation warnings were found (Set the ignoreDeprecations system property during the test to ignore):\n" + deprecations.collect { + " - $it" + }.join("\n")) + } + } + + protected void writeHelloWorld(String packageDotted, File baseDir = getProjectDir()) { + def path = 'src/main/java/' + packageDotted.replace('.', '/') + '/HelloWorld.java' + def javaFile = createFile(path, baseDir) + javaFile << """\ + package ${packageDotted}; + + public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello Integration Test"); + } + } + """.stripIndent() + } + + /** + * Creates a unit test for testing your plugin. + * @param failTest true if you want the test to fail, false if the test should pass + * @param baseDir the directory to begin creation from, defaults to projectDir + */ + protected void writeUnitTest(boolean failTest, File baseDir = getProjectDir()) { + writeTest('src/test/java/', 'nebula', failTest, baseDir) + } + + /** + * + * Creates a unit test for testing your plugin. + * @param srcDir the directory in the project where the source file should be created. + * @param packageDotted the package for the unit test class, written in dot notation (ex. - nebula.integration) + * @param failTest true if you want the test to fail, false if the test should pass + * @param baseDir the directory to begin creation from, defaults to projectDir + */ + protected void writeTest(String srcDir, String packageDotted, boolean failTest, File baseDir = getProjectDir()) { + def path = srcDir + packageDotted.replace('.', '/') + '/HelloWorldTest.java' + def javaFile = createFile(path, baseDir) + javaFile << """\ + package ${packageDotted}; + import org.junit.Test; + import static org.junit.Assert.assertFalse; + + public class HelloWorldTest { + @Test public void doesSomething() { + assertFalse( $failTest ); + } + } + """.stripIndent() + } + + /** + * Creates a properties file to included as project resource. + * @param srcDir the directory in the project where the source file should be created. + * @param fileName to be used for the file, sans extension. The .properties extension will be added to the name. + * @param baseDir the directory to begin creation from, defaults to projectDir + */ + protected void writeResource(String srcDir, String fileName, File baseDir = getProjectDir()) { + def path = "$srcDir/${fileName}.properties" + def resourceFile = createFile(path, baseDir) + resourceFile.text = "firstProperty=foo.bar" + } + + protected void addResource(String srcDir, String filename, String contents, File baseDir = getProjectDir()) { + def resourceFile = createFile("${srcDir}/${filename}", baseDir) + resourceFile.text = contents + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BuildLauncherBackedGradleHandle.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BuildLauncherBackedGradleHandle.groovy new file mode 100644 index 0000000..f798438 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/BuildLauncherBackedGradleHandle.groovy @@ -0,0 +1,96 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.tooling.BuildException +import org.gradle.tooling.BuildLauncher +import org.gradle.tooling.ProgressEvent +import org.gradle.tooling.ProgressListener + +class BuildLauncherBackedGradleHandle implements GradleHandle { + + final private ByteArrayOutputStream standardOutput = new ByteArrayOutputStream() + + final private ByteArrayOutputStream standardError = new ByteArrayOutputStream() + + final private BuildLauncher launcher + + final private boolean forkedProcess + + final private List tasksExecuted + + public static final String PROGRESS_TASK_PREFIX = "Execute :" + + private GradleHandleBuildListener buildListener + + BuildLauncherBackedGradleHandle(BuildLauncher launcher, boolean forkedProcess) { + this.forkedProcess = forkedProcess + launcher.setStandardOutput(standardOutput) + launcher.setStandardError(standardError) + tasksExecuted = new ArrayList() + launcher.addProgressListener(new ProgressListener() { + @Override + void statusChanged(ProgressEvent event) { + // These are free form strings, :-( + if (event.getDescription().startsWith(PROGRESS_TASK_PREFIX)) { // E.g. "Execute :echo" + String taskName = event.getDescription().substring(PROGRESS_TASK_PREFIX.length() - 1) + tasksExecuted.add(taskName) + } + } + }) + this.launcher = launcher + } + + @Override + void registerBuildListener(GradleHandleBuildListener buildListener) { + this.buildListener = buildListener + } + + @Override + boolean isForkedProcess() { + forkedProcess + } + + private String getStandardOutput() { + return standardOutput.toString() + } + + private String getStandardError() { + return standardError.toString() + } + + @Override + ExecutionResult run() { + Throwable failure = null + try { + buildListener?.buildStarted() + launcher.run() + } catch(BuildException e) { + failure = e.getCause() + } catch(Exception e) { + failure = e + } + finally { + buildListener?.buildFinished() + } + String stdout = getStandardOutput() + List tasks = new ArrayList() + for (String taskName: tasksExecuted) { + boolean upToDate = isTaskUpToDate(stdout, taskName) + boolean skipped = isTaskSkipped(stdout, taskName) + tasks.add(new MinimalExecutedTask(taskName, upToDate, skipped)) + } + boolean success = failure == null + new ToolingExecutionResult(success, stdout, getStandardError(), tasks, failure) + } + + private isTaskUpToDate(String stdout, String taskName) { + containsOutput(stdout, taskName, 'UP-TO-DATE') + } + + private isTaskSkipped(String stdout, String taskName) { + containsOutput(stdout, taskName, 'SKIPPED') + } + + private boolean containsOutput(String stdout, String taskName, String stateIdentifier) { + stdout.contains("$taskName $stateIdentifier".toString()) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathAddingInitScriptBuilder.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathAddingInitScriptBuilder.groovy new file mode 100644 index 0000000..8b63db3 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathAddingInitScriptBuilder.groovy @@ -0,0 +1,58 @@ +package org.xbib.gradle.plugin.rpm + +import com.google.common.base.Function +import com.google.common.base.Predicate +import com.google.common.collect.FluentIterable +import org.gradle.internal.ErroringAction +import org.gradle.internal.IoActions +import org.gradle.internal.classloader.ClasspathUtil +import org.gradle.internal.classpath.ClassPath +import org.gradle.util.TextUtil + +class ClasspathAddingInitScriptBuilder { + + private ClasspathAddingInitScriptBuilder() { + } + + static void build(File initScriptFile, final ClassLoader classLoader, Predicate classpathFilter) { + build(initScriptFile, getClasspathAsFiles(classLoader, classpathFilter)) + } + + static void build(File initScriptFile, final List classpath) { + IoActions.writeTextFile(initScriptFile, new ErroringAction() { + @Override + protected void doExecute(Writer writer) throws Exception { + writer.write("allprojects {\n") + writer.write(" buildscript {\n") + writer.write(" dependencies {\n") + for (File file : classpath) { + writer.write(String.format(" classpath files('%s')\n", TextUtil.escapeString(file.getAbsolutePath()))) + } + writer.write(" }\n") + writer.write(" }\n") + writer.write("}\n") + } + }) + } + + static List getClasspathAsFiles(ClassLoader classLoader, Predicate classpathFilter) { + List classpathUrls = getClasspathUrls(classLoader) + return FluentIterable.from(classpathUrls).filter(classpathFilter).transform(new Function() { + @Override + File apply(URL url) { + return new File(url.toURI()) + } + }).toList() + } + + private static List getClasspathUrls(ClassLoader classLoader) { + Object cp = ClasspathUtil.getClasspath(classLoader) + if (cp instanceof List) { + return (List) cp + } + if (cp instanceof ClassPath) { + return ((ClassPath) cp).asURLs + } + throw new IllegalStateException("Unable to extract classpath urls from type ${cp.class.canonicalName}") + } +} \ No newline at end of file diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathInjectingGradleHandleFactory.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathInjectingGradleHandleFactory.groovy new file mode 100644 index 0000000..4736b23 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ClasspathInjectingGradleHandleFactory.groovy @@ -0,0 +1,35 @@ +package org.xbib.gradle.plugin.rpm; + +import com.google.common.base.Predicate +import org.gradle.util.GFileUtils + +class ClasspathInjectingGradleHandleFactory implements GradleHandleFactory { + + private final ClassLoader classLoader + + private final GradleHandleFactory delegateFactory + + private Predicate classpathFilter + + ClasspathInjectingGradleHandleFactory(ClassLoader classLoader, GradleHandleFactory delegateFactory, + Predicate classpathFilter) { + this.classpathFilter = classpathFilter + this.classLoader = classLoader + this.delegateFactory = delegateFactory + } + + @Override + GradleHandle start(File projectDir, List arguments, List jvmArguments = []) { + File testKitDir = new File(projectDir, ".gradle-test-kit") + if (!testKitDir.exists()) { + GFileUtils.mkdirs(testKitDir) + } + File initScript = new File(testKitDir, "init.gradle"); + ClasspathAddingInitScriptBuilder.build(initScript, classLoader, classpathFilter) + List ammendedArguments = new ArrayList(arguments.size() + 2) + ammendedArguments.add("--init-script") + ammendedArguments.add(initScript.getAbsolutePath()) + ammendedArguments.addAll(arguments) + return delegateFactory.start(projectDir, ammendedArguments, jvmArguments) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/Coordinate.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/Coordinate.groovy new file mode 100644 index 0000000..21ec9f0 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/Coordinate.groovy @@ -0,0 +1,18 @@ +package org.xbib.gradle.plugin.rpm + +import groovy.transform.Immutable + +@Immutable +class Coordinate { + + String group + + String artifact + + String version + + @Override + String toString() { + "${group}:${artifact}:${version}" + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancementTest.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancementTest.groovy new file mode 100644 index 0000000..da40f2e --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/CopySpecEnhancementTest.groovy @@ -0,0 +1,107 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.internal.file.DefaultFileLookup +import org.gradle.api.internal.file.FileResolver +import org.gradle.api.internal.file.copy.DefaultCopySpec +import org.gradle.api.tasks.util.PatternSet +import org.gradle.api.tasks.util.internal.PatternSets +import org.gradle.internal.Factory +import org.gradle.internal.nativeintegration.filesystem.FileSystem +import org.gradle.internal.nativeintegration.services.NativeServices +import org.gradle.internal.reflect.DirectInstantiator +import org.gradle.internal.reflect.Instantiator +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNull + +class CopySpecEnhancementTest { + private final FileResolver fileResolver = [resolve: { it as File }, getPatternSetFactory: { + TestFiles.getPatternSetFactory() + }] as FileResolver + private final Instantiator instantiator = DirectInstantiator.INSTANCE + + def spec = new DefaultCopySpec(fileResolver, instantiator) + + @Test + public void addUser() { + assertNull(spec.metaClass.hasProperty('user')) + + CopySpecEnhancement.user(spec, 'USER') + + assertEquals('USER', spec.user) + } + + @Test + public void addAddParentDirs() { + CopySpecEnhancement.setAddParentDirs(spec, true) + + assertEquals(true, spec.addParentDirs) + } + + @Test + public void addCreateDirectoryEntry() { + use(CopySpecEnhancement) { + spec.createDirectoryEntry false + } + + assertEquals(false, spec.createDirectoryEntry) + + use(CopySpecEnhancement) { + spec.createDirectoryEntry true + } + + assertEquals(true, spec.createDirectoryEntry) + + use(CopySpecEnhancement) { + spec.setCreateDirectoryEntry(false) + } + + assertEquals(false, spec.createDirectoryEntry) + + use(CopySpecEnhancement) { + spec.setCreateDirectoryEntry(true) + } + + assertEquals(true, spec.createDirectoryEntry) + } +} + +// Copied from Gradle core as DefaultCopySpec can no longer have null arguments + +class TestFiles { + private static final FileSystem FILE_SYSTEM = NativeServicesTestFixture.getInstance().get(FileSystem.class); + private static final DefaultFileLookup FILE_LOOKUP = new DefaultFileLookup(FILE_SYSTEM, PatternSets.getNonCachingPatternSetFactory()); + + /** + * Returns a resolver with no base directory. + */ + static FileResolver resolver() { + return FILE_LOOKUP.getFileResolver() + } + + static Factory getPatternSetFactory() { + return resolver().getPatternSetFactory() + } +} + +class NativeServicesTestFixture { + static NativeServices nativeServices + static boolean initialized + + static void initialize() { + if (!initialized) { + File nativeDir = new File("build/native-libs") + NativeServices.initialize(nativeDir) + initialized = true + } + } + + static synchronized NativeServices getInstance() { + if (nativeServices == null) { + initialize() + nativeServices = NativeServices.getInstance() + } + nativeServices + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultExecutionResult.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultExecutionResult.groovy new file mode 100644 index 0000000..b0f5f9c --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultExecutionResult.groovy @@ -0,0 +1,90 @@ +package org.xbib.gradle.plugin.rpm; + +import org.gradle.api.GradleException + +abstract class DefaultExecutionResult implements ExecutionResult { + + private final Boolean success + + private final String standardOutput + + private final String standardError + + private final List executedTasks + + private final Throwable failure + + DefaultExecutionResult(Boolean success, String standardOutput, String standardError, + List executedTasks, Throwable failure) { + this.success = success + this.standardOutput = standardOutput + this.standardError = standardError + this.executedTasks = executedTasks + this.failure = failure + } + + @Override + Boolean getSuccess() { + success + } + + @Override + String getStandardOutput() { + standardOutput + } + + @Override + String getStandardError() { + standardError + } + + @Override + boolean wasExecuted(String taskPath) { + executedTasks.any { ExecutedTask task -> + taskPath = normalizeTaskPath(taskPath) + def match = task.path == taskPath + return match + } + } + + @Override + boolean wasUpToDate(String taskPath) { + getExecutedTaskByPath(taskPath).upToDate + } + + @Override + boolean wasSkipped(String taskPath) { + getExecutedTaskByPath(taskPath).skipped + } + + String normalizeTaskPath(String taskPath) { + taskPath.startsWith(':') ? taskPath : ":$taskPath" + } + + private ExecutedTask getExecutedTaskByPath(String taskPath) { + taskPath = normalizeTaskPath(taskPath) + def task = executedTasks.find { ExecutedTask task -> + task.path == taskPath + } + if (task == null) { + throw new RuntimeException("Task with path $taskPath was not found") + } + task + } + + @Override + Throwable getFailure() { + failure + } + + @Override + ExecutionResult rethrowFailure() { + if (failure instanceof GradleException) { + throw (GradleException) failure + } + if (failure != null) { + throw new GradleException("Build aborted because of an internal error.", failure) + } + this + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultGradleRunner.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultGradleRunner.groovy new file mode 100644 index 0000000..c9dda29 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DefaultGradleRunner.groovy @@ -0,0 +1,25 @@ +package org.xbib.gradle.plugin.rpm + +class DefaultGradleRunner implements GradleRunner { + + private final GradleHandleFactory handleFactory + + DefaultGradleRunner(GradleHandleFactory handleFactory) { + this.handleFactory = handleFactory + } + + @Override + ExecutionResult run(File projectDir, List arguments, List jvmArguments = [], + List preExecutionActions = []) { + handle(projectDir, arguments, jvmArguments, preExecutionActions).run() + } + + @Override + GradleHandle handle(File projectDir, List arguments, List jvmArguments = [], + List preExecutionActions = []) { + preExecutionActions?.each { + it.execute(projectDir, arguments, jvmArguments) + } + handleFactory.start(projectDir, arguments, jvmArguments) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraph.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraph.groovy new file mode 100644 index 0000000..86c6f81 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraph.groovy @@ -0,0 +1,37 @@ +package org.xbib.gradle.plugin.rpm + +class DependencyGraph { + + Collection nodes = [] + + DependencyGraph(List graph) { + graph.each { nodes << parseNode(it) } + } + + DependencyGraph(String... graph) { + this(graph as List) + } + + DependencyGraph(Map tuple) { + nodes = tuple.nodes + } + + private DependencyGraphNode parseNode(String s) { + // Don't use tokenize, it'll make each character a possible delimeter, e.g. \t\n would tokenize on both + // \t OR \n, not the combination of \t\n. + def parts = s.split('->') + def (group, artifact, version) = parts[0].trim().tokenize(':') + def coordinate = new Coordinate(group: group, artifact: artifact, version: version) + def dependencies = (parts.size() > 1) ? parseDependencies(parts[1]) : [] + new DependencyGraphNode(coordinate: coordinate, dependencies: dependencies) + } + + private List parseDependencies(String s) { + List dependencies = [] + s.tokenize('|').each { String dependency -> + def (group, artifact, version) = dependency.trim().tokenize(':') + dependencies << new Coordinate(group: group, artifact: artifact, version: version) + } + dependencies + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraphNode.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraphNode.groovy new file mode 100644 index 0000000..baa12fb --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/DependencyGraphNode.groovy @@ -0,0 +1,16 @@ +package org.xbib.gradle.plugin.rpm + +import groovy.transform.Immutable + +@Immutable +class DependencyGraphNode { + + @Delegate Coordinate coordinate + + List dependencies = [] + + @Override + String toString() { + "${group}:${artifact}:${version}" + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutedTask.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutedTask.groovy new file mode 100644 index 0000000..9ef5c89 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutedTask.groovy @@ -0,0 +1,10 @@ +package org.xbib.gradle.plugin.rpm + +interface ExecutedTask { + + String getPath() + + boolean isUpToDate() + + boolean isSkipped() +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutionResult.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutionResult.groovy new file mode 100644 index 0000000..f28b893 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ExecutionResult.groovy @@ -0,0 +1,20 @@ +package org.xbib.gradle.plugin.rpm; + +interface ExecutionResult { + + Boolean getSuccess() + + String getStandardOutput() + + String getStandardError() + + boolean wasExecuted(String taskPath) + + boolean wasUpToDate(String taskPath) + + boolean wasSkipped(String taskPath) + + Throwable getFailure() + + ExecutionResult rethrowFailure() +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleDependencyGenerator.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleDependencyGenerator.groovy new file mode 100644 index 0000000..322d865 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleDependencyGenerator.groovy @@ -0,0 +1,151 @@ +package org.xbib.gradle.plugin.rpm + +class GradleDependencyGenerator { + + static final String STANDARD_SUBPROJECT_BLOCK = '''\ + subprojects { + apply plugin: 'maven-publish' + apply plugin: 'ivy-publish' + apply plugin: 'java' + + publishing { + repositories { + maven { + url "../mavenrepo" + } + ivy { + url "../ivyrepo" + layout('pattern') { + ivy '[organisation]/[module]/[revision]/[module]-[revision]-ivy.[ext]' + artifact '[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]' + m2compatible = true + } + } + } + publications { + maven(MavenPublication) { + artifactId artifactName + + from components.java + } + ivy(IvyPublication) { + module artifactName + + from components.java + } + } + } + } + '''.stripIndent() + static final String BUILD_GRADLE = 'build.gradle' + + private boolean generated = false + + DependencyGraph graph + File gradleRoot + File ivyRepoDir + File mavenRepoDir + + GradleDependencyGenerator(DependencyGraph graph, String directory = 'build/testrepogen') { + this.graph = graph + this.gradleRoot = new File(directory) + this.ivyRepoDir = new File(directory, 'ivyrepo') + this.mavenRepoDir = new File(directory, 'mavenrepo') + generateGradleFiles() + } + + File generateTestMavenRepo() { + runTasks('publishMavenPublicationToMavenRepository') + + mavenRepoDir + } + + String getMavenRepoDirPath() { + mavenRepoDir.absolutePath + } + + String getMavenRepoUrl() { + mavenRepoDir.toURI().toURL() + } + + String getMavenRepositoryBlock() { + """\ + maven { url '${getMavenRepoUrl()}' } + """.stripIndent() + } + + File generateTestIvyRepo() { + runTasks('publishIvyPublicationToIvyRepository') + + ivyRepoDir + } + + String getIvyRepoDirPath() { + ivyRepoDir.absolutePath + } + + String getIvyRepoUrl() { + ivyRepoDir.toURI().toURL() + } + + String getIvyRepositoryBlock() { + """\ + ivy { + url '${getIvyRepoUrl()}' + layout('pattern') { + ivy '[organisation]/[module]/[revision]/[module]-[revision]-ivy.[ext]' + artifact '[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]' + m2compatible = true + } + } + """.stripIndent() + } + + private void generateGradleFiles() { + if (generated) { + return + } else { + generated = true + } + + gradleRoot.mkdirs() + def rootBuildGradle = new File(gradleRoot, BUILD_GRADLE) + rootBuildGradle.text = STANDARD_SUBPROJECT_BLOCK + def includes = [] + graph.nodes.each { DependencyGraphNode n -> + String subName = "${n.group}.${n.artifact}_${n.version.replaceAll(/\./, '_')}" + includes << subName + def subfolder = new File(gradleRoot, subName) + subfolder.mkdir() + def subBuildGradle = new File(subfolder, BUILD_GRADLE) + subBuildGradle.text = generateSubBuildGradle(n) + } + def settingsGradle = new File(gradleRoot, 'settings.gradle') + settingsGradle.text = 'include ' + includes.collect { "'${it}'"}.join(', ') + } + + private String generateSubBuildGradle(DependencyGraphNode node) { + + StringWriter block = new StringWriter() + if (node.dependencies) { + block.withPrintWriter { writer -> + writer.println 'dependencies {' + node.dependencies.each { writer.println " compile '${it}'" } + writer.println '}' + } + } + + """\ + group = '${node.group}' + version = '${node.version}' + ext { + artifactName = '${node.artifact}' + } + """.stripIndent() + block.toString() + } + + private void runTasks(String tasks) { + def runner = GradleRunnerFactory.createTooling() + runner.run(gradleRoot, tasks.tokenize()).rethrowFailure() + } +} \ No newline at end of file diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandle.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandle.groovy new file mode 100644 index 0000000..b4bbd2d --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandle.groovy @@ -0,0 +1,10 @@ +package org.xbib.gradle.plugin.rpm + +interface GradleHandle { + + ExecutionResult run() + + void registerBuildListener(GradleHandleBuildListener buildListener) + + boolean isForkedProcess() +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleBuildListener.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleBuildListener.groovy new file mode 100644 index 0000000..2e7ba93 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleBuildListener.groovy @@ -0,0 +1,8 @@ +package org.xbib.gradle.plugin.rpm + +interface GradleHandleBuildListener { + + void buildStarted() + + void buildFinished() +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleFactory.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleFactory.groovy new file mode 100644 index 0000000..57296d0 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleHandleFactory.groovy @@ -0,0 +1,8 @@ +package org.xbib.gradle.plugin.rpm + +interface GradleHandleFactory { + + GradleHandle start(File dir, List arguments) + + GradleHandle start(File dir, List arguments, List jvmArguments) +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunner.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunner.groovy new file mode 100644 index 0000000..4c22786 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunner.groovy @@ -0,0 +1,78 @@ +package org.xbib.gradle.plugin.rpm + +import com.google.common.base.Predicate +import com.google.common.base.Predicates +import com.google.common.base.StandardSystemProperty + +interface GradleRunner { + // These predicates are here, instead of on GradleRunnerFactory due to a Groovy static compiler bug + // https://issues.apache.org/jira/browse/GROOVY-7159 + + static final Predicate CLASSPATH_GRADLE_CACHE = new Predicate() { + @Override + boolean apply(URL url) { + return url.path.contains('/caches/modules-') + } + } + + static final Predicate CLASSPATH_PROJECT_DIR = new Predicate() { + @Override + boolean apply(URL url) { + File userDir = new File(StandardSystemProperty.USER_DIR.value()) + return url.path.startsWith(userDir.toURI().toURL().path) + } + } + + static final Predicate CLASSPATH_PROJECT_DEPENDENCIES = new Predicate() { + @Override + boolean apply(URL url) { + return url.path.contains('/build/classes') || + url.path.contains('/build/resources') || + url.path.contains('/build/libs') || + url.path.contains('/out/') + } + } + + /** + * Attempts to provide a classpath that approximates the 'normal' Gradle runtime classpath. Use {@link #CLASSPATH_ALL} + * to default to pre-2.2.2 behaviour. + */ + static final Predicate CLASSPATH_DEFAULT = + Predicates.or(CLASSPATH_PROJECT_DIR, CLASSPATH_GRADLE_CACHE, CLASSPATH_PROJECT_DEPENDENCIES) + + /** + * Accept all URLs. Provides pre-2.2.2 behaviour. + */ + static final Predicate CLASSPATH_ALL = new Predicate() { + @Override + boolean apply(URL url) { + return true + } + } + + /** + * Create handle and run build + * @param directory + * @param args + * @return results from execution + */ + ExecutionResult run(File directory, List args) + + ExecutionResult run(File directory, List args, List jvmArgs) + + ExecutionResult run(File directory, List args, List jvmArgs, + List preExecutionActions) + + /** + * Handle on instance of Gradle that can be run. + * @param directory + * @param args + * @return handle + */ + GradleHandle handle(File directory, List args) + + GradleHandle handle(File directory, List args, List jvmArgs) + + GradleHandle handle(File directory, List args, List jvmArgs, + List preExecutionActions) +} \ No newline at end of file diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunnerFactory.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunnerFactory.groovy new file mode 100644 index 0000000..ff83cd5 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/GradleRunnerFactory.groovy @@ -0,0 +1,26 @@ +package org.xbib.gradle.plugin.rpm + +import com.google.common.base.Predicate + +class GradleRunnerFactory { + + static GradleRunner createTooling(boolean fork = false, String version = null, + Integer daemonMaxIdleTimeInSeconds = null, + Predicate classpathFilter = null) { + GradleHandleFactory toolingApiHandleFactory = + new ToolingApiGradleHandleFactory(fork, version, daemonMaxIdleTimeInSeconds) + return create(toolingApiHandleFactory, classpathFilter ?: GradleRunner.CLASSPATH_DEFAULT) + } + + static GradleRunner create(GradleHandleFactory handleFactory, Predicate classpathFilter = null) { + ClassLoader sourceClassLoader = GradleRunnerFactory.class.getClassLoader() + create(handleFactory, sourceClassLoader, classpathFilter ?: GradleRunner.CLASSPATH_DEFAULT) + } + + static GradleRunner create(GradleHandleFactory handleFactory, ClassLoader sourceClassLoader, + Predicate classpathFilter) { + GradleHandleFactory classpathInjectingHandleFactory = + new ClasspathInjectingGradleHandleFactory(sourceClassLoader, handleFactory, classpathFilter) + return new DefaultGradleRunner(classpathInjectingHandleFactory) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/IntegrationSpec.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/IntegrationSpec.groovy new file mode 100644 index 0000000..518b712 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/IntegrationSpec.groovy @@ -0,0 +1,185 @@ +package org.xbib.gradle.plugin.rpm + +import com.google.common.base.Predicate +import org.gradle.api.logging.LogLevel + +abstract class IntegrationSpec extends BaseIntegrationSpec { + + private static final String DEFAULT_REMOTE_DEBUG_JVM_ARGUMENTS = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005" + + private static final Integer DEFAULT_DAEMON_MAX_IDLE_TIME_IN_SECONDS_IN_MEMORY_SAFE_MODE = 15; + + private ExecutionResult result + + protected String gradleVersion + + protected LogLevel logLevel = LogLevel.INFO + + protected String moduleName + + protected File settingsFile + + protected File buildFile + + protected boolean fork = false + + protected boolean remoteDebug = false + + protected List jvmArguments = [] + + protected Predicate classpathFilter + + protected List initScripts = [] + + protected List preExecutionActions = [] + + //Shutdown Gradle daemon after a few seconds to release memory. Useful for testing with multiple Gradle versions on shared CI server + protected boolean memorySafeMode = false + + protected Integer daemonMaxIdleTimeInSecondsInMemorySafeMode = DEFAULT_DAEMON_MAX_IDLE_TIME_IN_SECONDS_IN_MEMORY_SAFE_MODE + + private String findModuleName() { + getProjectDir().getName().replaceAll(/_\d+/, '') + } + + def setup() { + moduleName = findModuleName() + if (!settingsFile) { + settingsFile = new File(getProjectDir(), 'settings.gradle') + settingsFile.text = "rootProject.name='${moduleName}'\n" + } + if (!buildFile) { + buildFile = new File(getProjectDir(), 'build.gradle') + } + buildFile << "// Running test for ${moduleName}\n" + } + + protected GradleHandle launcher(String... args) { + List arguments = calculateArguments(args) + List jvmArguments = calculateJvmArguments() + Integer daemonMaxIdleTimeInSeconds = calculateMaxIdleDaemonTimeoutInSeconds() + GradleRunner runner = GradleRunnerFactory.createTooling(fork, gradleVersion, daemonMaxIdleTimeInSeconds, classpathFilter) + runner.handle(getProjectDir(), arguments, jvmArguments, preExecutionActions) + } + + private List calculateArguments(String... args) { + List arguments = [] + // Gradle will use these files name from the PWD, instead of the project directory. It's easier to just leave + // them out and let the default find them, since we're not changing their default names. + //arguments += '--build-file' + //arguments += (buildFile.canonicalPath - projectDir.canonicalPath).substring(1) + //arguments += '--settings-file' + //arguments += (settingsFile.canonicalPath - projectDir.canonicalPath).substring(1) + //arguments += '--no-daemon' + + switch (getLogLevel()) { + case LogLevel.INFO: + arguments += '--info' + break + case LogLevel.DEBUG: + arguments += '--debug' + break + } + arguments += '--stacktrace' + arguments.addAll(args) + arguments.addAll(initScripts.collect { file -> '-I' + file.absolutePath }) + arguments + } + + private List calculateJvmArguments() { + return jvmArguments + (remoteDebug ? [DEFAULT_REMOTE_DEBUG_JVM_ARGUMENTS] : [] as List) as List + } + + private Integer calculateMaxIdleDaemonTimeoutInSeconds() { + return memorySafeMode ? daemonMaxIdleTimeInSecondsInMemorySafeMode : null + } + + protected void addInitScript(File initFile) { + initScripts.add(initFile) + } + + protected void addPreExecute(PreExecutionAction preExecutionAction) { + preExecutionActions.add(preExecutionAction) + } + + /** + * Override to alter its value + * @return + */ + protected LogLevel getLogLevel() { + return logLevel + } + + /*protected void copyResources(String srcDir, String destination) { + ClassLoader classLoader = getClass().getClassLoader(); + URL resource = classLoader.getResource(srcDir); + if (resource == null) { + throw new RuntimeException("Could not find classpath resource: $srcDir") + } + File destinationFile = file(destination) + File resourceFile = new File(resource.toURI()) + if (resourceFile.file) { + FileUtils.copyFile(resourceFile, destinationFile) + } else { + FileUtils.copyDirectory(resourceFile, destinationFile) + } + }*/ + + protected String applyPlugin(Class pluginClass) { + "apply plugin: $pluginClass.name" + } + + /* Checks */ + protected boolean fileExists(String path) { + new File(projectDir, path).exists() + } + + @Deprecated + protected boolean wasExecuted(String taskPath) { + result.wasExecuted(taskPath) + } + + @Deprecated + protected boolean wasUpToDate(String taskPath) { + result.wasUpToDate(taskPath) + } + + @Deprecated + protected String getStandardError() { + result.standardError + } + + @Deprecated + protected String getStandardOutput() { + result.standardOutput + } + + protected ExecutionResult runTasksSuccessfully(String... tasks) { + ExecutionResult result = runTasks(tasks) + if (result.failure) { + result.rethrowFailure() + } + result + } + + protected ExecutionResult runTasksWithFailure(String... tasks) { + ExecutionResult result = runTasks(tasks) + assert result.failure + result + } + + protected ExecutionResult runTasks(String... tasks) { + ExecutionResult result = launcher(tasks).run() + this.result = result + return checkForDeprecations(result) + } + + protected ExecutionResult checkForDeprecations(ExecutionResult result) { + checkForDeprecations(result.standardOutput) + return result + } + + File getSettingsFile() { + return settingsFile + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MinimalExecutedTask.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MinimalExecutedTask.groovy new file mode 100644 index 0000000..15cd8b1 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MinimalExecutedTask.groovy @@ -0,0 +1,20 @@ +package org.xbib.gradle.plugin.rpm + +class MinimalExecutedTask implements ExecutedTask { + + String path + + boolean upToDate + + boolean skipped + + MinimalExecutedTask(String path, boolean upToDate, boolean skipped) { + this.path = path + this.upToDate = upToDate + this.skipped = skipped + } + + String toString() { + "executed $path" + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectHelper.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectHelper.groovy new file mode 100644 index 0000000..da9361f --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectHelper.groovy @@ -0,0 +1,43 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder + +class MultiProjectHelper { + + Project parent + + MultiProjectHelper(Project parent) { + this.parent = parent + } + + Map create(Collection projectNames) { + Map info = [:] + projectNames.each { + def subproject = ProjectBuilder.builder().withName(it).withParent(parent).build() + info[it] = new MultiProjectInfo(name: it, project: subproject, parent: parent) + } + info + } + + Map createWithDirectories(Collection projectNames) { + Map info = [:] + projectNames.each { + def subDirectory = new File(parent.projectDir, it) + subDirectory.mkdirs() + def subproject = ProjectBuilder.builder().withName(it).withProjectDir(subDirectory).withParent(parent).build() + info[it] = new MultiProjectInfo(name: it, project: subproject, parent: parent, directory: subDirectory) + } + info + } + + Project addSubproject(String name) { + ProjectBuilder.builder().withName(name).withParent(parent).build() + } + + Project addSubprojectWithDirectory(String name) { + def dir = new File(parent.projectDir, name) + dir.mkdirs() + ProjectBuilder.builder().withName(name).withProjectDir(dir).withParent(parent).build() + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectInfo.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectInfo.groovy new file mode 100644 index 0000000..b580eba --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/MultiProjectInfo.groovy @@ -0,0 +1,14 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.Project + +class MultiProjectInfo { + + String name + + Project parent + + Project project + + File directory +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/PreExecutionAction.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/PreExecutionAction.groovy new file mode 100644 index 0000000..167485d --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/PreExecutionAction.groovy @@ -0,0 +1,6 @@ +package org.xbib.gradle.plugin.rpm + +interface PreExecutionAction { + + void execute(File projectDir, List arguments, List jvmArguments) +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ProjectSpec.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ProjectSpec.groovy new file mode 100644 index 0000000..1112058 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ProjectSpec.groovy @@ -0,0 +1,8 @@ +package org.xbib.gradle.plugin.rpm + +class ProjectSpec extends AbstractProjectSpec { + + File getProjectDir() { + ourProjectDir + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmCopySpecVisitorTest.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmCopySpecVisitorTest.groovy new file mode 100755 index 0000000..73fa67a --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmCopySpecVisitorTest.groovy @@ -0,0 +1,50 @@ +package org.xbib.gradle.plugin.rpm + +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +class RpmCopySpecVisitorTest extends ProjectSpec { + + RpmCopyAction visitor + + @Before + void setup() { + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + packageName = 'can-execute-rpm-task-with-valid-version' + } + visitor = new RpmCopyAction(rpmTask) + } + + @Test + void withoutUtils() { + visitor.includeStandardDefines = false + File script = resourceFile("script.sh") + Object result = visitor.scriptWithUtils([], [script]) + assertTrue result instanceof String + assertEquals( + "#!/bin/bash\n" + + "hello\n", result) + } + + @Test + void withUtils() { + visitor.includeStandardDefines = false + Object result = visitor.scriptWithUtils([resourceFile("utils.sh")], [resourceFile("script.sh")]) + assertTrue result instanceof String + assertEquals( + "#!/bin/bash\n" + + "function hello() {\n" + + " echo 'Hello, world.'\n" + + "}\n" + + "hello\n", result) + } + + File resourceFile(String name) { + new File(getClass().getResource(name).getPath()) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginIntegrationTest.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginIntegrationTest.groovy new file mode 100644 index 0000000..7f2cc4e --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginIntegrationTest.groovy @@ -0,0 +1,26 @@ +package org.xbib.gradle.plugin.rpm + +class RpmPluginIntegrationTest extends IntegrationSpec { + + def "rpm task is marked up-to-date when setting arch or os property"() { + + given: + buildFile << ''' +apply plugin: 'org.xbib.gradle.plugin.rpm' + +task buildRpm(type: Rpm) { + packageName = 'rpmIsUpToDate' + arch = NOARCH + os = LINUX +} +''' + when: + runTasksSuccessfully('buildRpm') + + and: + def result = runTasksSuccessfully('buildRpm') + + then: + result.wasUpToDate(':buildRpm') + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginTest.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginTest.groovy new file mode 100755 index 0000000..6ef5914 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmPluginTest.groovy @@ -0,0 +1,1316 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.BasePlugin +import org.gradle.testfixtures.ProjectBuilder +import org.xbib.rpm.lead.Architecture +import org.xbib.rpm.format.Flags +import org.xbib.rpm.header.Header +import org.xbib.rpm.lead.Os +import org.xbib.rpm.lead.PackageType +import org.xbib.rpm.signature.SignatureTag +import spock.lang.Unroll + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +import static org.xbib.rpm.format.Flags.* +import static org.xbib.rpm.header.HeaderTag.* +import static org.xbib.rpm.payload.CpioHeader.* + +/** + * + */ +class RpmPluginTest extends ProjectSpec { + def 'files'() { + Project project = ProjectBuilder.builder().build() + + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + String fruit = 'apple' + new File(srcDir, fruit).withWriter { out -> + out.write(fruit) + } + + File noParentsDir = new File(projectDir, 'noParentsDir') + noParentsDir.mkdirs() + new File(noParentsDir, 'alone').withWriter { out -> + out.write('alone') + } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + type = PackageType.BINARY + arch = Architecture.I386.name() + os = Os.LINUX + permissionGroup = 'Development/Libraries' + summary = 'Bleah blarg' + packageDescription = 'Not a very interesting library.' + license = 'Free' + distribution = 'SuperSystem' + vendor = 'Super Associates, LLC' + url = 'http://www.example.com/' + + requires('blarg', '1.0', Flags.GREATER | Flags.EQUAL) + requires('blech') + + into '/opt/bleah' + from(srcDir) + + from(srcDir.toString() + '/main/groovy') { + createDirectoryEntry true + fileType = ['config', 'noreplace'] + } + + from(noParentsDir) { + addParentDirs false + into '/a/path/not/to/create' + } + + link('/opt/bleah/banana', '/opt/bleah/apple') + }) + + when: + project.tasks.buildRpm.execute() + + then: + def result = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + 'bleah' == RpmReader.getHeaderEntryString(result, NAME) + '1.0' == RpmReader.getHeaderEntryString(result, VERSION) + '1' == RpmReader.getHeaderEntryString(result, RELEASE) + 0 == RpmReader.getHeaderEntry(result, EPOCH).values[0] + 'i386' == RpmReader.getHeaderEntryString(result, ARCH) + 'linux' == RpmReader.getHeaderEntryString(result, OS) + ['SuperSystem'] == RpmReader.getHeaderEntry(result, DISTRIBUTION).values + result.files*.name.every { fileName -> + ['./a/path/not/to/create/alone', './opt/bleah', + './opt/bleah/apple', './opt/bleah/banana'].any { path -> + path.startsWith(fileName) + } + } + result.files*.name.every { fileName -> + ['./a/path/not/to/create'].every { path -> + ! path.startsWith(fileName) + } + } + } + + def 'obsoletesAndConflicts'() { + + Project project = ProjectBuilder.builder().build() + File buildDir = project.buildDir + + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + String fruit = 'apple' + new File(srcDir, fruit).withWriter { out -> + out.write(fruit) + } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/ObsoletesConflictsTest') + destinationDir.mkdirs() + + packageName = 'testing' + version = '1.2' + release = '3' + type = BINARY + arch = I386 + os = LINUX + license = 'Free' + distribution = 'SuperSystem' + vendor = 'Super Associates, LLC' + url = 'http://www.example.com/' + + obsoletes('blarg', '1.0', GREATER | EQUAL) + conflicts('blech') + conflicts('packageA', '1.0', LESS) + obsoletes('packageB', '2.2', GREATER) + + from(srcDir) + into '/opt/bleah' + }) + + when: + project.tasks.buildRpm.execute() + + then: + def result = RpmReader.read(project.file('build/tmp/ObsoletesConflictsTest/testing-1.2-3.i386.rpm').toPath()) + def obsoletes = RpmReader.getHeaderEntry(result, OBSOLETENAME) + def obsoleteVersions = RpmReader.getHeaderEntry(result, OBSOLETEVERSION) + def obsoleteComparisons = RpmReader.getHeaderEntry(result, OBSOLETEFLAGS) + def conflicts = RpmReader.getHeaderEntry(result, CONFLICTNAME) + def conflictVersions = RpmReader.getHeaderEntry(result, CONFLICTVERSION) + def conflictComparisons = RpmReader.getHeaderEntry(result, CONFLICTFLAGS) + def distribution = RpmReader.getHeaderEntry(result, DISTRIBUTION) + + 'blarg' == obsoletes.values[0] + '1.0' == obsoleteVersions.values[0] + (GREATER | EQUAL) == obsoleteComparisons.values[0] + + 'blech' == conflicts.values[0] + '' == conflictVersions.values[0] + 0 == conflictComparisons.values[0] + + 'packageA' == conflicts.values[1] + '1.0' ==conflictVersions.values[1] + LESS == conflictComparisons.values[1] + + 'packageB' == obsoletes.values[1] + '2.2' == obsoleteVersions.values[1] + GREATER == obsoleteComparisons.values[1] + + ['SuperSystem'] == distribution.values + } + + + def 'projectNameDefault'() { + + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + String fruit = 'apple' + new File(srcDir, fruit).withWriter { out -> + out.write(fruit) + } + + when: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', {}) + + then: + 'projectNameDefault' == project.buildRpm.packageName + + when: + project.tasks.buildRpm.execute() + + then: + noExceptionThrown() + } + + def 'file handle closed'() { + + when: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + project.task([type: Rpm], 'buildRpm', {}) + project.tasks.buildRpm.execute() + project.tasks.clean.execute() + then: + noExceptionThrown() + } + + def 'category_on_spec'() { + project.version = '1.0.0' + + File bananaFile = new File(projectDir, 'test/banana') + bananaFile.parentFile.mkdirs() + bananaFile.withWriter { out -> out.write('banana') } + + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + String fruit = 'apple' + File appleFile = new File(srcDir, fruit) + appleFile.withWriter { out -> out.write(fruit) } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = (Rpm) project.task([type: Rpm], 'buildRpm', { + addParentDirs = true + from(bananaFile.getParentFile()) { + into '/usr/share/myproduct/etc' + createDirectoryEntry false + } + from(appleFile.getParentFile()) { + into '/usr/local/myproduct/bin' + createDirectoryEntry true + } + }) + + when: + rpmTask.execute() + + then: + def files = RpmReader.read(rpmTask.getArchivePath().toPath()).files + ['./usr/local/myproduct', './usr/local/myproduct/bin', './usr/local/myproduct/bin/apple', './usr/share/myproduct', './usr/share/myproduct/etc', './usr/share/myproduct/etc/banana'] == files*.name + [ DIR, DIR, FILE, DIR, DIR, FILE] == files*.type + + } + + def 'filter_expression'() { + + project.version = '1.0.0' + File appleFile = new File(projectDir, 'src/apple') + appleFile.parentFile.mkdirs() + appleFile.withWriter { out -> out.write('{{BASE}}/apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = (Rpm) project.task([type: Rpm], 'buildRpm') { + from(appleFile.getParentFile()) { + into '/usr/local/myproduct/bin' + filter({ line -> + return line.replaceAll(/\{\{BASE\}\}/, '/usr/local/myproduct') + }) + } + } + + when: + rpmTask.execute() + + then: + def res = RpmReader.read(rpmTask.getArchivePath().toPath()) + def scannerApple = res.files.find { it.name =='./usr/local/myproduct/bin/apple'} + scannerApple.asString() == '/usr/local/myproduct/apple' + } + + def 'usesArchivesBaseName'() { + + // archivesBaseName is an artifact of the BasePlugin, and won't exist until it's applied. + project.apply plugin: BasePlugin + project.archivesBaseName = 'foo' + + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + String fruit = 'apple' + new File(srcDir, fruit).withWriter { out -> + out.write(fruit) + } + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + when: + project.task([type: Rpm], 'buildRpm', {}) + + then: + 'foo' == project.buildRpm.packageName + + when: + project.tasks.buildRpm.execute() + + then: + noExceptionThrown() + } + + def 'verifyValuesCanComeFromExtension'() { + + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + String fruit = 'apple' + new File(srcDir, fruit).withWriter { out -> + out.write(fruit) + } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + def parentExten = project.extensions.create('rpmParent', ProjectPackagingExtension, project) + + Rpm rpmTask = project.task([type: Rpm], 'buildRpm') + rpmTask.permissionGroup = 'GROUP' + rpmTask.requires('openjdk') + rpmTask.link('/dev/null', '/dev/random') + + when: + parentExten.user = 'USER' + parentExten.permissionGroup = 'GROUP2' + parentExten.requires('java') + parentExten.link('/tmp', '/var/tmp') + + project.description = 'DESCRIPTION' + + then: + 'USER' == rpmTask.user // From Extension + 'GROUP' == rpmTask.permissionGroup // From task, overriding extension + 'DESCRIPTION' == rpmTask.packageDescription // From Project, even though extension could have a value + 2 == rpmTask.getAllLinks().size() + 2 == rpmTask.getAllDependencies().size() + } + + def 'verifyCopySpecCanComeFromExtension'() { + setup: + + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + String fruit = 'apple' + new File(srcDir, fruit).withWriter { out -> + out.write(fruit) + } + + File etcDir = new File(projectDir, 'etc') + etcDir.mkdirs() + new File(etcDir, 'banana.conf').text = 'banana=true' + + // Simulate SystemPackagingBasePlugin + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + ProjectPackagingExtension parentExten = project.extensions.create('rpmParent', ProjectPackagingExtension, project) + + // Configure + Rpm rpmTask = (Rpm) project.task([type: Rpm, name:'buildRpm']) { + release 3 + } + project.version = '1.0' + + rpmTask.from(srcDir) { + into('/usr/local/src') + } + parentExten.from(etcDir) { + createDirectoryEntry true + into('/conf/defaults') + } + + // Execute + when: + rpmTask.execute() + + then: + // Evaluate response + rpmTask.getArchivePath().exists() + def res = RpmReader.read(rpmTask.getArchivePath().toPath()) + // Parent will come first + ['./conf', './conf/defaults', './conf/defaults/banana.conf', './usr/local/src', './usr/local/src/apple'] == res.files*.name + [DIR, DIR, FILE, DIR, FILE] == res.files*.type + } + + def 'differentUsersBetweenCopySpecs'() { + + def srcDir = [new File(projectDir, 'src1'), + new File(projectDir, 'src2'), + new File(projectDir, 'src3')] + def fruits = ['apple', 'banana', 'cherry'] + srcDir.eachWithIndex { file, idx -> + file.mkdirs() + String fruit = fruits[idx] + new File(file, fruit).withWriter { out -> + out.write(fruit) + } + } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'userTest' + version = '2.0' + release = '2' + type = BINARY + arch = I386 + os = LINUX + + into '/tiny' + user = 'default' + + from(srcDir[0]) { + user 'user1' + // user = 'user1' // Won't work, since setter via Categories won't pass hasProperty + } + + from(srcDir[1]) { + // should be default user + } + + from(srcDir[2]) { + user 'user2' + } + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/userTest-2.0-2.i386.rpm').toPath()) + [DIR, FILE, FILE, FILE] == res.files*.type + ['./tiny', './tiny/apple', './tiny/banana', './tiny/cherry'] == res.files*.name + ['user1', 'user1', 'default', 'user2'] == res.format.header.getEntry(FILEUSERNAME).values.toList() + } + + def 'differentGroupsBetweenCopySpecs'() { + Project project = ProjectBuilder.builder().build() + + File buildDir = project.buildDir + + def fruits = ['apple', 'banana', 'cherry'] + def srcDir = [new File(buildDir, 'src1'), new File(buildDir, 'src2'), new File(buildDir, 'src3')] + srcDir.eachWithIndex { file, idx -> + file.mkdirs() + String word = fruits[idx] + new File(file, word).withWriter { out -> + out.write(word) + } + } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'userTest' + version = '2.0' + release = '2' + type = BINARY + arch = I386 + os = LINUX + + into '/tiny' + permissionGroup 'default' + + from(srcDir[0]) { + // should be default group + } + + from(srcDir[1]) { + //setPermissionGroup 'group2' // works + //permissionGroup = 'group2' // Does not work + permissionGroup 'group2' // Does not work + } + + from(srcDir[2]) { + // should be default group + } + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/userTest-2.0-2.i386.rpm').toPath()) + [DIR, FILE, FILE, FILE] == res.files*.type + ['./tiny', './tiny/apple', './tiny/banana', './tiny/cherry'] == res.files*.name + def allFiles = res.files + def groups = res.format.header.getEntry(FILEGROUPNAME).values + ['default', 'default', 'group2', 'default'] == res.format.header.getEntry(FILEGROUPNAME).values.toList() + } + + def 'differentPermissionsBetweenCopySpecs'() { + File srcDir1 = new File(projectDir, 'src1') + File srcDir2 = new File(projectDir, 'src2') + File srcDir3 = new File(projectDir, 'src3') + + srcDir1.mkdirs() + srcDir2.mkdirs() + srcDir3.mkdirs() + + new File(srcDir1, 'apple').withWriter { out -> out.write('apple') } + new File(srcDir2, 'banana').withWriter { out -> out.write('banana') } + new File(srcDir3, 'cherry').withWriter { out -> out.write('cherry') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'userTest' + version = '2.0' + release = '2' + type = BINARY + arch = I386 + os = LINUX + + into '/tiny' + fileMode 0555 + + from(srcDir1) { + // should be default group + } + + from(srcDir2) { + fileMode 0666 + } + + from(srcDir3) { + fileMode 0555 + } + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/userTest-2.0-2.i386.rpm').toPath()) + [DIR, FILE, FILE, FILE] == res.files*.type + ['./tiny', './tiny/apple', './tiny/banana', './tiny/cherry'] == res.files*.name + + // #define S_IFIFO 0010000 /* named pipe (fifo) */ + // #define S_IFCHR 0020000 /* character special */ + // #define S_IFDIR 0040000 /* directory */ + // #define S_IFBLK 0060000 /* block special */ + // #define S_IFREG 0100000 /* regular */ + // #define S_IFLNK 0120000 /* symbolic link */ + // #define S_IFSOCK 0140000 /* socket */ + // #define S_ISUID 0004000 /* set user id on execution */ + // #define S_ISGID 0002000 /* set group id on execution */ + // #define S_ISTXT 0001000 /* sticky bit */ + // #define S_IRWXU 0000700 /* RWX mask for owner */ + // #define S_IRUSR 0000400 /* R for owner */ + // #define S_IWUSR 0000200 /* W for owner */ + // #define S_IXUSR 0000100 /* X for owner */ + // #define S_IRWXG 0000070 /* RWX mask for group */ + // #define S_IRGRP 0000040 /* R for group */ + // #define S_IWGRP 0000020 /* W for group */ + // #define S_IXGRP 0000010 /* X for group */ + // #define S_IRWXO 0000007 /* RWX mask for other */ + // #define S_IROTH 0000004 /* R for other */ + // #define S_IWOTH 0000002 /* W for other */ + // #define S_IXOTH 0000001 /* X for other */ + // #define S_ISVTX 0001000 /* save swapped text even after use */ + + // drwxr-xr-x is 0040755 + // NOTE: Not sure why directory is getting user write permission + [(short)0040755, (short)0100555, (short)0100666, (short)0100555] == res.format.header.getEntry(FILEMODES).values.toList() + } + + def 'no Prefix Value'() { + given: + Project project = ProjectBuilder.builder().build() + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'one-prefix' + version = '1.0' + release = '1' + arch = I386 + os = LINUX + + into '/opt/myprefix' + from (srcDir) + }) + + when: + rpmTask.execute() + + then: + def scan = RpmReader.read(project.file('build/tmp/RpmPluginTest/one-prefix-1.0-1.i386.rpm').toPath()) + null == RpmReader.getHeaderEntry(scan, PREFIXES) + } + + def 'one Prefix Value'() { + given: + Project project = ProjectBuilder.builder().build() + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'one-prefix' + version = '1.0' + release = '1' + arch = I386 + os = LINUX + + into '/opt/myprefix' + from (srcDir) + + prefixes '/opt/myprefix' + }) + + when: + rpmTask.execute() + + then: + def scan = RpmReader.read(project.file('build/tmp/RpmPluginTest/one-prefix-1.0-1.i386.rpm').toPath()) + '/opt/myprefix' == RpmReader.getHeaderEntryString(scan, PREFIXES) + } + + def 'multiple Prefix Values'() { + given: + Project project = ProjectBuilder.builder().build() + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'one-prefix' + version = '1.0' + release = '1' + arch = I386 + os = LINUX + + into '/opt/myprefix' + from (srcDir) + + prefixes '/opt/myprefix', '/etc/init.d' + }) + + when: + rpmTask.execute() + + then: + def scan = RpmReader.read(project.file('build/tmp/RpmPluginTest/one-prefix-1.0-1.i386.rpm').toPath()) + // NOTE: Scanner just jams things together as one string + '/opt/myprefix/etc/init.d' == RpmReader.getHeaderEntryString(scan, PREFIXES) + } + + def 'multiple Added then cleared Prefix Values'() { + given: + Project project = ProjectBuilder.builder().build() + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'one-prefix' + version = '1.0' + release = '1' + arch = I386 + os = LINUX + + into '/opt/myprefix' + from (srcDir) + + prefixes '/opt/myprefix', '/etc/init.d' + prefixes.clear() + }) + + when: + rpmTask.execute() + + then: + def scan = RpmReader.read(project.file('build/tmp/RpmPluginTest/one-prefix-1.0-1.i386.rpm').toPath()) + null == RpmReader.getHeaderEntry(scan, PREFIXES) + } + + def 'direct assignment of Prefix Values'() { + given: + Project project = ProjectBuilder.builder().build() + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'multi-prefix' + version = '1.0' + release = '1' + arch = I386 + os = LINUX + + into '/opt/myprefix' + from (srcDir) + + prefixes = ['/opt/myprefix', '/etc/init.d'] + }) + + when: + rpmTask.execute() + + then: + def scan = RpmReader.read(project.file('build/tmp/RpmPluginTest/multi-prefix-1.0-1.i386.rpm').toPath()) + // NOTE: Scanner just jams things together as one string + '/opt/myprefix/etc/init.d' == RpmReader.getHeaderEntryString(scan, PREFIXES) + } + + def 'ospackage assignment of Prefix Values'() { + given: + Project project = ProjectBuilder.builder().build() + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + //project.ospackage { prefixes = ['/opt/ospackage', '/etc/maybe'] } + + def rpmTask = project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'multi-prefix' + version = '1.0' + release = '1' + arch = I386 + os = LINUX + prefix '/apps' + + into '/opt/myprefix' + from (srcDir) + }) + + when: + rpmTask.execute() + + then: + def scan = RpmReader.read(project.file('build/tmp/RpmPluginTest/multi-prefix-1.0-1.i386.rpm').toPath()) + // NOTE: Scanner just jams things together as one string + def foundPrefixes = RpmReader.getHeaderEntry(scan, PREFIXES) + foundPrefixes.values.contains('/apps') + //foundPrefixes.values.contains('/opt/ospackage') + //foundPrefixes.values.contains('/etc/maybe') + } + + def 'Avoids including empty directories'() { + Project project = ProjectBuilder.builder().build() + + File myDir = new File(projectDir, 'my') + File contentDir = new File(myDir, 'own/content') + contentDir.mkdirs() + new File(contentDir, 'myfile.txt').withWriter { out -> out.write('test') } + + File emptyDir = new File(myDir, 'own/empty') + emptyDir.mkdirs() + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + arch = I386 + + from(myDir) { + addParentDirs false + } + includeEmptyDirs false + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + res.files*.name.every { './own/content/myfile.txt'.startsWith(it) } + } + + def 'Can create empty directories'() { + Project project = ProjectBuilder.builder().build() + + File myDir = new File(projectDir, 'my') + File contentDir = new File(myDir, 'own/content') + contentDir.mkdirs() + new File(contentDir, 'myfile.txt').withWriter { out -> out.write('test') } + + File otherDir = new File(projectDir, 'other') + otherDir.mkdirs() + File someDir = new File(otherDir, 'some') + someDir.mkdirs() + File emptyDir = new File(someDir, 'empty') + emptyDir.mkdirs() + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + arch = I386 + + from(myDir) { + addParentDirs false + } + + from(someDir) { + into '/inside/the/archive' + addParentDirs false + createDirectoryEntry true + } + + directory('/using/the/dsl') + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + res.files*.name.containsAll(['./inside/the/archive/empty', './own/content/myfile.txt', './using/the/dsl']) + res.files*.type.containsAll([DIR, FILE]) + } + + def 'Sets owner and group for directory DSL'() { + Project project = ProjectBuilder.builder().build() + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + arch = I386 + + user 'test' + permissionGroup 'test' + + directory('/using/the/dsl') + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + res.files*.name == ['./using/the/dsl'] + res.files*.type == [DIR] + res.format.header.getEntry(FILEGROUPNAME).values.toList() == ['test'] + } + + def 'has epoch value'() { + given: + Project project = ProjectBuilder.builder().build() + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + def rpmTask = project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'has-epoch' + version = '1.0' + release = '1' + epoch = 2 + arch = I386 + os = LINUX + + into '/opt/bleah' + from (srcDir) + }) + + when: + rpmTask.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/has-epoch-1.0-1.i386.rpm').toPath()) + 2 == RpmReader.getHeaderEntry(res, EPOCH).values[0] + } + + def 'Does not include signature header if signing is not fully configured'() { + given: + Project project = ProjectBuilder.builder().build() + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + arch = I386 + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + res.format.signatureHeader.getEntry(SignatureTag.LEGACY_PGP) == null + } + + def 'Does include signature header'() { + given: + Project project = ProjectBuilder.builder().build() + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + arch = I386 + + signingKeyId = 'F02C6D2C' + signingKeyPassphrase = 'test' + signingKeyRing = 'src/test/resources/pgp/test-secring.gpg' + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + res.format.signatureHeader.getEntry(SignatureTag.LEGACY_PGP) != null + } + + /** + * Verifies that a symlink can be preserved. + * + * The following directory structure is assumed: + * + * . + * └── usr + * └── bin + * ├── foo -> foo-1.2 + * └── foo-1.2 + * └── foo.txt + */ + def 'Preserves symlinks'() { + setup: + File symlinkDir = new File(projectDir, 'symlink') + File binDir = new File(symlinkDir, 'usr/bin') + File fooDir = new File(binDir, 'foo-1.2') + binDir.mkdirs() + fooDir.mkdirs() + new File(fooDir, 'foo.txt').withWriter { out -> out.write('foo') } + Files.createSymbolicLink(binDir.toPath().resolve('foo'), fooDir.toPath()) + + when: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + Task task = project.task('buildRpm', type: Rpm) { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + type = BINARY + arch = I386 + + from(symlinkDir) { + createDirectoryEntry true + } + } + + task.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + res.files*.name == ['./usr', './usr/bin', './usr/bin/foo', './usr/bin/foo-1.2', './usr/bin/foo-1.2/foo.txt'] + res.files*.type == [DIR, DIR, SYMLINK, DIR, FILE] + } + + def "Does not throw UnsupportedOperationException when copying external artifact with createDirectoryEntry option"() { + given: + String testCoordinates = 'com.netflix.nebula:a:1.0.0' + DependencyGraph graph = new DependencyGraph([testCoordinates]) + File reposRootDir = new File(project.buildDir, 'repos') + GradleDependencyGenerator generator = new GradleDependencyGenerator(graph, reposRootDir.absolutePath) + generator.generateTestMavenRepo() + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.configurations { + myConf + } + + project.dependencies { + myConf testCoordinates + } + + project.repositories { + maven { + url { + "file://$reposRootDir/mavenrepo" + } + } + } + + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + packageName = 'bleah' + + from(project.configurations.myConf) { + createDirectoryEntry = true + into('root/lib') + } + } + + when: + rpmTask.execute() + + then: + noExceptionThrown() + } + + @Unroll + def "Translates package description '#description' to header entry"() { + given: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + version = '1.0' + packageName = 'bleah' + packageDescription = description + } + + when: + rpmTask.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0.noarch.rpm').toPath()) + RpmReader.getHeaderEntryString(res, DESCRIPTION) == headerEntry + + where: + description | headerEntry + 'This is a description' | 'This is a description' + '' | '' + null | '' + } + + @Unroll + def "Translates project description '#description' to header entry"() { + given: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + project.description = description + + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + version = '1.0' + packageName = 'bleah' + } + + when: + rpmTask.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0.noarch.rpm').toPath()) + RpmReader.getHeaderEntryString(res, DESCRIPTION) == headerEntry + + where: + description | headerEntry + 'This is a description' | 'This is a description' + '' | '' + null | '' + } + + def "Can set user and group for packaged files"() { + given: + File srcDir = new File(projectDir, 'src') + srcDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + version = '1.0' + packageName = 'bleah' + + from(srcDir) { + user = 'me' + permissionGroup = 'awesome' + } + } + + when: + rpmTask.execute() + + then: + Header header = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0.noarch.rpm').toPath()).format.header + ['awesome'] == header.getEntry(FILEGROUPNAME).values.toList() + ['me'] == header.getEntry(FILEUSERNAME).values.toList() + } + + def "Can set multiple users and groups for packaged files"() { + given: + File srcDir = new File(projectDir, 'src') + File scriptDir = new File(projectDir, 'script') + srcDir.mkdirs() + scriptDir.mkdirs() + new File(srcDir, 'apple').withWriter { out -> out.write('apple') } + new File(scriptDir, 'orange').withWriter { out -> out.write('orange') } + new File(scriptDir, 'banana').withWriter { out -> out.write('banana') } + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + version = '1.0' + packageName = 'bleah' + + user 'defaultUser' + permissionGroup 'defaultGroup' + + from(srcDir) { + user 'me' + permissionGroup 'awesome' + } + + from(scriptDir) { + into '/etc' + } + } + + when: + rpmTask.execute() + + then: + Header header = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0.noarch.rpm').toPath()).format.header + ['awesome', 'defaultGroup', 'defaultGroup'] == header.getEntry(FILEGROUPNAME).values.toList() + ['me', 'defaultUser', 'defaultUser'] == header.getEntry(FILEUSERNAME).values.toList() + } + + @Unroll + def 'handle semantic versions with dashes and metadata (+) expect #version to be #expected'() { + given: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + project.version = version + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + packageName = 'semvertest' + }) + + project.tasks.buildRpm.execute() + + expect: + project.file("build/tmp/RpmPluginTest/semvertest-${expected}.noarch.rpm").exists() + + where: + version | expected + '1.0' | '1.0' + '1.0.0' | '1.0.0' + '1.0.0-rc.1' | '1.0.0~rc.1' + '1.0.0-dev.3+abc219' | '1.0.0~dev.3' + } + + def 'handles multiple provides'() { + given: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + project.version = '1.0' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + packageName = 'providesTest' + provides 'foo' + provides 'bar' + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/providesTest-1.0.noarch.rpm').toPath()) + def provides = RpmReader.getHeaderEntry(res, PROVIDENAME) + ['foo', 'bar'].every { it in provides.values } + } + + def 'Add preTrans and postTrans scripts'() { + given: + Path prescript = Paths.get(projectDir.toString(), 'prescript') + Path postscript = Paths.get(projectDir.toString(), 'postscript') + prescript.withWriter { out -> out.write('MyPreTransScript') } + postscript.withWriter { out -> out.write('MyPostTransScript') } + + Project project = ProjectBuilder.builder().build() + + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + project.task([type: Rpm], 'buildRpm', { + destinationDir = project.file('build/tmp/RpmPluginTest') + destinationDir.mkdirs() + + packageName = 'bleah' + version = '1.0' + release = '1' + arch = I386 + + preTrans prescript + postTrans postscript + }) + + when: + project.tasks.buildRpm.execute() + + then: + def res = RpmReader.read(project.file('build/tmp/RpmPluginTest/bleah-1.0-1.i386.rpm').toPath()) + def PRE_TRANS_HEADER_INDEX = 1151 + def POST_TRANS_HEADER_INDEX = 1152 + res.format.header.entries[PRE_TRANS_HEADER_INDEX].values[0].contains('MyPreTransScript') + res.format.header.entries[POST_TRANS_HEADER_INDEX].values[0].contains('MyPostTransScript') + } + + def 'preserve symlinks without closure'() { + given: + Path target = Files.createTempFile("file-to-symlink-to", "sh") + File file = project.file('bin/my-symlink') + file.parentFile.mkdirs() + Files.createSymbolicLink(Paths.get(file.path), target) + + when: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + Rpm rpmTask = project.task([type: Rpm], 'buildRpm', { + from 'bin' + }) + rpmTask.execute() + + then: + def res = RpmReader.read(rpmTask.getArchivePath().toPath()) + def symlink = res.files.find { it.name == 'my-symlink' } + symlink.header.type == SYMLINK + } + + def 'preserve symlinks with closure'() { + given: + Path target = Files.createTempFile("file-to-symlink-to", "sh") + File file = project.file('bin/my-symlink') + file.parentFile.mkdirs() + Files.createSymbolicLink(file.toPath(), target) + + when: + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + + Rpm rpmTask = project.task([type: Rpm], 'buildRpm', { + from('bin') { + into 'lib' + } + }) + rpmTask.execute() + + then: + def res = RpmReader.read(rpmTask.getArchivePath().toPath()) + def symlink = res.files.find { it.name == 'lib/my-symlink' } + symlink.header.type == SYMLINK + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmReader.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmReader.groovy new file mode 100644 index 0000000..64ea920 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/RpmReader.groovy @@ -0,0 +1,131 @@ +package org.xbib.gradle.plugin.rpm + +import groovy.transform.Canonical +import org.xbib.io.compress.bzip2.Bzip2InputStream +import org.xbib.io.compress.xz.XZInputStream +import org.xbib.rpm.header.Header +import org.xbib.rpm.header.HeaderTag +import org.xbib.rpm.header.entry.SpecEntry +import org.xbib.rpm.io.ChannelWrapper +import org.xbib.rpm.io.ReadableChannelWrapper +import org.xbib.rpm.format.Format +import org.xbib.rpm.payload.CompressionType +import org.xbib.rpm.payload.CpioHeader +import org.spockframework.util.Nullable + +import java.nio.ByteBuffer +import java.nio.CharBuffer +import java.nio.channels.Channels +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import java.util.zip.GZIPInputStream + +import static org.xbib.rpm.header.HeaderTag.HEADERIMMUTABLE +import static org.xbib.rpm.signature.SignatureTag.SIGNATURES +import static org.junit.Assert.assertEquals + +/** + * + */ +class RpmReader { + + @Canonical + static class ReaderResult { + Format format + List files + } + + @Canonical + static class ReaderFile { + @Delegate + CpioHeader header + + @Nullable + ByteBuffer contents + + String asString() { + if (contents == null ) { + return null + } + Charset charset = StandardCharsets.UTF_8 + CharBuffer buffer = charset.decode(contents) + return buffer.toString() + } + } + + static ReaderResult read(Path path) throws Exception { + ReaderResult readerResult = null + path.withInputStream { InputStream inputStream -> + readerResult = read(inputStream) + } + readerResult + } + + static ReaderResult read(InputStream inputStream, boolean includeContents = true) { + ReadableChannelWrapper wrapper = new ReadableChannelWrapper(Channels.newChannel(inputStream)) + Format format = readHeader(wrapper) + InputStream uncompressed = createUncompressedStream(format.getHeader(), inputStream) + wrapper = new ReadableChannelWrapper(Channels.newChannel(uncompressed)) + CpioHeader header = null + def files = [] + int total = 0 + while (header == null || !header.isLast()) { + header = new CpioHeader() + total = header.read(wrapper, total) + final int fileSize = header.getFileSize() + boolean includingContents = includeContents && header.type == CpioHeader.FILE + if (!header.isLast()) { + ByteBuffer descriptor = includingContents ? ChannelWrapper.fill(wrapper, fileSize) : null + files += new ReaderFile(header, descriptor) + } + if (!includingContents) { + assertEquals(fileSize, uncompressed.skip(fileSize)) + } + total += fileSize + } + return new ReaderResult(format,files) + } + + static InputStream createUncompressedStream(Header header, InputStream inputStream) { + InputStream compressedInput = inputStream + SpecEntry pcEntry = header.getEntry(HeaderTag.PAYLOADCOMPRESSOR) + String[] pc = (String[]) pcEntry.getValues() + CompressionType compressionType = CompressionType.valueOf(pc[0].toUpperCase()) + switch (compressionType) { + case CompressionType.NONE: + break + case CompressionType.GZIP: + compressedInput = new GZIPInputStream(inputStream) + break + case CompressionType.BZIP2: + compressedInput = new Bzip2InputStream(inputStream) + break + case CompressionType.XZ: + compressedInput = new XZInputStream(inputStream) + break + } + compressedInput + } + + static Format readHeader(ReadableChannelWrapper wrapper) throws Exception { + Format format = new Format() + format.getLead().read(wrapper) + int count = format.signatureHeader.read(wrapper) + int expected = ByteBuffer.wrap(format.signatureHeader.getEntry(SIGNATURES).values, 8, 4).getInt() / -16 + assertEquals(expected, count) + count = format.getHeader().read(wrapper) + expected = ByteBuffer.wrap(format.getHeader().getEntry(HEADERIMMUTABLE).values, 8, 4).getInt() / -16 + assertEquals(expected, count) + return format + } + + def static getHeaderEntry(ReaderResult res, tag) { + def header = res.format.header + header.getEntry(tag.code) + } + + def static getHeaderEntryString(ReaderResult res, tag) { + getHeaderEntry(res, tag)?.values?.join('') + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtensionTest.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtensionTest.groovy new file mode 100644 index 0000000..a51c6bb --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/SystemPackagingExtensionTest.groovy @@ -0,0 +1,77 @@ +package org.xbib.gradle.plugin.rpm + +import spock.lang.Specification + +import java.nio.file.Paths + +class SystemPackagingExtensionTest extends Specification { + + SystemPackagingExtension extension = new SystemPackagingExtension() + + def "Can define required package name without version and flag"() { + given: + String packageName = 'myPackage' + + when: + extension.requires(packageName) + + then: + extension.dependencies.size() == 1 + Dependency dep = extension.dependencies[0] + dep.packageName == packageName + dep.version == '' + dep.flag == 0 + } + + def "Can define required package name with version and without flag"(){ + given: + String packageName = 'myPackage' + + when: + extension.requires(packageName, '1.0.0') + + then: + extension.dependencies.size() == 1 + Dependency dep = extension.dependencies[0] + dep.packageName == packageName + dep.version == '1.0.0' + dep.flag == 0 + } + + def "Can define required package name with version and flag"() { + given: + String packageName = 'myPackage' + + when: + extension.requires(packageName, '1.0.0', 5) + + then: + extension.dependencies.size() == 1 + Dependency dep = extension.dependencies[0] + dep.packageName == packageName + dep.version == '1.0.0' + dep.flag == 5 + } + + def "Cannot define required package name containing comma without version and flag"() { + given: + String packageName = 'myPackage,something' + + when: + extension.requires(packageName) + + then: + Throwable t = thrown(IllegalArgumentException) + } + + def "Cannot define required package name containing comma with version and flag"() { + given: + String packageName = 'myPackage,something' + + when: + extension.requires(packageName, '1.0.0', 5) + + then: + Throwable t = thrown(IllegalArgumentException) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingApiGradleHandleFactory.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingApiGradleHandleFactory.groovy new file mode 100644 index 0000000..d613d09 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingApiGradleHandleFactory.groovy @@ -0,0 +1,113 @@ +package org.xbib.gradle.plugin.rpm + +import org.gradle.tooling.BuildLauncher +import org.gradle.tooling.GradleConnector +import org.gradle.tooling.ProjectConnection + +import java.util.concurrent.TimeUnit + +class ToolingApiGradleHandleFactory implements GradleHandleFactory { + + private final boolean fork + + private final String version + + private final Integer daemonMaxIdleTimeInSeconds + + ToolingApiGradleHandleFactory(boolean fork, String version, Integer daemonMaxIdleTimeInSeconds = null) { + this.fork = fork + this.version = version + this.daemonMaxIdleTimeInSeconds = daemonMaxIdleTimeInSeconds + } + + @Override + GradleHandle start(File projectDir, List arguments, List jvmArguments = []) { + GradleConnector connector = createGradleConnector(projectDir) + boolean forkedProcess = isForkedProcess() + connector.embedded(!forkedProcess) + if (daemonMaxIdleTimeInSeconds != null) { + connector.daemonMaxIdleTime(daemonMaxIdleTimeInSeconds, TimeUnit.SECONDS) + } + ProjectConnection connection = connector.connect(); + BuildLauncher launcher = createBuildLauncher(connection, arguments, jvmArguments) + createGradleHandle(connection, launcher, forkedProcess) + } + + private GradleConnector createGradleConnector(File projectDir) { + GradleConnector connector = GradleConnector.newConnector(); + connector.forProjectDirectory(projectDir); + configureGradleVersion(connector, projectDir) + connector + } + + private void configureGradleVersion(GradleConnector connector, File projectDir) { + if (version != null) { + connector.useGradleVersion(version) + } else { + configureWrapperDistributionIfUsed(connector, projectDir) + } + } + + private static void configureWrapperDistributionIfUsed(GradleConnector connector, File projectDir) { + File target = projectDir.absoluteFile + while (target != null) { + URI distribution = prepareDistributionURI(target) + if (distribution) { + connector.useDistribution(distribution) + return + } + target = target.parentFile + } + } + + private static URI prepareDistributionURI(File target) { + File propertiesFile = new File(target, "gradle/wrapper/gradle-wrapper.properties") + if (propertiesFile.exists()) { + Properties properties = new Properties() + propertiesFile.withInputStream { + properties.load(it) + } + URI source = new URI(properties.getProperty("distributionUrl")) + return source.getScheme() == null ? (new File(propertiesFile.getParentFile(), source.getSchemeSpecificPart())).toURI() : source; + } + return null + } + + private boolean isForkedProcess() { + fork + } + + private static BuildLauncher createBuildLauncher(ProjectConnection connection, List arguments, + List jvmArguments) { + BuildLauncher launcher = connection.newBuild(); + launcher.withArguments(arguments as String[]); + launcher.setJvmArguments(jvmArguments as String[]) + launcher + } + + private GradleHandle createGradleHandle(ProjectConnection connection, BuildLauncher launcher, boolean forkedProcess) { + GradleHandleBuildListener toolingApiBuildListener = + new ToolingApiBuildListener(connection) + BuildLauncherBackedGradleHandle buildLauncherBackedGradleHandle = + new BuildLauncherBackedGradleHandle(launcher, forkedProcess) + buildLauncherBackedGradleHandle.registerBuildListener(toolingApiBuildListener) + buildLauncherBackedGradleHandle + } + + private class ToolingApiBuildListener implements GradleHandleBuildListener { + private final ProjectConnection connection + + ToolingApiBuildListener(ProjectConnection connection) { + this.connection = connection + } + + @Override + void buildStarted() { + } + + @Override + void buildFinished() { + connection.close() + } + } +} \ No newline at end of file diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingExecutionResult.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingExecutionResult.groovy new file mode 100644 index 0000000..2f881e9 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/ToolingExecutionResult.groovy @@ -0,0 +1,12 @@ +package org.xbib.gradle.plugin.rpm + +/** + * Hold additional response data, that is only available + */ +class ToolingExecutionResult extends DefaultExecutionResult { + + ToolingExecutionResult(Boolean success, String standardOutput, String standardError, + List executedTasks, Throwable failure) { + super(success, standardOutput, standardError, executedTasks, failure) + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidatorTest.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidatorTest.groovy new file mode 100644 index 0000000..bd99d6c --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmPackageNameAttributeValidatorTest.groovy @@ -0,0 +1,35 @@ +package org.xbib.gradle.plugin.rpm.validation + +import spock.lang.Specification +import spock.lang.Unroll + +class RpmPackageNameAttributeValidatorTest extends Specification { + + RpmPackageNameAttributeValidator validator = new RpmPackageNameAttributeValidator() + + @Unroll + def "verifies #description: '#attribute'"() { + when: + boolean valid = validator.validate(attribute) + + then: + valid == result + + where: + attribute | result | description + 'a25b' | true | 'valid package name with mixed alphanumeric characters' + 'my.awesome.package' | true | 'package with dot characters' + 'my-awesome-package' | true | 'package with dash characters' + 'my_awesome_package' | true | 'package with underscore characters' + 'My-Awesome-Package' | true | 'package with upper case characters' + 'abc^' | false | 'package name with an invalid character' + } + + def "provide error message"() { + when: + String errorMessage = validator.getErrorMessage('abc^') + + then: + errorMessage == "Invalid package name 'abc^' - a valid package name must only contain [a-zA-Z0-9-._+]" + } +} diff --git a/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidatorIntegrationTest.groovy b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidatorIntegrationTest.groovy new file mode 100644 index 0000000..69c0718 --- /dev/null +++ b/gradle-plugin-rpm/src/test/groovy/org/xbib/gradle/plugin/rpm/validation/RpmTaskPropertiesValidatorIntegrationTest.groovy @@ -0,0 +1,41 @@ +package org.xbib.gradle.plugin.rpm.validation + +import org.gradle.api.InvalidUserDataException +import org.xbib.gradle.plugin.rpm.ProjectSpec +import org.xbib.gradle.plugin.rpm.Rpm + +class RpmTaskPropertiesValidatorIntegrationTest extends ProjectSpec { + + RpmTaskPropertiesValidator validator = new RpmTaskPropertiesValidator() + + def setup() { + project.apply plugin: 'org.xbib.gradle.plugin.rpm' + } + + def 'can execute Rpm task with valid version and package name'() { + given: + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + packageName = 'can-execute-rpm-task-with-valid-version' + } + + when: + validator.validate(rpmTask) + + then: + noExceptionThrown() + } + + def 'executing a Rpm task with invalid package name throws exception'() { + given: + Rpm rpmTask = project.task('buildRpm', type: Rpm) { + packageName = 'abc^' + } + + when: + validator.validate(rpmTask) + + then: + Throwable t = thrown(InvalidUserDataException) + t.message == "Invalid package name 'abc^' - a valid package name must only contain [a-zA-Z0-9-._+]" + } +} diff --git a/gradle-plugin-rpm/src/test/resources/pgp/test-secring.gpg b/gradle-plugin-rpm/src/test/resources/pgp/test-secring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..2044048a0212d321a6926cc727415cf4da5fcf81 GIT binary patch literal 2551 zcmVAo#(g@*%9Adr8N`H!W04^zbqGp|s z*@XictD`iQ3z27Fv1sf}W1jTn2iB$^Sq8T?r;__F`>eE&Z(`xhNL4EBE(TXiRo~o# zY>3TSf&iUhPpHISL0{UszGqN^I?hO;+h85KNB|b!IQoPxV!I4~E!x{b#Z!|Q&Y$hU- zUo~!&XQI^3A@~np78SNnh>AAE69;WPX6tj9e5s?75%r=ny0;BQ2t#ma)pb5V$73CuCVEb`^|Ayr&&!^%>qw;3) zv_!~WG|m$(pXe5^#W1BDn^fJ(PkQ*2m6}~7$U%D2og>#<0UmD8fZSQQu3v~^Mk9;M z#;!wg#1}DKfxg$sFOs|k8?1U2KXN!29_*z?fo{UCJ`K+Vr!+GX)YIdwwgM`k&E-cNGfO$p7(NvZy}8U5XQS%P5ZtAA{uezQ)yhGQqw$EVR> zSvs!qu9lk&G?)cn5az%g%w)iNqwCEv%J&bSdEq@J{Kxt{#~l0vZ*cov^*1~3b<=p) ztDeziiI~dJ%F0Vohxl*n_xJXCBglCTkaQqJ5z>AKMX}WmctN%Zv=DSj(W2Ki|~xA6%%&Z7&$eSvJ%(vYi7ZVt0o=`;yizxY50*`-LdG#1G@Yy7jvZPJXmW zk^pTeW(EKZXS{dmy2*T3NooYY`0vEJD%_-qxJ_+UcUrPae8*O{FlUS(q~8ra&`5y( zb$X&t(S{hkFO;IzD?g|+6Icy?juS`z*l6TzQGk*4K6suaGu`?fS#@u`2?rliL11?K z24_C=HdQO#8KFh!juAK6c0K$MtGquGU?}*j6ulqmKQZ z1I7ed)72&c2mrj`KHvo^DCz zj^b-JB0$3FO)bq6qjF&64ZZN~sub^37!&Xife%Oz93m*y60>s7fBJ+KwkSS{qb{5{ z%{6NBIg$h)AJJdmoi9jSlNapOdIYvM6Q5q5y8=mxm`6RNvNRVe)uk6wi2s1G#H+-J zOwYzY_7}_h)UB{PE{zFqZubptV{w~@Po%h66>>}=pQhJzxz0gXNx59*iato{Yg~9e ztua4;{E+|=0RRF12?Gchy$jF`KlRJNK~8%_2;TSwdUfY&qd_D0%L@b`ucNqc?-;MCk4z1wWzvGr+w(>2eE)|; zO%f|hhgoeI==|bx*x41%k`|&$^tU_~p-bFFbbyR|R;G0FyFFxS^x2d8fDdmH7$(I; zcaE@d@Z)1PD1?299f_$5wzSL~6qJ*iu)lFLI2onQhLoonlV>3s_sd+@r3G=^~4FR$rfvQQ@4SGFlH;dc;D4~GtL&mo9JG!WiS9N(Uq6e}bD z-#sHdigow5Tr5|tT}$kyS$kg8GNh!tREkF7Bl7NX@b?gD8QtY2sjRvT9FyOWW!4e+ zi;lESrv!(pAJ}hBVTeyr9p(!DbPhaE)&zI*%|VMH(?MHq)QL0LGyuy70%uM4GET~}G)yh1qsqPTk|0wFSeGP0C=o7;; zZZ8pbiYrGnNzCo?|t4?M!8opwr zQ;B?t&ocQWTGqEWTuX-B3xWv$2PLw%csMRtklDGu|4mbJDUUyn z2@tx{NEYxcZ7d-E2mp_L6hp!C#7Xi@K_9PR;&b(XNZ1TSkfD#tziBd&x&&&lMl>8s zyOmhBpufP2??1vJ*9XmV2YCEhGkX=C)>(CYaV=^L=#!>!_>RB`9#<;0+*a-jt literal 0 HcmV?d00001 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..51dad86 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,15 @@ +group = org.xbib +name = rpm +version = 1.0.0 + +bouncycastle.version = 1.57 +xbib-archive.version = 0.0.1 +ant.version = 1.10.1 +log4j.version = 2.8.2 +junit.version = 4.12 +wagon.version = 2.12 +groovy.version = 2.4.11 +spock-core.version = 1.1-groovy-2.4 +xbib-guice.version = 4.0.4 +maven.version = 3.5.0 +mvel.version = 2.3.2.Final \ No newline at end of file diff --git a/gradle/ext.gradle b/gradle/ext.gradle new file mode 100644 index 0000000..e159ee4 --- /dev/null +++ b/gradle/ext.gradle @@ -0,0 +1,8 @@ +ext { + user = 'xbib' + projectName = 'rpm' + projectDescription = 'Java 8 RPM implementation with plugins for Ant, Maven, Gradle' + scmUrl = 'https://github.com/xbib/rpm' + scmConnection = 'scm:git:git://github.com/xbib/rpm.git' + scmDeveloperConnection = 'scm:git:git://github.com/xbib/rpm.git' +} diff --git a/gradle/publish.gradle b/gradle/publish.gradle new file mode 100644 index 0000000..0b478bf --- /dev/null +++ b/gradle/publish.gradle @@ -0,0 +1,67 @@ + +task xbibUpload(type: Upload) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('xbibUsername')) { + mavenDeployer { + configuration = configurations.wagon + repository(url: uri('sftp://xbib.org/repository')) { + authentication(userName: xbibUsername, privateKey: xbibPrivateKey) + } + } + } + } +} + +task sonatypeUpload(type: Upload) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('ossrhUsername')) { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + repository(url: uri(ossrhReleaseUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + snapshotRepository(url: uri(ossrhSnapshotUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + pom.project { + name projectName + description projectDescription + packaging 'jar' + inceptionYear '2017' + url scmUrl + organization { + name 'xbib' + url 'http://xbib.org' + } + developers { + developer { + id user + name 'Jörg Prante' + email 'joergprante@gmail.com' + url 'https://github.com/jprante' + } + } + scm { + url scmUrl + connection scmConnection + developerConnection scmDeveloperConnection + } + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } + } +} + +nexusStaging { + packageGroup = "org.xbib" +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..736fb7d3f94c051b359fc7ae7212d351bc094bdd GIT binary patch literal 54708 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giYMoi z2tt1q)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^ShTtO;VyD{dezY;XD@Rwl_9#j4Uo!1W&ZHVe0H>f=h#9k>~KUj^iUJ%@wU{Xuy z3FItk0<;}6D02$u(RtEY#O^hrB>qgxnOD^0AJPGC9*WXw_$k%1a%-`>uRIeeAIf3! zbx{GRnG4R$4)3rVmg63gW?4yIWW_>;t3>4@?3}&ct0Tk}<5ljU>jIN1 z&+mzA&1B6`v(}i#vAzvqWH~utZzQR;fCQGLuCN|p0hey7iCQ8^^dr*hi^wC$bTk`8M(JRKtQuXlSf$d(EISvuY0dM z7&ff;p-Ym}tT8^MF5ACG4sZmAV!l;0h&Mf#ZPd--_A$uv2@3H!y^^%_&Iw$*p79Uc5@ZXLGK;edg%)6QlvrN`U7H@e^P*0Atd zQB%>4--B1!9yeF(3vk;{>I8+2D;j`zdR8gd8dHuCQ_6|F(5-?gd&{YhLeyq_-V--4 z(SP#rP=-rsSHJSHDpT1{dMAb7-=9K1-@co_!$dG^?c(R-W&a_C5qy2~m3@%vBGhgnrw|H#g9ABb7k{NE?m4xD?;EV+fPdE>S2g$U(&_zGV+TPvaot>W_ zf8yY@)yP8k$y}UHVgF*uxtjW2zX4Hc3;W&?*}K&kqYpi%FHarfaC$ETHpSoP;A692 zR*LxY1^BO1ry@7Hc9p->hd==U@cuo*CiTnozxen;3Gct=?{5P94TgQ(UJoBb`7z@BqY z;q&?V2D1Y%n;^Dh0+eD)>9<}=A|F5{q#epBu#sf@lRs`oFEpkE%mrfwqJNFCpJC$| zy6#N;GF8XgqX(m2yMM2yq@TxStIR7whUIs2ar$t%Avh;nWLwElVBSI#j`l2$lb-!y zK|!?0hJ1T-wL{4uJhOFHp4?@28J^Oh61DbeTeSWub(|dL-KfxFCp0CjQjV`WaPW|U z=ev@VyC>IS@{ndzPy||b3z-bj5{Y53ff}|TW8&&*pu#?qs?)#&M`ACfb;%m+qX{Or zb+FNNHU}mz!@!EdrxmP_6eb3Cah!mL0ArL#EA1{nCY-!jL8zzz7wR6wAw(8K|IpW; zUvH*b1wbuRlwlUt;dQhx&pgsvJcUpm67rzkNc}2XbC6mZAgUn?VxO6YYg=M!#e=z8 zjX5ZLyMyz(VdPVyosL0}ULO!Mxu>hh`-MItnGeuQ;wGaU0)gIq3ZD=pDc(Qtk}APj z#HtA;?idVKNF)&0r|&w#l7DbX%b91b2;l2=L8q#}auVdk{RuYn3SMDo1%WW0tD*62 zaIj65Y38;?-~@b82AF!?Nra2;PU)t~qYUhl!GDK3*}%@~N0GQH7zflSpfP-ydOwNe zOK~w((+pCD&>f!b!On);5m+zUBFJtQ)mV^prS3?XgPybC2%2LiE5w+S4B|lP z+_>3$`g=%P{IrN|1Oxz30R{kI`}ZL!r|)RS@8Do;ZD3_=PbBrrP~S@EdsD{V+`!4v z{MSF}j!6odl33rA+$odIMaK%ersg%xMz>JQ^R+!qNq$5S{KgmGN#gAApX*3ib)TDsVVi>4ypIX|Ik4d6E}v z=8+hs9J=k3@Eiga^^O|ESMQB-O6i+BL*~*8coxjGs{tJ9wXjGZ^Vw@j93O<&+bzAH z9+N^ALvDCV<##cGoo5fX;wySGGmbH zHsslio)cxlud=iP2y=nM>v8vBn*hJ0KGyNOy7dr8yJKRh zywBOa4Lhh58y06`5>ESYXqLt8ZM1axd*UEp$wl`APU}C9m1H8-ModG!(wfSUQ%}rT3JD*ud~?WJdM}x>84)Cra!^J9wGs6^G^ze~eV(d&oAfm$ z_gwq4SHe=<#*FN}$5(0d_NumIZYaqs|MjFtI_rJb^+ZO?*XQ*47mzLNSL7~Nq+nw8 zuw0KwWITC43`Vx9eB!0Fx*CN9{ea$xjCvtjeyy>yf!ywxvv6<*h0UNXwkEyRxX{!e$TgHZ^db3r;1qhT)+yt@|_!@ zQG2aT`;lj>qjY`RGfQE?KTt2mn=HmSR>2!E38n8PlFs=1zsEM}AMICb z86Dbx(+`!hl$p=Z)*W~+?_HYp+CJacrCS-Fllz!7E>8*!E(yCh-cWbKc7)mPT6xu= zfKpF3I+p%yFXkMIq!ALiXF89-aV{I6v+^k#!_xwtQ*Nl#V|hKg=nP=fG}5VB8Ki7) z;19!on-iq&Xyo#AowvpA)RRgF?YBdDc$J8*)2Wko;Y?V6XMOCqT(4F#U2n1jg*4=< z8$MfDYL|z731iEKB3WW#kz|c3qh7AXjyZ}wtSg9xA(ou-pLoxF{4qk^KS?!d3J0!! zqE#R9NYGUyy>DEs%^xW;oQ5Cs@fomcrsN}rI2Hg^6y9kwLPF`K3llX00aM_r)c?ay zevlHA#N^8N+AI=)vx?4(=?j^ba^{umw140V#g58#vtnh8i7vRs*UD=lge;T+I zl1byCNr5H%DF58I2(rk%8hQ;zuCXs=sipbQy?Hd;umv4!fav@LE4JQ^>J{aZ=!@Gc~p$JudMy%0{=5QY~S8YVP zaP6gRqfZ0>q9nR3p+Wa8icNyl0Zn4k*bNto-(+o@-D8cd1Ed7`}dN3%wezkFxj_#_K zyV{msOOG;n+qbU=jBZk+&S$GEwJ99zSHGz8hF1`Xxa^&l8aaD8OtnIVsdF0cz=Y)? zP$MEdfKZ}_&#AC)R%E?G)tjrKsa-$KW_-$QL}x$@$NngmX2bHJQG~77D1J%3bGK!- zl!@kh5-uKc@U4I_Er;~epL!gej`kdX>tSXVFP-BH#D-%VJOCpM(-&pOY+b#}lOe)Z z0MP5>av1Sy-dfYFy%?`p`$P|`2yDFlv(8MEsa++Qv5M?7;%NFQK0E`Ggf3@2aUwtBpCoh`D}QLY%QAnJ z%qcf6!;cjOTYyg&2G27K(F8l^RgdV-V!~b$G%E=HP}M*Q*%xJV3}I8UYYd)>*nMvw zemWg`K6Rgy+m|y!8&*}=+`STm(dK-#b%)8nLsL&0<8Zd^|# z;I2gR&e1WUS#v!jX`+cuR;+yi(EiDcRCouW0AHNd?;5WVnC_Vg#4x56#0FOwTH6_p z#GILFF0>bb_tbmMM0|sd7r%l{U!fI0tGza&?65_D7+x9G zf3GA{c|mnO(|>}y(}%>|2>p0X8wRS&Eb0g)rcICIctfD_I9Wd+hKuEqv?gzEZBxG-rG~e!-2hqaR$Y$I@k{rLyCccE}3d)7Fn3EvfsEhA|bnJ374&pZDq&i zr(9#eq(g8^tG??ZzVk(#jU+-ce`|yiQ1dgrJ)$|wk?XLEqv&M+)I*OZ*oBCizjHuT zjZ|mW=<1u$wPhyo#&rIO;qH~pu4e3X;!%BRgmX%?&KZ6tNl386-l#a>ug5nHU2M~{fM2jvY*Py< zbR&^o&!T19G6V-pV@CB)YnEOfmrdPG%QByD?=if99ihLxP6iA8$??wUPWzptC{u5H z38Q|!=IW`)5Gef4+pz|9fIRXt>nlW)XQvUXBO8>)Q=$@gtwb1iEkU4EOWI4`I4DN5 zTC-Pk6N>2%7Hikg?`Poj5lkM0T_i zoCXfXB&}{TG%IB)ENSfI_Xg3=lxYc6-P059>oK;L+vGMy_h{y9soj#&^q5E!pl(Oq zl)oCBi56u;YHkD)d`!iOAhEJ0A^~T;uE9~Yp0{E%G~0q|9f34F!`P56-ZF{2hSaWj zio%9RR%oe~he22r@&j_d(y&nAUL*ayBY4#CWG&gZ8ybs#UcF?8K#HzziqOYM-<`C& z1gD?j)M0bp1w*U>X_b1@ag1Fx=d*wlr zEAcpmI#5LtqcX95LeS=LXlzh*l;^yPl_6MKk)zPuTz_p8ynQ5;oIOUAoPED=+M6Q( z8YR!DUm#$zTM9tbNhxZ4)J0L&Hpn%U>wj3z<=g;`&c_`fGufS!o|1%I_sA&;14bRC z3`BtzpAB-yl!%zM{Aiok8*X%lDNrPiAjBnzHbF0=Ua*3Lxl(zN3Thj2x6nWi^H7Jlwd2fxIvnI-SiC%*j z2~wIWWKT^5fYipo-#HSrr;(RkzzCSt?THVEH2EPvV-4c#Gu4&1X% z<1zTAM7ZM(LuD@ZPS?c30Ur`;2w;PXPVevxT)Ti25o}1JL>MN5i1^(aCF3 zbp>RI?X(CkR9*Hnv!({Ti@FBm;`Ip%e*D2tWEOc62@$n7+gWb;;j}@G()~V)>s}Bd zw+uTg^ibA(gsp*|&m7Vm=heuIF_pIukOedw2b_uO8hEbM4l=aq?E-7M_J`e(x9?{5 zpbgu7h}#>kDQAZL;Q2t?^pv}Y9Zlu=lO5e18twH&G&byq9XszEeXt$V93dQ@Fz2DV zs~zm*L0uB`+o&#{`uVYGXd?)Fv^*9mwLW4)IKoOJ&(8uljK?3J`mdlhJF1aK;#vlc zJdTJc2Q>N*@GfafVw45B03)Ty8qe>Ou*=f#C-!5uiyQ^|6@Dzp9^n-zidp*O`YuZ|GO28 zO0bqi;)fspT0dS2;PLm(&nLLV&&=Ingn(0~SB6Fr^AxPMO(r~y-q2>gRWv7{zYW6c zfiuqR)Xc41A7Eu{V7$-yxYT-opPtqQIJzMVkxU)cV~N0ygub%l9iHT3eQtB>nH0c` zFy}Iwd9vocxlm!P)eh0GwKMZ(fEk92teSi*fezYw3qRF_E-EcCh-&1T)?beW?9Q_+pde8&UW*(avPF4P}M#z*t~KlF~#5TT!&nu z>FAKF8vQl>Zm(G9UKi4kTqHj`Pf@Z@Q(bmZkseb1^;9k*`a9lKXceKX#dMd@ds`t| z2~UPsbn2R0D9Nm~G*oc@(%oYTD&yK)scA?36B7mndR9l*hNg!3?6>CR+tF1;6sr?V zzz8FBrZ@g4F_!O2igIGZcWd zRe_0*{d6cyy9QQ(|Ct~WTM1pC3({5qHahk*M*O}IPE6icikx48VZ?!0Oc^FVoq`}eu~ zpRq0MYHaBA-`b_BVID}|oo-bem76;B2zo7j7yz(9JiSY6JTjKz#+w{9mc{&#x}>E? zSS3mY$_|scfP3Mo_F5x;r>y&Mquy*Q1b3eF^*hg3tap~%?@ASeyodYa=dF&k=ZyWy z3C+&C95h|9TAVM~-8y(&xcy0nvl}6B*)j0FOlSz%+bK-}S4;F?P`j55*+ZO0Ogk7D z5q30zE@Nup4lqQoG`L%n{T?qn9&WC94%>J`KU{gHIq?n_L;75kkKyib;^?yXUx6BO zju%DyU(l!Vj(3stJ>!pMZ*NZFd60%oSAD1JUXG0~2GCXpB0Am(YPyhzQda-e)b^+f zzFaEZdVTJRJXPJo%w z$?T;xq^&(XjmO>0bNGsT|1{1UqGHHhasPC;H!oX52(AQ7h9*^npOIRdQbNrS0X5#5G?L4V}WsAYcpq-+JNXhSl)XbxZ)L@5Q+?wm{GAU z9a7X8hAjAo;4r_eOdZfXGL@YpmT|#qECEcPTQ;nsjIkQ;!0}g?T>Zr*Fg}%BZVA)4 zCAzvWr?M&)KEk`t9eyFi_GlPV9a2kj9G(JgiZadd_&Eb~#DyZ%2Zcvrda_A47G&uW z^6TnBK|th;wHSo8ivpScU?AM5HDu2+ayzExMJc@?4{h-c`!b($ExB`ro#vkl<;=BA z961c*n(4OR!ebT*7UV7sqL;rZ3+Z)BYs<1I|9F|TOKebtLPxahl|ZXxj4j!gjj!3*+iSb5Zni&EKVt$S{0?2>A}d@3PSF3LUu)5 z*Y#a1uD6Y!$=_ghsPrOqX!OcIP`IW};tZzx1)h_~mgl;0=n zdP|Te_7)~R?c9s>W(-d!@nzQyxqakrME{Tn@>0G)kqV<4;{Q?Z-M)E-|IFLTc}WQr z1Qt;u@_dN2kru_9HMtz8MQx1aDYINH&3<+|HA$D#sl3HZ&YsjfQBv~S>4=u z7gA2*X6_cI$2}JYLIq`4NeXTz6Q3zyE717#>RD&M?0Eb|KIyF;xj;+3#DhC-xOj~! z$-Kx#pQ)_$eHE3Zg?V>1z^A%3jW0JBnd@z`kt$p@lch?A9{j6hXxt$(3|b>SZiBxOjA%LsIPii{=o(B`yRJ>OK;z_ELTi8xHX)il z--qJ~RWsZ%9KCNuRNUypn~<2+mQ=O)kd59$Lul?1ev3c&Lq5=M#I{ zJby%%+Top_ocqv!jG6O6;r0Xwb%vL6SP{O(hUf@8riADSI<|y#g`D)`x^vHR4!&HY`#TQMqM`Su}2(C|KOmG`wyK>uh@3;(prdL{2^7T3XFGznp{-sNLLJH@mh* z^vIyicj9yH9(>~I-Ev7p=yndfh}l!;3Q65}K}()(jp|tC;{|Ln1a+2kbctWEX&>Vr zXp5=#pw)@-O6~Q|><8rd0>H-}0Nsc|J6TgCum{XnH2@hFB09FsoZ_ow^Nv@uGgz3# z<6dRDt1>>-!kN58&K1HFrgjTZ^q<>hNI#n8=hP&pKAL4uDcw*J66((I?!pE0fvY6N zu^N=X8lS}(=w$O_jlE(;M9F={-;4R(K5qa=P#ZVW>}J&s$d0?JG8DZJwZcx3{CjLg zJA>q-&=Ekous)vT9J>fbnZYNUtvox|!Rl@e^a6ue_4-_v=(sNB^I1EPtHCFEs!>kK6B@-MS!(B zST${=v9q6q8YdSwk4}@c6cm$`qZ86ipntH8G~51qIlsYQ)+2_Fg1@Y-ztI#aa~tFD_QUxb zU-?g5B}wU@`tnc_l+B^mRogRghXs!7JZS=A;In1|f(1T(+xfIi zvjccLF$`Pkv2w|c5BkSj>>k%`4o6#?ygojkV78%zzz`QFE6nh{(SSJ9NzVdq>^N>X zpg6+8u7i(S>c*i*cO}poo7c9%i^1o&3HmjY!s8Y$5aO(!>u1>-eai0;rK8hVzIh8b zL53WCXO3;=F4_%CxMKRN^;ggC$;YGFTtHtLmX%@MuMxvgn>396~ zEp>V(dbfYjBX^!8CSg>P2c5I~HItbe(dl^Ax#_ldvCh;D+g6-%WD|$@S6}Fvv*eHc zaKxji+OG|_KyMe2D*fhP<3VP0J1gTgs6JZjE{gZ{SO-ryEhh;W237Q0 z{yrDobsM6S`bPMUzr|lT|99m6XDI$RzW4tQ$|@C2RjhBYPliEXFV#M*5G4;Kb|J8E z0IH}-d^S-53kFRZ)ZFrd2%~Sth-6BN?hnMa_PC4gdWyW3q-xFw&L^x>j<^^S$y_3_ zdZxouw%6;^mg#jG@7L!g9Kdw}{w^X9>TOtHgxLLIbfEG^Qf;tD=AXozE6I`XmOF=# zGt$Wl+7L<8^VI-eSK%F%dqXieK^b!Z3yEA$KL}X@>fD9)g@=DGt|=d(9W%8@Y@!{PI@`Nd zyF?Us(0z{*u6|X?D`kKSa}}Q*HP%9BtDEA^buTlI5ihwe)CR%OR46b+>NakH3SDbZmB2X>c8na&$lk zYg$SzY+EXtq2~$Ep_x<~+YVl<-F&_fbayzTnf<7?Y-un3#+T~ahT+eW!l83sofNt; zZY`eKrGqOux)+RMLgGgsJdcA3I$!#zy!f<$zL0udm*?M5w=h$Boj*RUk8mDPVUC1RC8A`@7PgoBIU+xjB7 z25vky+^7k_|1n1&jKNZkBWUu1VCmS}a|6_+*;fdUZAaIR4G!wv=bAZEXBhcjch6WH zdKUr&>z^P%_LIx*M&x{!w|gij?nigT8)Ol3VicXRL0tU}{vp2fi!;QkVc#I38op3O z=q#WtNdN{x)OzmH;)j{cor)DQ;2%m>xMu_KmTisaeCC@~rQwQTfMml7FZ_ zU2AR8yCY_CT$&IAn3n#Acf*VKzJD8-aphMg(12O9cv^AvLQ9>;f!4mjyxq_a%YH2+{~=3TMNE1 z#r3@ynnZ#p?RCkPK36?o{ILiHq^N5`si(T_cKvO9r3^4pKG0AgDEB@_72(2rvU^-; z%&@st2+HjP%H)u50t81p>(McL{`dTq6u-{JM|d=G1&h-mtjc2{W0%*xuZVlJpUSP-1=U6@5Q#g(|nTVN0icr-sdD~DWR=s}`$#=Wa zt5?|$`5`=TWZevaY9J9fV#Wh~Fw@G~0vP?V#Pd=|nMpSmA>bs`j2e{)(827mU7rxM zJ@ku%Xqhq!H)It~yXm=)6XaPk=$Rpk*4i4*aSBZe+h*M%w6?3&0>>|>GHL>^e4zR!o%aGzUn40SR+TdN%=Dbn zsRfXzGcH#vjc-}7v6yRhl{V5PhE-r~)dnmNz=sDt?*1knNZ>xI5&vBwrosF#qRL-Y z;{W)4W&cO0XMKy?{^d`Xh(2B?j0ioji~G~p5NQJyD6vouyoFE9w@_R#SGZ1DR4GnN z{b=sJ^8>2mq3W;*u2HeCaKiCzK+yD!^i6QhTU5npwO+C~A#5spF?;iuOE>o&p3m1C zmT$_fH8v+5u^~q^ic#pQN_VYvU>6iv$tqx#Sulc%|S7f zshYrWq7IXCiGd~J(^5B1nGMV$)lo6FCTm1LshfcOrGc?HW7g>pV%#4lFbnt#94&Rg{%Zbg;Rh?deMeOP(du*)HryI zCdhO$3|SeaWK<>(jSi%qst${Z(q@{cYz7NA^QO}eZ$K@%YQ^Dt4CXzmvx~lLG{ef8 zyckIVSufk>9^e_O7*w2z>Q$8me4T~NQDq=&F}Ogo#v1u$0xJV~>YS%mLVYqEf~g*j zGkY#anOI9{(f4^v21OvYG<(u}UM!-k;ziH%GOVU1`$0VuO@Uw2N{$7&5MYjTE?Er) zr?oZAc~Xc==KZx-pmoh9KiF_JKU7u0#b_}!dWgC>^fmbVOjuiP2FMq5OD9+4TKg^2 z>y6s|sQhI`=fC<>BnQYV433-b+jBi+N6unz%6EQR%{8L#=4sktI>*3KhX+qAS>+K#}y5KnJ8YuOuzG(Ea5;$*1P$-9Z+V4guyJ#s) zRPH(JPN;Es;H72%c8}(U)CEN}Xm>HMn{n!d(=r*YP0qo*^APwwU5YTTeHKy#85Xj< zEboiH=$~uIVMPg!qbx~0S=g&LZ*IyTJG$hTN zv%2>XF``@S9lnLPC?|myt#P)%7?%e_j*aU4TbTyxO|3!h%=Udp;THL+^oPp<6;TLlIOa$&xeTG_a*dbRDy+(&n1T=MU z+|G5{2UprrhN^AqODLo$9Z2h(3^wtdVIoSk@}wPajVgIoZipRft}^L)2Y@mu;X-F{LUw|s7AQD-0!otW#W9M@A~08`o%W;Bq-SOQavG*e-sy8) zwtaucR0+64B&Pm++-m56MQ$@+t{_)7l-|`1kT~1s!swfc4D9chbawUt`RUOdoxU|j z$NE$4{Ysr@2Qu|K8pD37Yv&}>{_I5N49a@0<@rGHEs}t zwh_+9T0oh@ptMbjy*kbz<&3>LGR-GNsT8{x1g{!S&V7{5tPYX(GF>6qZh>O&F)%_I zkPE-pYo3dayjNQAG+xrI&yMZy590FA1unQ*k*Zfm#f9Z5GljOHBj-B83KNIP1a?<^1vOhDJkma0o- zs(TP=@e&s6fRrU(R}{7eHL*(AElZ&80>9;wqj{|1YQG=o2Le-m!UzUd?Xrn&qd8SJ0mmEYtW;t(;ncW_j6 zGWh4y|KMK^s+=p#%fWxjXo434N`MY<8W`tNH-aM6x{@o?D3GZM&+6t4V3I*3fZd{a z0&D}DI?AQl{W*?|*%M^D5{E>V%;=-r&uQ>*e)cqVY52|F{ptA*`!iS=VKS6y4iRP6 zKUA!qpElT5vZvN}U5k-IpeNOr6KF`-)lN1r^c@HnT#RlZbi(;yuvm9t-Noh5AfRxL@j5dU-X37(?S)hZhRDbf5cbhDO5nSX@WtApyp` zT$5IZ*4*)h8wShkPI45stQH2Y7yD*CX^Dh@B%1MJSEn@++D$AV^ttKXZdQMU`rxiR z+M#45Z2+{N#uR-hhS&HAMFK@lYBWOzU^Xs-BlqQDyN4HwRtP2$kks@UhAr@wlJii%Rq?qy25?Egs z*a&iAr^rbJWlv+pYAVUq9lor}#Cm|D$_ev2d2Ko}`8kuP(ljz$nv3OCDc7zQp|j6W zbS6949zRvj`bhbO(LN3}Pq=$Ld3a_*9r_24u_n)1)}-gRq?I6pdHPYHgIsn$#XQi~ z%&m_&nnO9BKy;G%e~fa7i9WH#MEDNQ8WCXhqqI+oeE5R7hLZT_?7RWVzEGZNz4*Po ze&*a<^Q*ze72}UM&$c%FuuEIN?EQ@mnILwyt;%wV-MV+|d%>=;3f0(P46;Hwo|Wr0 z>&FS9CCb{?+lDpJMs`95)C$oOQ}BSQEv0Dor%-Qj0@kqlIAm1-qSY3FCO2j$br7_w zlpRfAWz3>Gh~5`Uh?ER?@?r0cXjD0WnTx6^AOFii;oqM?|M9QjHd*GK3WwA}``?dK15`ZvG>_nB2pSTGc{n2hYT6QF^+&;(0c`{)*u*X7L_ zaxqyvVm$^VX!0YdpSNS~reC+(uRqF2o>jqIJQkC&X>r8|mBHvLaduM^Mh|OI60<;G zDHx@&jUfV>cYj5+fAqvv(XSmc(nd@WhIDvpj~C#jhZ6@M3cWF2HywB1yJv2#=qoY| zIiaxLsSQa7w;4YE?7y&U&e6Yp+2m(sb5q4AZkKtey{904rT08pJpanm->Z75IdvW^ z!kVBy|CIUZn)G}92_MgoLgHa?LZJDp_JTbAEq8>6a2&uKPF&G!;?xQ*+{TmNB1H)_ z-~m@CTxDry_-rOM2xwJg{fcZ41YQDh{DeI$4!m8c;6XtFkFyf`fOsREJ`q+Bf4nS~ zKDYs4AE7Gugv?X)tu4<-M8ag{`4pfQ14z<(8MYQ4u*fl*DCpq66+Q1-gxNCQ!c$me zyTrmi7{W-MGP!&S-_qJ%9+e08_9`wWGG{i5yLJ;8qbt-n_0*Q371<^u@tdz|;>fPW zE=&q~;wVD_4IQ^^jyYX;2shIMiYdvIpIYRT>&I@^{kL9Ka2ECG>^l>Ae!GTn{r~o= z|I9=J#wNe)zYRqGZ7Q->L{dfewyC$ZYcLaoNormZ3*gfM=da*{heC)&46{yTS!t10 zn_o0qUbQOs$>YuY>YHi|NG^NQG<_@jD&WnZcW^NTC#mhVE7rXlZ=2>mZkx{bc=~+2 z{zVH=Xs0`*K9QAgq9cOtfQ^BHh-yr=qX8hmW*0~uCup89IJMvWy%#yt_nz@6dTS)L{O3vXye< zW4zUNb6d|Tx`XIVwMMgqnyk?c;Kv`#%F0m^<$9X!@}rI##T{iXFC?(ui{;>_9Din8 z7;(754q!Jx(~sb!6+6Lf*l{fqD7GW*v{>3wp+)@wq2abADBK!kI8To}7zooF%}g-z zJ1-1lp-lQI6w^bov9EfhpxRI}`$PTpJI3uo@ZAV729JJ2Hs68{r$C0U=!d$Bm+s(p z8Kgc(Ixf4KrN%_jjJjTx5`&`Ak*Il%!}D_V)GM1WF!k$rDJ-SudXd_Xhl#NWnET&e-P!rH~*nNZTzxj$?^oo3VWc-Ay^`Phze3(Ft!aNW-f_ zeMy&BfNCP^-FvFzR&rh!w(pP5;z1$MsY9Voozmpa&A}>|a{eu}>^2s)So>&kmi#7$ zJS_-DVT3Yi(z+ruKbffNu`c}s`Uo`ORtNpUHa6Q&@a%I%I;lm@ea+IbCLK)IQ~)JY zp`kdQ>R#J*i&Ljer3uz$m2&Un9?W=Ue|hHv?xlM`I&*-M;2{@so--0OAiraN1TLra z>EYQu#)Q@UszfJj&?kr%RraFyi*eG+HD_(!AWB;hPgB5Gd-#VDRxxv*VWMY0hI|t- zR=;TL%EKEg*oet7GtmkM zgH^y*1bfJ*af(_*S1^PWqBVVbejFU&#m`_69IwO!aRW>Rcp~+7w^ptyu>}WFYUf;) zZrgs;EIN9$Immu`$umY%$I)5INSb}aV-GDmPp!d_g_>Ar(^GcOY%2M)Vd7gY9llJR zLGm*MY+qLzQ+(Whs8-=ty2l)G9#82H*7!eo|B6B$q%ak6eCN%j?{SI9|K$u3)ORoz zw{bAGaWHrMb|X^!UL~_J{jO?l^}lI^|7jIn^p{n%JUq9{tC|{GM5Az3SrrPkuCt_W zq#u0JfDw{`wAq`tAJmq~sz`D_P-8qr>kmms>I|);7Tn zLl^n*Ga7l=U)bQmgnSo5r_&#Pc=eXm~W75X9Cyy0WDO|fbSn5 zLgpFAF4fa90T-KyR4%%iOq6$6BNs@3ZV<~B;7V=u zdlB8$lpe`w-LoS;0NXFFu@;^^bc?t@r3^XTe*+0;o2dt&>eMQeDit(SfDxYxuA$uS z**)HYK7j!vJVRNfrcokVc@&(ke5kJzvi};Lyl7@$!`~HM$T!`O`~MQ1k~ZH??fQr zNP)33uBWYnTntKRUT*5lu&8*{fv>syNgxVzEa=qcKQ86Vem%Lpae2LM=TvcJLs?`=o9%5Mh#k*_7zQD|U7;A%=xo^_4+nX{~b1NJ6@ z*=55;+!BIj1nI+)TA$fv-OvydVQB=KK zrGWLUS_Chm$&yoljugU=PLudtJ2+tM(xj|E>Nk?c{-RD$sGYNyE|i%yw>9gPItE{ zD|BS=M>V^#m8r?-3swQofD8j$h-xkg=F+KM%IvcnIvc)y zl?R%u48Jeq7E*26fqtLe_b=9NC_z|axW#$e0adI#r(Zsui)txQ&!}`;;Z%q?y2Kn! zXzFNe+g7+>>`9S0K1rmd)B_QVMD?syc3e0)X*y6(RYH#AEM9u?V^E0GHlAAR)E^4- zjKD+0K=JKtf5DxqXSQ!j?#2^ZcQoG5^^T+JaJa3GdFeqIkm&)dj76WaqGukR-*&`13ls8lU2ayVIR%;79HYAr5aEhtYa&0}l}eAw~qKjUyz4v*At z?})QplY`3cWB6rl7MI5mZx&#%I0^iJm3;+J9?RA(!JXjl?(XgmA-D#2cY-^?g1c*Q z3GVLh!8Jhe;QqecbMK#XIJxKMb=6dcs?1vbb?@ov-raj`hnYO92y8pv@>RVr=9Y-F zv`BK)9R6!m4Pfllu4uy0WBL+ZaUFFzbZZtI@J8{OoQ^wL-b$!FpGT)jYS-=vf~b-@ zIiWs7j~U2yI=G5;okQz%gh6}tckV5wN;QDbnu|5%%I(#)8Q#)wTq8YYt$#f9=id;D zJbC=CaLUyDIPNOiDcV9+=|$LE9v2;Qz;?L+lG{|g&iW9TI1k2_H;WmGH6L4tN1WL+ zYfSVWq(Z_~u~U=g!RkS|YYlWpKfZV!X%(^I3gpV%HZ_{QglPSy0q8V+WCC2opX&d@eG2BB#(5*H!JlUzl$DayI5_J-n zF@q*Fc-nlp%Yt;$A$i4CJ_N8vyM5fNN`N(CN53^f?rtya=p^MJem>JF2BEG|lW|E) zxf)|L|H3Oh7mo=9?P|Y~|6K`B3>T)Gw`0ESP9R`yKv}g|+qux(nPnU(kQ&&x_JcYg9+6`=; z-EI_wS~l{T3K~8}8K>%Ke`PY!kNt415_x?^3QOvX(QUpW&$LXKdeZM-pCI#%EZ@ta zv(q-(xXIwvV-6~(Jic?8<7ain4itN>7#AqKsR2y(MHMPeL)+f+v9o8Nu~p4ve*!d3 z{Lg*NRTZsi;!{QJknvtI&QtQM_9Cu%1QcD0f!Fz+UH4O#8=hvzS+^(e{iG|Kt7C#u zKYk7{LFc+9Il>d6)blAY-9nMd(Ff0;AKUo3B0_^J&ESV@4UP8PO0no7G6Gp_;Z;YnzW4T-mCE6ZfBy(Y zXOq^Of&?3#Ra?khzc7IJT3!%IKK8P(N$ST47Mr=Gv@4c!>?dQ-&uZihAL1R<_(#T8Y`Ih~soL6fi_hQmI%IJ5qN995<{<@_ z;^N8AGQE+?7#W~6X>p|t<4@aYC$-9R^}&&pLo+%Ykeo46-*Yc(%9>X>eZpb8(_p{6 zwZzYvbi%^F@)-}5%d_z^;sRDhjqIRVL3U3yK0{Q|6z!PxGp?|>!%i(!aQODnKUHsk^tpeB<0Qt7`ZBlzRIxZMWR+|+ z3A}zyRZ%0Ck~SNNov~mN{#niO**=qc(faGz`qM16H+s;Uf`OD1{?LlH!K!+&5xO%6 z5J80-41C{6)j8`nFvDaeSaCu_f`lB z_Y+|LdJX=YYhYP32M556^^Z9MU}ybL6NL15ZTV?kfCFfpt*Pw5FpHp#2|ccrz#zoO zhs=+jQI4fk*H0CpG?{fpaSCmXzU8bB`;kCLB8T{_3t>H&DWj0q0b9B+f$WG=e*89l zzUE)b9a#aWsEpgnJqjVQETpp~R7gn)CZd$1B8=F*tl+(iPH@s9jQtE33$dBDOOr=% ziOpR8R|1eLI?Rn*d+^;_U#d%bi$|#obe0(-HdB;K>=Y=mg{~jTA_WpChe8QquhF`N z>hJ}uV+pH`l_@d>%^KQNm*$QNJ(lufH>zv9M`f+C-y*;hAH(=h;kp@eL=qPBeXrAo zE7my75EYlFB30h9sdt*Poc9)2sNP9@K&4O7QVPQ^m$e>lqzz)IFJWpYrpJs)Fcq|P z5^(gnntu!+oujqGpqgY_o0V&HL72uOF#13i+ngg*YvPcqpk)Hoecl$dx>C4JE4DWp z-V%>N7P-}xWv%9Z73nn|6~^?w$5`V^xSQbZceV<_UMM&ijOoe{Y^<@3mLSq_alz8t zr>hXX;zTs&k*igKAen1t1{pj94zFB;AcqFwV)j#Q#Y8>hYF_&AZ?*ar1u%((E2EfZ zcRsy@s%C0({v=?8oP=DML`QsPgzw3|9|C22Y>;=|=LHSm7~+wQyI|;^WLG0_NSfrf zamq!5%EzdQ&6|aTP2>X=Z^Jl=w6VHEZ@=}n+@yeu^ke2Yurrkg9up3g$0SI8_O-WQu$bCsKc(juv|H;vz6}%7ONww zKF%!83W6zO%0X(1c#BM}2l^ddrAu^*`9g&1>P6m%x{gYRB)}U`40r>6YmWSH(|6Ic zH~QNgxlH*;4jHg;tJiKia;`$n_F9L~M{GiYW*sPmMq(s^OPOKm^sYbBK(BB9dOY`0 z{0!=03qe*Sf`rcp5Co=~pfQyqx|umPHj?a6;PUnO>EZGb!pE(YJgNr{j;s2+nNV(K zDi#@IJ|To~Zw)vqGnFwb2}7a2j%YNYxe2qxLk)VWJIux$BC^oII=xv-_}h@)Vkrg1kpKokCmX({u=lSR|u znu_fA0PhezjAW{#Gu0Mdhe8F4`!0K|lEy+<1v;$ijSP~A9w%q5-4Ft|(l7UqdtKao zs|6~~nmNYS>fc?Nc=yzcvWNp~B0sB5ForO5SsN(z=0uXxl&DQsg|Y?(zS)T|X``&8 z*|^p?~S!vk8 zg>$B{oW}%rYkgXepmz;iqCKY{R@%@1rcjuCt}%Mia@d8Vz5D@LOSCbM{%JU#cmIp! z^{4a<3m%-p@JZ~qg)Szb-S)k{jv92lqB(C&KL(jr?+#ES5=pUH$(;CO9#RvDdErmW z3(|f{_)dcmF-p*D%qUa^yYngNP&Dh2gq5hr4J!B5IrJ?ODsw@*!0p6Fm|(ebRT%l) z#)l22@;4b9RDHl1ys$M2qFc;4BCG-lp2CN?Ob~Be^2wQJ+#Yz}LP#8fmtR%o7DYzoo1%4g4D+=HonK7b!3nvL0f1=oQp93dPMTsrjZRI)HX-T}ApZ%B#B;`s? z9Kng{|G?yw7rxo(T<* z1+O`)GNRmXq3uc(4SLX?fPG{w*}xDCn=iYo2+;5~vhWUV#e5e=Yfn4BoS@3SrrvV9 zrM-dPU;%~+3&>(f3sr$Rcf4>@nUGG*vZ~qnxJznDz0irB(wcgtyATPd&gSuX^QK@+ z)7MGgxj!RZkRnMSS&ypR94FC$;_>?8*{Q110XDZ)L);&SA8n>72s1#?6gL>gydPs` zM4;ert4-PBGB@5E` zBaWT=CJUEYV^kV%@M#3(E8>g8Eg|PXg`D`;K8(u{?}W`23?JgtNcXkUxrH}@H_4qN zw_Pr@g%;CKkgP(`CG6VTIS4ZZ`C22{LO{tGi6+uPvvHkBFK|S6WO{zo1MeK$P zUBe}-)3d{55lM}mDVoU@oGtPQ+a<=wwDol}o=o1z*)-~N!6t09du$t~%MlhM9B5~r zy|zs^LmEF#yWpXZq!+Nt{M;bE%Q8z7L8QJDLie^5MKW|I1jo}p)YW(S#oLf(sWn~* zII>pocNM5#Z+-n2|495>?H?*oyr0!SJIl(}q-?r`Q;Jbqqr4*_G8I7agO298VUr9x z8ZcHdCMSK)ZO@Yr@c0P3{`#GVVdZ{zZ$WTO zuvO4ukug&& ze#AopTVY3$B>c3p8z^Yyo8eJ+(@FqyDWlR;uxy0JnSe`gevLF`+ZN6OltYr>oN(ZV z>76nIiVoll$rDNkck6_eh%po^u16tD)JXcii|#Nn(7=R9mA45jz>v}S%DeMc(%1h> zoT2BlF9OQ080gInWJ3)bO9j$ z`h6OqF0NL4D3Kz?PkE8nh;oxWqz?<3_!TlN_%qy*T7soZ>Pqik?hWWuya>T$55#G9 zxJv=G&=Tm4!|p1#!!hsf*uQe}zWTKJg`hkuj?ADST2MX6fl_HIDL7w`5Dw1Btays1 zz*aRwd&>4*H%Ji2bt-IQE$>sbCcI1Poble0wL`LAhedGRZp>%>X6J?>2F*j>`BX|P zMiO%!VFtr_OV!eodgp-WgcA-S=kMQ^zihVAZc!vdx*YikuDyZdHlpy@Y3i!r%JI85$-udM6|7*?VnJ!R)3Qfm4mMm~Z#cvNrGUy|i0u zb|(7WsYawjBK0u1>@lLhMn}@X>gyDlx|SMXQo|yzkg-!wIcqfGrA!|t<3NC2k` zq;po50dzvvHD>_mG~>W0iecTf@3-)<$PM5W@^yMcu@U;)(^eu@e4jAX7~6@XrSbIE zVG6v2miWY^g8bu5YH$c2QDdLkg2pU8xHnh`EUNT+g->Q8Tp4arax&1$?CH($1W&*} zW&)FQ>k5aCim$`Ph<9Zt?=%|pz&EX@_@$;3lQT~+;EoD(ho|^nSZDh*M0Z&&@9T+e zHYJ;xB*~UcF^*7a_T)9iV5}VTYKda8n*~PSy@>h7c(mH~2AH@qz{LMQCb+-enMhX} z2k0B1JQ+6`?Q3Lx&(*CBQOnLBcq;%&Nf<*$CX2<`8MS9c5zA!QEbUz1;|(Ua%CiuL zF2TZ>@t7NKQ->O#!;0s;`tf$veXYgq^SgG>2iU9tCm5&^&B_aXA{+fqKVQ*S9=58y zddWqy1lc$Y@VdB?E~_B5w#so`r552qhPR649;@bf63_V@wgb!>=ij=%ptnsq&zl8^ zQ|U^aWCRR3TnoKxj0m0QL2QHM%_LNJ(%x6aK?IGlO=TUoS%7YRcY{!j(oPcUq{HP=eR1>0o^(KFl-}WdxGRjsT);K8sGCkK0qVe{xI`# z@f+_kTYmLbOTxRv@wm2TNBKrl+&B>=VaZbc(H`WWLQhT=5rPtHf)#B$Q6m1f8We^)f6ylbO=t?6Y;{?&VL|j$VXyGV!v8eceRk zl>yOWPbk%^wv1t63Zd8X^Ck#12$*|yv`v{OA@2;-5Mj5sk#ptfzeX(PrCaFgn{3*hau`-a+nZhuJxO;Tis51VVeKAwFML#hF9g26NjfzLs8~RiM_MFl1mgDOU z=ywk!Qocatj1Q1yPNB|FW>!dwh=aJxgb~P%%7(Uydq&aSyi?&b@QCBiA8aP%!nY@c z&R|AF@8}p7o`&~>xq9C&X6%!FAsK8gGhnZ$TY06$7_s%r*o;3Y7?CenJUXo#V-Oag z)T$d-V-_O;H)VzTM&v8^Uk7hmR8v0)fMquWHs6?jXYl^pdM#dY?T5XpX z*J&pnyJ<^n-d<0@wm|)2SW9e73u8IvTbRx?Gqfy_$*LI_Ir9NZt#(2T+?^AorOv$j zcsk+t<#!Z!eC|>!x&#l%**sSAX~vFU0|S<;-ei}&j}BQ#ekRB-;c9~vPDIdL5r{~O zMiO3g0&m-O^gB}<$S#lCRxX@c3g}Yv*l)Hh+S^my28*fGImrl<-nbEpOw-BZ;WTHL zgHoq&ftG|~ouV<>grxRO6Z%{!O+j`Cw_4~BIzrjpkdA5jH40{1kDy|pEq#7`$^m*? zX@HxvW`e}$O$mJvm+65Oc4j7W@iVe)rF&-}R>KKz>rF&*Qi3%F0*tz!vNtl@m8L9= zyW3%|X}0KsW&!W<@tRNM-R>~~QHz?__kgnA(G`jWOMiEaFjLzCdRrqzKlP1vYLG`Y zh6_knD3=9$weMn4tBD|5=3a9{sOowXHu(z5y^RYrxJK z|L>TUvbDuO?3=YJ55N5}Kj0lC(PI*Te0>%eLNWLnawD54geX5>8AT(oT6dmAacj>o zC`Bgj-RV0m3Dl2N=w3e0>wWWG5!mcal`Xu<(1=2$b{k(;kC(2~+B}a(w;xaHPk^@V zGzDR|pt%?(1xwNxV!O6`JLCM!MnvpbLoHzKziegT_2LLWAi4}UHIo6uegj#WTQLet z9Dbjyr{8NAk+$(YCw~_@Az9N|iqsliRYtR7Q|#ONIV|BZ7VKcW$phH9`ZAlnMTW&9 zIBqXYuv*YY?g*cJRb(bXG}ts-t0*|HXId4fpnI>$9A?+BTy*FG8f8iRRKYRd*VF_$ zoo$qc+A(d#Lx0@`ck>tt5c$L1y7MWohMnZd$HX++I9sHoj5VXZRZkrq`v@t?dfvC} z>0h!c4HSb8%DyeF#zeU@rJL2uhZ^8dt(s+7FNHJeY!TZJtyViS>a$~XoPOhHsdRH* zwW+S*rIgW0qSPzE6w`P$Jv^5dsyT6zoby;@z=^yWLG^x;e557RnndY>ph!qCF;ov$ ztSW1h3@x{zm*IMRx|3lRWeI3znjpbS-0*IL4LwwkWyPF1CRpQK|s42dJ{ddA#BDDqio-Y+mF-XcP-z4bi zAhfXa2=>F0*b;F0ftEPm&O+exD~=W^qjtv&>|%(4q#H=wbA>7QorDK4X3~bqeeXv3 zV1Q<>_Fyo!$)fD`fd@(7(%6o-^x?&+s=)jjbQ2^XpgyYq6`}ISX#B?{I$a&cRcW?X zhx(i&HWq{=8pxlA2w~7521v-~lu1M>4wL~hDA-j(F2;9ICMg+6;Zx2G)ulp7j;^O_ zQJIRUWQam(*@?bYiRTKR<;l_Is^*frjr-Dj3(fuZtK{Sn8F;d*t*t{|_lnlJ#e=hx zT9?&_n?__2mN5CRQ}B1*w-2Ix_=CF@SdX-cPjdJN+u4d-N4ir*AJn&S(jCpTxiAms zzI5v(&#_#YrKR?B?d~ge1j*g<2yI1kp`Lx>8Qb;aq1$HOX4cpuN{2ti!2dXF#`AG{ zp<iD=Z#qN-yEwLwE7%8w8&LB<&6{WO$#MB-|?aEc@S1a zt%_p3OA|kE&Hs47Y8`bdbt_ua{-L??&}uW zmwE7X4Y%A2wp-WFYPP_F5uw^?&f zH%NCcbw_LKx!c!bMyOBrHDK1Wzzc5n7A7C)QrTj_Go#Kz7%+y^nONjnnM1o5Sw(0n zxU&@41(?-faq?qC^kO&H301%|F9U-Qm(EGd3}MYTFdO+SY8%fCMTPMU3}bY7ML1e8 zrdOF?E~1uT)v?UX(XUlEIUg3*UzuT^g@QAxEkMb#N#q0*;r zF6ACHP{ML*{Q{M;+^4I#5bh#c)xDGaIqWc#ka=0fh*_Hlu%wt1rBv$B z%80@8%MhIwa0Zw$1`D;Uj1Bq`lsdI^g_18yZ9XUz2-u6&{?Syd zHGEh-3~HH-vO<)_2^r|&$(q7wG{@Q~un=3)Nm``&2T99L(P+|aFtu1sTy+|gwL*{z z)WoC4rsxoWhz0H$rG|EwhDT z0zcOAod_k_Ql&Y`YV!#&Mjq{2ln|;LMuF$-G#jX_2~oNioTHb4GqFatn@?_KgsA7T z(ouy$cGKa!m}6$=C1Wmb;*O2p*@g?wi-}X`v|QA4bNDU*4(y8*jZy-Ku)S3iBN(0r ztfLyPLfEPqj6EV}xope=?b0Nyf*~vDz-H-Te@B`{ib?~F<*(MmG+8zoYS77$O*3vayg#1kkKN+Bu9J9;Soev<%2S&J zr8*_PKV4|?RVfb#SfNQ;TZC$8*9~@GR%xFl1 z3MD?%`1PxxupvVO>2w#8*zV<-!m&Lis&B>)pHahPQ@I_;rY~Z$1+!4V1jde&L8y0! zha7@F+rOENF{~0$+a~oId0R|_!PhO=8)$>LcO)ca6YeOQs?ZG;`4O`x=Pd??Bl?Qf zgkaNj7X5@3_==zlQ-u6?omteA!_e-6gfDtw6CBnP2o1wo-7U!Y@89rU1HFb|bIr!I z=qIz=AW(}L^m z=I9RiS{DRtTYS6jsnvt1zs)W;kSVFOK|WMyZ@dxs+8{*W9-aTmS79J4R{Cis>EIqS zw+~gJqwz)(!z>)KDyhS{lM*xQ-8mNvo$A=IwGu+iS564tgX`|MeEuis!aN-=7!L&e zhNs;g1MBqDyx{y@AI&{_)+-?EEg|5C*!=OgD#$>HklRVU+R``HYZZq5{F9C0KKo!d z$bE2XC(G=I^YUxYST+Hk>0T;JP_iAvCObcrPV1Eau865w6d^Wh&B?^#h2@J#!M2xp zLGAxB^i}4D2^?RayxFqBgnZ-t`j+~zVqr+9Cz9Rqe%1a)c*keP#r54AaR2*TH^}7j zmJ48DN);^{7+5|+GmbvY2v#qJy>?$B(lRlS#kyodlxA&Qj#9-y4s&|eq$5} zgI;4u$cZWKWj`VU%UY#SH2M$8?PjO-B-rNPMr=8d=-D(iLW#{RWJ}@5#Z#EK=2(&LvfW&{P4_jsDr^^rg9w#B7h`mBwdL9y)Ni;= zd$jFDxnW7n-&ptjnk#<0zmNNt{;_30vbQW!5CQ7SuEjR1be!vxvO53!30iOermrU1 zXhXaen8=4Q(574KO_h$e$^1khO&tQL59=)Dc^8iPxz8+tC3`G$w|yUzkGd%Wg4(3u zJ<&7r^HAaEfG?F8?2I64j4kPpsNQk7qBJa9_hFT;*j;A%H%;QI@QWqJaiOl=;u>G8 zG`5Ow4K5ifd=OS|7F;EFc1+GzLld0RCQxG>Fn?~5Wl5VHJ=$DeR-2zwBgzSrQsGG0 zBqrILuB+_SgLxh~S~^QNHWW(2P;Z?d!Rd1lnEM=z23xPzyrbO_L0k43zruDkrJO*D zlzN(peBMLji`xfgYUirul-7c#3t(*=x6A^KSU-L|$(0pp9A*43#=Q!cu%9ZHP!$J| zSk8k=Z8cl811Vvn(4p8xx+EdKQV(sjC4_mEvlWeuIfwEVcF2LiC{H!oW)LSW=0ul| zT?$5PCc(pf-zKzUH`p7I7coVvCK;Dv-3_c?%~bPz`#ehbfrSrFf{RAz0I5e*W1S)kTW{0gf5X2v2k=S=W{>pr44tQ?o` zih8gE29VGR_SL~YJtcA)lRLozPg!<3Mh(`Hp)5{bclb)reTScXzJ>7{?i^yR@{(^% z#=$BYXPIX%fhgsofP-T`3b<5#V(TTS)^$vlhV&Kn=(LXOTAADIR1v8UqmW5c`n`S% zC8SOW$e?>&0dwKD%Jt{+67PfCLnqX0{8K^(q_^^2#puPYPkJsyXWMa~?V?p5{flYi z-1!uqI2x%puPG)r7b8y+Pc0Z5C%aA6`Q1_?W9k!YbiVVJVJwGLL?)P0M&vo{^IgEE zrX3eTgrJl_AeXYmiciYX9OP?NPN%-7Ji%z3U`-iXX=T~OI0M=ek|5IvIsvXM$%S&v zKw{`Kj(JVc+Pp^?vLKEyoycfnk)Hd>et78P^Z*{#rBY~_>V7>{gtB$0G99nbNBt+r zyXvEg_2=#jjK+YX1A>cj5NsFz9rjB_LB%hhx4-2I73gr~CW_5pD=H|e`?#CQ2)p4& z^v?Dlxm-_j6bO5~eeYFZGjW3@AGkIxY=XB*{*ciH#mjQ`dgppNk4&AbaRYKKY-1CT z>)>?+ME)AcCM7RRZQsH5)db7y!&jY-qHp%Ex9N|wKbN$!86i>_LzaD=f4JFc6Dp(a z%z>%=q(sXlJ=w$y^|tcTy@j%AP`v1n0oAt&XC|1kA`|#jsW(gwI0vi3a_QtKcL+yh z1Y=`IRzhiUvKeZXH6>>TDej)?t_V8Z7;WrZ_7@?Z=HRhtXY+{hlY?x|;7=1L($?t3 z6R$8cmez~LXopZ^mH9=^tEeAhJV!rGGOK@sN_Zc-vmEr;=&?OBEN)8aI4G&g&gdOb zfRLZ~dVk3194pd;=W|Z*R|t{}Evk&jw?JzVERk%JNBXbMDX82q~|bv%!2%wFP9;~-H?={C1sZ( zuDvY5?M8gGX*DyN?nru)UvdL|Rr&mXzgZ;H<^KYvzIlet!aeFM@I?JduKj=!(+ zM7`37KYhd*^MrKID^Y1}*sZ#6akDBJyKna%xK%vLlBqzDxjQ3}jx8PBOmXkvf@B{@ zc#J;~wQ<6{B;``j+B!#7s$zONYdXunbuKvl@zvaWq;`v2&iCNF2=V9Kl|77-mpCp= z2$SxhcN=pZ?V{GW;t6s)?-cNPAyTi&8O0QMGo#DcdRl#+px!h3ayc*(VOGR95*Anj zL0YaiVN2mifzZ){X+fl`Z^P=_(W@=*cIe~BJd&n@HD@;lRmu8cx7K8}wPbIK)GjF> zQGQ2h#21o6b2FZI1sPl}9_(~R|2lE^h}UyM5A0bJQk2~Vj*O)l-4WC4$KZ>nVZS|d zZv?`~2{uPYkc?254B9**q6tS|>We?uJ&wK3KIww|zzSuj>ncI4D~K z1Y6irVFE{?D-|R{!rLhZxAhs+Ka9*-(ltIUgC;snNek4_5xhO}@+r9Sl*5=7ztnXO zAVZLm$Kdh&rqEtdxxrE9hw`aXW1&sTE%aJ%3VL3*<7oWyz|--A^qvV3!FHBu9B-Jj z4itF)3dufc&2%V_pZsjUnN=;s2B9<^Zc83>tzo)a_Q$!B9jTjS->%_h`ZtQPz@{@z z5xg~s*cz`Tj!ls3-hxgnX}LDGQp$t7#d3E}>HtLa12z&06$xEQfu#k=(4h{+p%aCg zzeudlLc$=MVT+|43#CXUtRR%h5nMchy}EJ;n7oHfTq6wN6PoalAy+S~2l}wK;qg9o zcf#dX>ke;z^13l%bwm4tZcU1RTXnDhf$K3q-cK576+TCwgHl&?9w>>_(1Gxt@jXln zt3-Qxo3ITr&sw1wP%}B>J$Jy>^-SpO#3e=7iZrXCa2!N69GDlD{97|S*og)3hG)Lk zuqxK|PkkhxV$FP45%z*1Z?(LVy+ruMkZx|(@1R(0CoS6`7FWfr4-diailmq&Q#ehn zc)b&*&Ub;7HRtFVjL%((d$)M=^6BV@Kiusmnr1_2&&aEGBpbK7OWs;+(`tRLF8x?n zfKJB3tB^F~N`_ak3^exe_3{=aP)3tuuK2a-IriHcWv&+u7p z_yXsd6kyLV@k=(QoSs=NRiKNYZ>%4wAF;2#iu1p^!6>MZUPd;=2LY~l2ydrx10b#OSAlltILY%OKTp{e{ zzNogSk~SJBqi<_wRa#JqBW8Ok=6vb%?#H(hG}Dv98{JST5^SSh>_GQ@UK-0J`6l#E za}X#ud0W?cp-NQE@jAx>NUv65U~%YYS%BC0Cr$5|2_A)0tW;(nqoGJUHG5R`!-{1M-4T{<^pOE!Dvyuu1x7?Wt#YIgq zA$Vwj`St+M#ZxJXXGkepIF6`xL&XPu^qiFlZcX+@fOAdQ9d(h{^xCiAWJ0Ixp~3&E z(WwdT$O$7ez?pw>Jf{`!T-205_zJv+y~$w@XmQ;CiL8d*-x_z~0@vo4|3xUermJ;Q z9KgxjkN8Vh)xZ2xhX0N@{~@^d@BLoYFW%Uys83=`15+YZ%KecmWXjVV2}YbjBonSh zVOwOfI7^gvlC~Pq$QDHMQ6_Pd10OV{q_Zai^Yg({5XysuT`3}~3K*8u>a2FLBQ%#_YT6$4&6(?ZGwDE*C-p8>bM?hj*XOIoj@C!L5) zH1y!~wZ^dX5N&xExrKV>rEJJjkJDq*$K>qMi`Lrq08l4bQW~!Fbxb>m4qMHu6weTiV6_9(a*mZ23kr9AM#gCGE zBXg8#m8{ad@214=#w0>ylE7qL$4`xm!**E@pw484-VddzN}DK2qg&W~?%hcv3lNHx zg(CE<2)N=p!7->aJ4=1*eB%fbAGJcY65f3=cKF4WOoCgVelH$qh0NpIka5J-6+sY* zBg<5!R=I*5hk*CR@$rY6a8M%yX%o@D%{q1Jn=8wAZ;;}ol>xFv5nXvjFggCQ_>N2} zXHiC~pCFG*oEy!h_sqF$^NJIpQzXhtRU`LR0yU;MqrYUG0#iFW4mbHe)zN&4*Wf)G zV6(WGOq~OpEoq##E{rC?!)8ygAaAaA0^`<8kXmf%uIFfNHAE|{AuZd!HW9C^4$xW; zmIcO#ti!~)YlIU4sH(h&s6}PH-wSGtDOZ+%H2gAO(%2Ppdec9IMViuwwWW)qnqblH9xe1cPQ@C zS4W|atjGDGKKQAQlPUVUi1OvGC*Gh2i&gkh0up%u-9ECa7(Iw}k~0>r*WciZyRC%l z7NX3)9WBXK{mS|=IK5mxc{M}IrjOxBMzFbK59VI9k8Yr$V4X_^wI#R^~RFcme2)l!%kvUa zJ{zpM;;=mz&>jLvON5j>*cOVt1$0LWiV>x)g)KKZnhn=%1|2E|TWNfRQ&n?vZxQh* zG+YEIf33h%!tyVBPj>|K!EB{JZU{+k`N9c@x_wxD7z~eFVw%AyU9htoH6hmo0`%kb z55c#c80D%0^*6y|9xdLG$n4Hn%62KIp`Md9Jhyp8)%wkB8<%RlPEwC&FL z;hrH(yRr(Ke$%TZ09J=gGMC3L?bR2F4ZU!}pu)*8@l(d9{v^^(j>y+GF*nGran5*M z{pl5ig0CVsG1etMB8qlF4MDFRkLAg4N=l{Sc*F>K_^AZQc{dSXkvonBI)qEN1*U&? zKqMr?Wu)q9c>U~CZUG+-ImNrU#c`bS?RpvVgWXqSsOJrCK#HNIJ+k_1Iq^QNr(j|~ z-rz67Lf?}jj^9Ik@VIMBU2tN{Ts>-O%5f?=T^LGl-?iC%vfx{}PaoP7#^EH{6HP!( zG%3S1oaiR;OmlKhLy@yLNns`9K?60Zg7~NyT0JF(!$jPrm^m_?rxt~|J2)*P6tdTU z25JT~k4RH9b_1H3-y?X4=;6mrBxu$6lsb@xddPGKA*6O`Cc^>Ul`f9c&$SHFhHN!* zjj=(Jb`P}R%5X@cC%+1ICCRh1^G&u548#+3NpYTVr54^SbFhjTuO-yf&s%r4VIU!lE!j(JzHSc9zRD_fw@CP0pkL(WX6 zn+}LarmQP9ZGF9So^+jr<(LGLlOxGiCsI^SnuC{xE$S;DA+|z+cUk=j^0ipB(WTZ} zR0osv{abBd)HOjc(SAV&pcP@37SLnsbtADj?bT#cPZq|?W1Ar;4Vg5m!l{@{TA~|g zXYOeU`#h-rT@(#msh%%kH>D=`aN}2Rysez?E@R6|@SB(_gS0}HC>83pE`obNA9vsH zSu^r>6W-FSxJA}?oTuH>-y9!pQg|*<7J$09tH=nq4GTx+5($$+IGlO^bptmxy#=)e zuz^beIPpUB_YK^?eb@gu(D%pJJwj3QUk6<3>S>RN^0iO|DbTZNheFX?-jskc5}Nho zf&1GCbE^maIL$?i=nXwi)^?NiK`Khb6A*kmen^*(BI%Kw&Uv4H;<3ib-2UwG{7M&* zn$qyi8wD9cKOuxWhRmFupwLuFn!G5Vj6PZ#GCNJLlTQuQ?bqAYd7Eva5YR~OBbIim zf(6yXS4pei1Bz4w4rrB6Ke~gKYErlC=l9sm*Zp_vwJe7<+N&PaZe|~kYVO%uChefr%G4-=0eSPS{HNf=vB;p~ z5b9O1R?WirAZqcdRn9wtct>$FU2T8p=fSp;E^P~zR!^C!)WHe=9N$5@DHk6(L|7s@ zcXQ6NM9Q~fan1q-u8{ez;RADoIqwkf4|6LfsMZK6h{ZUGYo>vD%JpY<@w;oIN-*sK zxp4@+d{zxe>Z-pH#_)%|d(AC`fa!@Jq)5K8hd71!;CEG|ZI{I2XI`X~n|ae;B!q{I zJDa#T+fRviR&wAN^Sl{z8Ar1LQOF&$rDs18h0{yMh^pZ#hG?c5OL8v07qRZ-Lj5(0 zjFY(S4La&`3IjOT%Jqx4z~08($iVS;M10d@q~*H=Py)xnKt(+G-*o33c7S3bJ8cmwgj45` zU|b7xCoozC!-7CPOR194J-m9N*g`30ToBo!Io?m>T)S{CusNZx0J^Hu6hOmvv;0~W zFHRYJgyRhP1sM_AQ%pkD!X-dPu_>)`8HunR4_v$4T78~R<})-@K2LBt03PBLnjHzuYY)AK?>0TJe9 zmmOjwSL%CTaLYvYlJ~|w?vc*R+$@vEAYghtgGhZ2LyF+UdOn+v^yvD9R%xbU$fUjK{{VQ4VL&&UqAFa>CZuX4kX zJ)njewLWfKXneB+r}Y$`ezzwDoRT3r{9(@=I3-z>8tT)n3whDyi(r*lAnxQJefj_x z-8lc=r!Vua{b}v;LT)oXW>~6Q03~RAp~R}TZq9sGbeUBMS)?ZrJqiu|E&ZE)uN1uL zXcAj3#aEz zzbcCF)+;Hia#OGBvOatkPQfE{*RtBlO1QFVhi+3q0HeuFa*p+Dj)#8Mq9yGtIx%0A znV5EmN(j!&b%kNz4`Vr-)mX_?$ng&M^a6loFO(G3SA!~eBUEY!{~>C|Ht1Q4cw)X5~dPiEYQJNg?B2&P>bU7N(#e5cr8qc7A{a7J9cdMcRx)N|?;$L~O|E)p~ zIC}oi3iLZKb>|@=ApsDAfa_<$0Nm<3nOPdr+8Y@dnb|u2S<7CUmTGKd{G57JR*JTo zb&?qvusnu{i^`v+g=n|Q6)iINjWk4myhio zh{63hNTme0e*Dy*<%dM<|2-xvC?_cE`^0GdevP+yk60CEBRBL4&k_-?qm2|78N0)&;#41P+Nykv+vLmWf+ zkyROxWr6T745@(j`3HtSreiPRX5{EsvH>tdfQ#`jaQo>02nVRIiM^47gA5>hw~_UK zawfcl_X=S^)B!Z*4!~r7gaiC6UjVPtFKP?Wl(uwo00_B=nOPbM8W;c=Wc94|{x6OF zO9IfM_bXa}23G(y_+O191pk)=;`Vxg)S1cv(MJgzDAFKN{UU~_} z%?zN8*#Jo~{)z`d_iH?B2S+_i%l~G>=`f7~B!D;d3NV-u{Hb<8K-jGRg!k*(<-0L7 zsQ@|%2(Wel^vIuzr^GMOWNb|SYj3|yF#i+nwe&B+ekXfIk3!SlN&ABRR~^VhsTNQ~Ul1L3{b|%TzHxA5Q=K z!~eUP}1?BpS8+8_}QY|6c_CU&6oCqW=kR zVEs?bVP8tH|Ag(f`6t*vdl_D0z7zodiJ9#5Pndrkq5W?o<4dXCpQwS(zk&MS zg?C?8|D}59Pa^F1zf1H-^ZZ*&^d-Sdsm7lK2%f(|@DIX`FPUBny8UEQ^!*K{-;#HG z$@x;I>nG=#|8H>qlW5mVs+W2nKdF$ze}n2D)IVM_z0_6s$%OjxH<m`wgo9*;X&(bbjKKXZ{BMKS%AnY`2$T4L`|@a{f2j zuP0eA_n&`azOMZn=D(Wb@4L}2>-p06{S$Mq<$q)T(>Lm+Kk+B>ar@tqf8V?Kw_otW z$Mut^tMhL>FQ=V?&-hEf%TJp4?*E(8{WmYnf9m`npUa<}CO>>GKg(AD*njiD ZypaY2tb=~UE;0eV1Nd9`dw%@&{{Y^v%e4Ri literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..14521a8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Oct 04 23:29:00 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/maven-plugin-rpm/NOTICE.txt b/maven-plugin-rpm/NOTICE.txt new file mode 100644 index 0000000..87335d8 --- /dev/null +++ b/maven-plugin-rpm/NOTICE.txt @@ -0,0 +1,13 @@ +This is a derived work of + +https://github.com/spaulg/redlinerpm-maven-plugin + +licensed under Apache Software License 2.0 + +Copyright 2014 Simon Paulger spaulger@codezen.co.uk + +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. \ No newline at end of file diff --git a/maven-plugin-rpm/build.gradle b/maven-plugin-rpm/build.gradle new file mode 100644 index 0000000..0986742 --- /dev/null +++ b/maven-plugin-rpm/build.gradle @@ -0,0 +1,72 @@ +configurations { + mavenEmbedder +} + +dependencies { + compile project(':rpm-core') + compile "org.mvel:mvel2:${project.property('mvel.version')}" + compile "org.apache.ant:ant:${project.property('ant.version')}" + compileOnly "org.apache.maven:maven-core:${project.property('maven.version')}" + compileOnly "org.apache.maven:maven-plugin-api:${project.property('maven.version')}" + compileOnly 'org.apache.maven.plugin-tools:maven-plugin-annotations:3.5' + testCompile "org.apache.maven:maven-core:${project.property('maven.version')}" + testCompile "org.apache.maven:maven-plugin-api:${project.property('maven.version')}" + testCompile 'org.apache.maven.plugin-tools:maven-plugin-annotations:3.5' + mavenEmbedder "org.apache.maven:maven-embedder:${project.property('maven.version')}" + mavenEmbedder 'org.slf4j:slf4j-simple:1.7.5' + mavenEmbedder 'org.apache.maven.wagon:wagon-http:2.12:shaded' + mavenEmbedder 'org.apache.maven.wagon:wagon-provider-api:2.12' + mavenEmbedder 'org.eclipse.aether:aether-connector-basic:1.0.2.v20150114' + mavenEmbedder 'org.eclipse.aether:aether-transport-wagon:1.0.2.v20150114' +} + +test { + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + } + systemProperty 'project.build.testOutputDirectory', project.buildDir.path + "/resources/test" +} + +install.repositories.mavenInstaller.pom.with { + groupId = project.group + artifactId = project.name + version = project.version + packaging = 'maven-plugin' +} + +task generatePluginDescriptor(type: JavaExec, dependsOn: compileJava) { + def pomFile = file("$buildDir/pom.xml") + def pluginDescriptorFile = new File(file(project.compileJava.destinationDir), 'META-INF/maven/plugin.xml') + def directory = buildDir.canonicalPath + def outputDirectory = compileJava.destinationDir.canonicalPath + inputs.files project.compileJava.outputs.files + outputs.file pluginDescriptorFile + classpath = configurations.mavenEmbedder + main = 'org.apache.maven.cli.MavenCli' + systemProperties['maven.multiModuleProjectDirectory'] = projectDir + args = [ + '--update-snapshots', + '--errors', + '--batch-mode', + '--settings', '../config/maven/repo-settings.xml', + '--file', "${buildDir}/pom.xml", + "org.apache.maven.plugins:maven-plugin-plugin:3.5:descriptor" + ] + doFirst { + install.repositories.mavenInstaller.pom.withXml { + asNode().appendNode('build').with { + appendNode('directory', directory) + appendNode('outputDirectory', outputDirectory) + } + }.writeTo(pomFile) + assert pomFile.file, "${pomFile.canonicalPath}: was not generated" + logger.info("POM is generated in ${pomFile.canonicalPath}") + } + doLast { + assert pluginDescriptorFile.file, "${pluginDescriptorFile.canonicalPath}: was not generated" + logger.info("Maven plugin descriptor generated in ${pluginDescriptorFile.canonicalPath}") + } +} + +project.jar.dependsOn(generatePluginDescriptor) diff --git a/maven-plugin-rpm/config/checkstyle/checkstyle.xml b/maven-plugin-rpm/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..55e59d2 --- /dev/null +++ b/maven-plugin-rpm/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmBaseObject.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmBaseObject.java new file mode 100644 index 0000000..9fffd2b --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmBaseObject.java @@ -0,0 +1,95 @@ +package org.xbib.maven.plugin.rpm; + +/** + * + */ +public abstract class RpmBaseObject { + /** + * Destination permissions. + */ + private int permissions = 0; + + /** + * Destination file owner. + */ + private String owner = null; + + /** + * Destination file group. + */ + private String group = null; + + protected abstract RpmPackage getPackage(); + + /** + * Set permissions. + * + * @param permissions permissions + */ + public void setPermissions(int permissions) { + this.permissions = permissions; + } + + /** + * Get permissions, or the default setting if not set. + * + * @return permissions + */ + public int getPermissionsOrDefault() { + if (0 == permissions) { + return getPackage().getMojo().getDefaultFileMode(); + } else { + return permissions; + } + } + + /** + * Set owner. + * + * @param owner owner + */ + public void setOwner(String owner) { + if (null != owner && owner.equals("")) { + owner = null; + } + this.owner = owner; + } + + /** + * Get owner, or the default setting if not set. + * + * @return owner + */ + public String getOwnerOrDefault() { + if (null == this.owner) { + return this.getPackage().getMojo().getDefaultOwner(); + } else { + return this.owner; + } + } + + /** + * Set group. + * + * @param group group + */ + public void setGroup(String group) { + if (null != group && group.equals("")) { + group = null; + } + this.group = group; + } + + /** + * Get group, or the default setting if not set. + * + * @return group + */ + public String getGroupOrDefault() { + if (null == this.group) { + return this.getPackage().getMojo().getDefaultGroup(); + } else { + return this.group; + } + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmLink.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmLink.java new file mode 100644 index 0000000..8a15153 --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmLink.java @@ -0,0 +1,68 @@ +package org.xbib.maven.plugin.rpm; + +/** + * + */ +public class RpmLink extends RpmBaseObject { + + private RpmPackage rpmPackage; + + private String path; + + private String target; + + /** + * Get associated RPM package. + * + * @return RPM package + */ + @Override + public RpmPackage getPackage() { + return this.rpmPackage; + } + + /** + * Set associated RPM package. + * + * @param rpmPackage RPM package + */ + public void setPackage(RpmPackage rpmPackage) { + this.rpmPackage = rpmPackage; + } + + /** + * Get symlink path. + * + * @return symlink path + */ + public String getPath() { + return path; + } + + /** + * Set symlink path. + * + * @param path symlink path + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Get symlink target. + * + * @return symlink target + */ + public String getTarget() { + return target; + } + + /** + * Set symlink target. + * + * @param target symlink target + */ + public void setTarget(String target) { + this.target = target; + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackage.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackage.java new file mode 100644 index 0000000..cacdffa --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackage.java @@ -0,0 +1,1336 @@ +package org.xbib.maven.plugin.rpm; + +import org.apache.maven.plugin.logging.Log; +import org.xbib.maven.plugin.rpm.mojo.RpmMojo; +import org.xbib.rpm.RpmBuilder; +import org.xbib.rpm.exception.RpmException; +import org.xbib.rpm.exception.SigningKeyNotFoundException; +import org.xbib.rpm.exception.UnknownArchitectureException; +import org.xbib.rpm.exception.UnknownOperatingSystemException; +import org.xbib.rpm.format.Flags; +import org.xbib.rpm.lead.Architecture; +import org.xbib.rpm.lead.Os; +import org.xbib.rpm.lead.PackageType; +import org.xbib.rpm.payload.CompressionType; +import org.xbib.rpm.security.HashAlgo; +import org.xbib.rpm.trigger.Trigger; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * RPM package. + */ +public class RpmPackage { + /** + * Plugin mojo currently in use. + */ + private RpmMojo mojo = null; + + /** + * Package name. + */ + private String name = null; + + /** + * Package version. + */ + private String version = null; + + /** + * Project version. + */ + private String projectVersion = null; + + /** + * Package release. + */ + private String release = null; + + /** + * Final name of RPM artifact. + */ + private String finalName = null; + + /** + * Package url. + */ + private String url = null; + + /** + * Package group. + */ + private String group = null; + + /** + * Package license. + */ + private String license = null; + + /** + * Package summary. + */ + private String summary = null; + + /** + * Package description. + */ + private String description = null; + + /** + * Package distribution. + */ + private String distribution = null; + + /** + * Build architecture. + * Defaults to detected architecture of build environment if non given + */ + private Architecture architecture = null; + + /** + * Build operating system. + * Defaults to detected operating system of build environment if non given + */ + private Os operatingSystem = null; + + /** + * Build host name. + * Defaults to hostname of build server provided by hostname service + */ + private String buildHostName = null; + + /** + * Packager of RPM. + */ + private String packager = null; + + /** + * Source RPM Name. + */ + private String sourceRpm = null; + + /** + * Attach the artifact. + */ + private boolean attach = true; + + /** + * Artifact classifier. + */ + private String classifier = null; + + /** + * Pre transaction event hook script file. + */ + private Path preTransactionScriptPath = null; + + /** + * Pre transaction event hook script program. + */ + private String preTransactionProgram = null; + + /** + * Pre install event hook script file. + */ + private Path preInstallScriptPath = null; + + /** + * Pre install event hook program. + */ + private String preInstallProgram = null; + + /** + * Post install event hook script file. + */ + private Path postInstallScriptPath = null; + + /** + * Post install event hook program. + */ + private String postInstallProgram = null; + + /** + * Pre uninstall event hook script file. + */ + private Path preUninstallScriptPath = null; + + /** + * Pre uninstall event hook script program. + */ + private String preUninstallProgram = null; + + /** + * Post uninstall event hook script file. + */ + private Path postUninstallScriptPath = null; + + /** + * Post uninstall event hook program. + */ + private String postUninstallProgram = null; + + /** + * Post transaction event hook script file. + */ + private Path postTransactionScriptPath = null; + + /** + * Post transaction event hook program. + */ + private String postTransactionProgram = null; + + /** + * List of triggers. + */ + private List triggers = new ArrayList<>(); + + /** + * Signing key. + */ + private String signingKey = null; + + /** + * Signing key pass phrase. + */ + private String signingKeyPassPhrase = null; + + /** + * Signing key id. + */ + private Long signingKeyId = null; + + /** + * Prefixes. + */ + private List prefixes = new ArrayList<>(); + + /** + * Builtins. + */ + private List builtins = new ArrayList<>(); + + /** + * Dependencies. + */ + private List dependencies = new ArrayList<>(); + + /** + * Obsoletes. + */ + private List obsoletes = new ArrayList<>(); + + /** + * Conflicts. + */ + private List conflicts = new ArrayList<>(); + + /** + * Links. + */ + private List links = new ArrayList<>(); + + /** + * Package file matching rules. + */ + private List rules = new ArrayList<>(); + + /** + * Get mojo in use by Maven. + * + * @return Current maven mojo + */ + public RpmMojo getMojo() { + return mojo; + } + + /** + * Set mojo in use by Maven. + * + * @param mojo Current maven mojo + */ + public void setMojo(RpmMojo mojo) { + this.mojo = mojo; + } + + /** + * Get package name. + * + * @return Package name + */ + public String getName() { + if (null == name) { + name = getMojo().getProjectArtifactId(); + } + + return name; + } + + /** + * Set package name. + * + * @param name Package name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get package version. + * + * @return Package version + */ + public String getVersion() { + if (null == version) { + version = sanitiseVersion(getMojo().getProjectVersion()); + } + + return version; + } + + /** + * Set package version. + * + * @param version Package version + */ + public void setVersion(String version) { + if (null != version && version.equals("")) { + version = null; + } + projectVersion = version; + this.version = sanitiseVersion(version); + } + + /** + * Get project version. + * + * @return Project version + */ + public String getProjectVersion() { + if (null == projectVersion) { + projectVersion = getMojo().getProjectVersion(); + } + + return projectVersion; + } + + /** + * Sanitise the version number for use in packaging. + * + * @param version Un-sanitised version + * @return Sanitised version number + */ + private String sanitiseVersion(String version) { + if (null != version && !version.replaceAll("[a-zA-Z0-9\\.]", "").equals("")) { + version = version.replaceAll("-", "."); + version = version.replaceAll("[^a-zA-Z0-9\\.]", ""); + } + return version; + } + + /** + * Get package release. + * + * @return Package release + */ + public String getRelease() { + if (null == release) { + release = Long.toString(System.currentTimeMillis() / 1000); + } + return release; + } + + /** + * Set package release. + * + * @param release Package release + */ + public void setRelease(String release) { + this.release = release; + } + + /** + * Get final name of the RPM artifact. + * If a final name is not set, the final name will default to {name}-{version}-{release}.{architecture}.rpm + * + * @return Final name of RPM artifact + */ + public String getFinalName() { + if (finalName == null) { + finalName = String.format("%s-%s-%s.%s.rpm", + getName(), getVersion(), getRelease(), + getArchitecture().toString().toLowerCase()); + } + return finalName; + } + + /** + * Set final name of RPM artifact. + * + * @param finalName Final name of RPM artifact + */ + public void setFinalName(String finalName) { + this.finalName = finalName; + } + + /** + * Get package dependencies. + * + * @return Package dependencies + */ + public List getDependencies() { + return dependencies; + } + + /** + * Set package dependencies. + * + * @param dependencies Package dependencies + */ + public void setDependencies(List dependencies) { + this.dependencies = dependencies; + } + + /** + * Get package obsoletes. + * + * @return Package obsoletes + */ + public List getObsoletes() { + return obsoletes; + } + + /** + * Set package obsoletes. + * + * @param obsoletes Package obsoletes + */ + public void setObsoletes(List obsoletes) { + this.obsoletes = obsoletes; + } + + /** + * Get package conflicts. + * + * @return Package conflicts + */ + public List getConflicts() { + return conflicts; + } + + /** + * Set package conflicts. + * + * @param conflicts Package conflicts + */ + public void setConflicts(List conflicts) { + this.conflicts = conflicts; + } + + /** + * Get package links. + * + * @return Package links + */ + public List getLinks() { + return links; + } + + /** + * Set package links. + * + * @param links Package links + */ + public void setLinks(List links) { + if (null != links) { + for (RpmLink link : links) { + link.setPackage(this); + } + } + this.links = links; + } + + /** + * Get package url. + * + * @return Package url + */ + public String getUrl() { + if (null == url) { + url = getMojo().getProjectUrl(); + } + return url; + } + + /** + * Set package url. + * + * @param url Package url + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Get package group. + * + * @return Package group + */ + public String getGroup() { + return group; + } + + /** + * Set package group. + * + * @param group Package group + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * Get package license. + * + * @return Package license + */ + public String getLicense() { + if (null == license) { + license = getMojo().getCollapsedProjectLicense(); + } + return license; + } + + /** + * Set package license. + * + * @param license Package license + */ + public void setLicense(String license) { + this.license = license; + } + + /** + * Get package summary. + * + * @return Package summary + */ + public String getSummary() { + return summary; + } + + /** + * Set package summary. + * + * @param summary Package summary + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Get package description. + * + * @return Package description + */ + public String getDescription() { + return description; + } + + /** + * Set package description. + * + * @param description Package description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Get package distribution. + * + * @return Package distribution + */ + public String getDistribution() { + return distribution; + } + + /** + * Set package distribution. + * + * @param distribution Package distribution + */ + public void setDistribution(String distribution) { + this.distribution = distribution; + } + + /** + * Get package architecture. + * + * @return Package architecture + */ + public Architecture getArchitecture() { + if (null == architecture) { + architecture = Architecture.NOARCH; + } + return architecture; + } + + /** + * Set package architecture. + * + * @param architecture Package architecture + * @throws UnknownArchitectureException The architecture supplied is not recognised. + */ + public void setArchitecture(String architecture) throws UnknownArchitectureException { + if (null == architecture || architecture.equals("")) { + throw new UnknownArchitectureException(architecture); + } + architecture = architecture.toUpperCase(); + try { + this.architecture = Architecture.valueOf(architecture); + } catch (IllegalArgumentException ex) { + throw new UnknownArchitectureException(architecture, ex); + } + } + + /** + * Get package operating system. + * Defaults to LINUX if not set. + * + * @return Package operating system + */ + public Os getOperatingSystem() { + if (null == operatingSystem) { + operatingSystem = Os.LINUX; + } + return operatingSystem; + } + + /** + * Set package operating system. + * + * @param operatingSystem Package operating system + * @throws UnknownOperatingSystemException The operating system supplied is not recognised. + */ + public void setOperatingSystem(String operatingSystem) throws UnknownOperatingSystemException { + if (null == operatingSystem || operatingSystem.equals("")) { + throw new UnknownOperatingSystemException(operatingSystem); + } + operatingSystem = operatingSystem.toUpperCase(); + try { + this.operatingSystem = Os.valueOf(operatingSystem); + } catch (IllegalArgumentException ex) { + throw new UnknownOperatingSystemException(operatingSystem, ex); + } + } + + /** + * Get package build host name. + * If one is not supplied, the default hostname of the machine running the build is used. + * + * @return Package build host name + * @throws UnknownHostException The build host could not be retrieved automatically. + */ + public String getBuildHostName() throws UnknownHostException { + if (null == buildHostName) { + buildHostName = InetAddress.getLocalHost().getHostName(); + } + return buildHostName; + } + + /** + * Set package build host name. + * + * @param buildHostName Package build host name + */ + public void setBuildHostName(String buildHostName) { + this.buildHostName = buildHostName; + } + + /** + * Get packager. + * + * @return Packager + */ + public String getPackager() { + return packager; + } + + /** + * Set package packager. + * + * @param packager Package packager + */ + public void setPackager(String packager) { + this.packager = packager; + } + + /** + * Get source RPM name. + * + * @return sourceRpm + */ + public String getSourceRpm() { + return sourceRpm; + } + + /** + * Set source RPM name. + * + * @param sourceRpm Package sourceRpm + */ + public void setSourceRpm(String sourceRpm) { + this.sourceRpm = sourceRpm; + } + + /** + * Get artifact attachment. + * + * @return Artifact is attached + */ + public boolean isAttach() { + return attach; + } + + /** + * Set artifact attachment. + * + * @param attach Artifact attachment + */ + public void setAttach(boolean attach) { + this.attach = attach; + } + + /** + * Get the artifact classifier. + * + * @return Artifact classifier + */ + public String getClassifier() { + return classifier; + } + + /** + * Set the artifact classifier. + * + * @param classifier Artifact classifier + */ + public void setClassifier(String classifier) { + this.classifier = classifier; + } + + /** + * Get pre transactions script path. + * + * @return Pre transaction script path + */ + public Path getPreTransactionScriptPath() { + return preTransactionScriptPath; + } + + /** + * Set pre transaction script path. + * + * @param preTransactionScriptPath Pre transaction script path + */ + public void setPreTransactionScriptPath(Path preTransactionScriptPath) { + this.preTransactionScriptPath = preTransactionScriptPath; + } + + /** + * Get pre transaction program. + * + * @return Pre transaction program + */ + public String getPreTransactionProgram() { + return preTransactionProgram; + } + + /** + * Set pre transaction program. + * + * @param preTransactionProgram Pre transaction program + */ + public void setPreTransactionProgram(String preTransactionProgram) { + this.preTransactionProgram = preTransactionProgram; + } + + /** + * Get pre install script path. + * + * @return Pre install script path + */ + public Path getPreInstallScriptPath() { + return preInstallScriptPath; + } + + /** + * Set pre install script path. + * + * @param preInstallScriptPath Pre install script path + */ + public void setPreInstallScriptPath(Path preInstallScriptPath) { + this.preInstallScriptPath = preInstallScriptPath; + } + + /** + * Get pre install program. + * + * @return Pre install program + */ + public String getPreInstallProgram() { + return preInstallProgram; + } + + /** + * Set pre install program. + * + * @param preInstallProgram Pre install program + */ + public void setPreInstallProgram(String preInstallProgram) { + this.preInstallProgram = preInstallProgram; + } + + /** + * Get post install script path. + * + * @return Post install script path + */ + public Path getPostInstallScriptPath() { + return postInstallScriptPath; + } + + /** + * Set post install script path. + * + * @param postInstallScriptPath Post install script path + */ + public void setPostInstallScriptPath(Path postInstallScriptPath) { + this.postInstallScriptPath = postInstallScriptPath; + } + + /** + * Get post install program. + * + * @return Post install program + */ + public String getPostInstallProgram() { + return postInstallProgram; + } + + /** + * Set post install program. + * + * @param postInstallProgram Post install program + */ + public void setPostInstallProgram(String postInstallProgram) { + this.postInstallProgram = postInstallProgram; + } + + /** + * Get pre uninstall script path. + * + * @return Pre uninstall script path + */ + public Path getPreUninstallScriptPath() { + return preUninstallScriptPath; + } + + /** + * Set pre uninstall script path. + * + * @param preUninstallScriptPath Pre uninstall script path + */ + public void setPreUninstallScriptPath(Path preUninstallScriptPath) { + this.preUninstallScriptPath = preUninstallScriptPath; + } + + /** + * Get pre uninstall program. + * + * @return Pre uninstall program + */ + public String getPreUninstallProgram() { + return preUninstallProgram; + } + + /** + * Set pre uninstall program. + * + * @param preUninstallProgram Pre uninstall program + */ + public void setPreUninstallProgram(String preUninstallProgram) { + this.preUninstallProgram = preUninstallProgram; + } + + /** + * Get post uninstall script path. + * + * @return Post uninstall script path + */ + public Path getPostUninstallScriptPath() { + return postUninstallScriptPath; + } + + /** + * Set post uninstall script path. + * + * @param postUninstallScriptPath Post uninstall script path + */ + public void setPostUninstallScriptPath(Path postUninstallScriptPath) { + this.postUninstallScriptPath = postUninstallScriptPath; + } + + /** + * Get post uninstall program. + * + * @return Post uninstall program + */ + public String getPostUninstallProgram() { + return postUninstallProgram; + } + + /** + * Set post uninstall program. + * + * @param postUninstallProgram Post uninstall program + */ + public void setPostUninstallProgram(String postUninstallProgram) { + this.postUninstallProgram = postUninstallProgram; + } + + /** + * Get post transaction script path. + * + * @return Post transaction script path + */ + public Path getPostTransactionScriptPath() { + return postTransactionScriptPath; + } + + /** + * Set post transaction script path. + * + * @param postTransactionScriptPath Post transaction script path + */ + public void setPostTransactionScriptPath(Path postTransactionScriptPath) { + this.postTransactionScriptPath = postTransactionScriptPath; + } + + /** + * Get post transaction program. + * + * @return Post transaction program + */ + public String getPostTransactionProgram() { + return postTransactionProgram; + } + + /** + * Set post transaction program. + * + * @param postTransactionProgram Post transaction program + */ + public void setPostTransactionProgram(String postTransactionProgram) { + this.postTransactionProgram = postTransactionProgram; + } + + /** + * Get triggers. + * + * @return Triggers + */ + public List getTriggers() { + return triggers; + } + + /** + * Set triggers. + * + * @param triggers Triggers + */ + public void setTriggers(List triggers) { + this.triggers = triggers; + } + + /** + * Get signing key. + * + * @return Signing key + */ + public String getSigningKey() { + return signingKey; + } + + /** + * Set signing key. + * + * @param signingKey Signing key + */ + public void setSigningKey(String signingKey) { + this.signingKey = signingKey; + } + + /** + * Get signing key id. + * + * @return Signing key id + */ + public Long getSigningKeyId() { + return signingKeyId; + } + + /** + * Set signing key id. + * + * @param signingKeyId Signing key id + */ + public void setSigningKeyId(Long signingKeyId) { + this.signingKeyId = signingKeyId; + } + + /** + * Get signing key pass phrase. + * + * @return Signing key pass phrase + */ + public String getSigningKeyPassPhrase() { + return signingKeyPassPhrase; + } + + /** + * Set signing key pass phrase. + * + * @param signingKeyPassPhrase Signing key pass phrase + */ + public void setSigningKeyPassPhrase(String signingKeyPassPhrase) { + this.signingKeyPassPhrase = signingKeyPassPhrase; + } + + /** + * Get list of prefixes. + * + * @return List of prefixes + */ + public List getPrefixes() { + return prefixes; + } + + /** + * Set list of prefixes. + * + * @param prefixes List of Prefixes + */ + public void setPrefixes(List prefixes) { + if (null == prefixes) { + prefixes = new ArrayList(); + } + this.prefixes = prefixes; + } + + /** + * Get list of builtin directories. + * + * @return List of builtin directories + */ + public List getBuiltins() { + return builtins; + } + + /** + * Set list of builtin directories. + * + * @param builtins List of Builtin Directories + */ + public void setBuiltins(List builtins) { + if (null == builtins) { + builtins = new ArrayList<>(); + } + this.builtins = builtins; + } + + /** + * Get package rules. + * + * @return Package rules + */ + public List getRules() { + return rules; + } + + /** + * Set package rules. + * + * @param rules Package rules + */ + public void setRules(List rules) { + if (null != rules) { + for (RpmPackageRule rpmPackageRule : rules) { + rpmPackageRule.setPackage(this); + } + } + this.rules = rules; + } + + /** + * Get the logger. + * + * @return logger + */ + public Log getLog() { + return getMojo().getLog(); + } + + /** + * Build the package. + * + * @return Files included within the package + * @throws IOException if writing the package fails + * @throws RpmException if building the package fails + */ + public Set build() throws IOException, RpmException { + Set fileList = new HashSet<>(); + String buildDirectory = getMojo().getBuildDirectory(); + getLog().debug("Creating RPM archive"); + RpmBuilder builder = new RpmBuilder(HashAlgo.SHA256, CompressionType.GZIP); + builder.setType(PackageType.BINARY); + getLog().debug("Setting package information"); + builder.setPackage(getName(), getVersion(), getRelease()); + builder.setPlatform(getArchitecture(), getOperatingSystem()); + builder.setGroup(getGroup()); + builder.setLicense(getLicense()); + builder.setSummary(getSummary()); + builder.setDescription(getDescription()); + builder.setDistribution(getDistribution()); + builder.setBuildHost(getBuildHostName()); + builder.setPackager(getPackager()); + builder.setUrl(getUrl()); + builder.setPrefixes(getPrefixes().toArray(new String[0])); + builder.setSourceRpm(getSourceRpm()); + for (String builtin : getBuiltins()) { + builder.addBuiltinDirectory(builtin); + } + for (RpmPackageAssociation dependency : getDependencies()) { + if (null != dependency.getName()) { + if (dependency.isVersionRange()) { + if (null != dependency.getMinVersion()) { + builder.addDependency(dependency.getName(), Flags.GREATER | Flags.EQUAL, dependency.getMinVersion()); + } + if (null != dependency.getMaxVersion()) { + builder.addDependency(dependency.getName(), Flags.LESS, dependency.getMaxVersion()); + } + } else { + if (null != dependency.getVersion()) { + builder.addDependency(dependency.getName(), Flags.EQUAL, dependency.getVersion()); + } else { + builder.addDependency(dependency.getName(), 0, ""); + } + } + } + } + for (RpmPackageAssociation obsolete : getObsoletes()) { + if (null != obsolete.getName()) { + if (obsolete.isVersionRange()) { + if (null != obsolete.getMinVersion()) { + builder.addObsoletes(obsolete.getName(), Flags.GREATER | Flags.EQUAL, obsolete.getMinVersion()); + } + + if (null != obsolete.getMaxVersion()) { + builder.addObsoletes(obsolete.getName(), Flags.LESS, obsolete.getMaxVersion()); + } + } else { + if (null != obsolete.getVersion()) { + builder.addObsoletes(obsolete.getName(), Flags.EQUAL, obsolete.getVersion()); + } else { + builder.addObsoletes(obsolete.getName(), 0, ""); + } + } + } + } + for (RpmPackageAssociation conflict : getConflicts()) { + if (null != conflict.getName()) { + if (conflict.isVersionRange()) { + if (null != conflict.getMinVersion()) { + builder.addConflicts(conflict.getName(), Flags.GREATER | Flags.EQUAL, conflict.getMinVersion()); + } + + if (null != conflict.getMaxVersion()) { + builder.addConflicts(conflict.getName(), Flags.LESS, conflict.getMaxVersion()); + } + } else { + if (null != conflict.getVersion()) { + builder.addConflicts(conflict.getName(), Flags.EQUAL, conflict.getVersion()); + } else { + builder.addConflicts(conflict.getName(), 0, ""); + } + } + } + } + for (RpmLink link : getLinks()) { + builder.addLink(link.getPath(), link.getTarget(), + link.getPermissionsOrDefault(), link.getOwnerOrDefault(), link.getGroupOrDefault()); + } + getLog().debug("Setting trigger scripts"); + RpmScriptTemplateRenderer scriptTemplateRenderer = getMojo().getTemplateRenderer(); + Path scriptPath = Paths.get(buildDirectory, getName() + "-" + getProjectVersion()); + Path scriptTemplate; + scriptTemplate = getPreTransactionScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-pretrans-hook", scriptPath.toString())); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.setPreTrans(scriptFile); + builder.setPreTransProgram(getPreTransactionProgram()); + } + scriptTemplate = getPreInstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-preinstall-hook", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.setPreInstall(scriptFile); + builder.setPreInstallProgram(getPreInstallProgram()); + } + scriptTemplate = getPostInstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-postinstall-hook", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.setPostInstall(scriptFile); + builder.setPostInstallProgram(getPostInstallProgram()); + } + scriptTemplate = getPreUninstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-preuninstall-hook", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.setPreUninstall(scriptFile); + builder.setPreUninstallProgram(getPreUninstallProgram()); + } + scriptTemplate = getPostUninstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-postuninstall-hook", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.setPostUninstall(scriptFile); + builder.setPostUninstallProgram(getPostUninstallProgram()); + } + scriptTemplate = getPostTransactionScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-posttrans-hook", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.setPostTrans(scriptFile); + builder.setPostTransProgram(getPostTransactionProgram()); + } + for (RpmTrigger trigger : getTriggers()) { + Map depends = new HashMap<>(); + for (RpmPackageAssociation dependency : trigger.getDependencies()) { + int flags = 0; + String version = ""; + if (null != dependency.getVersion()) { + version = dependency.getVersion(); + } else if (null != dependency.getMinVersion()) { + flags = Flags.GREATER | Flags.EQUAL; + version = dependency.getMinVersion(); + } else if (null != dependency.getMaxVersion()) { + flags = Flags.LESS; + version = dependency.getMaxVersion(); + } + depends.put(dependency.getName(), new Trigger.IntString(flags, version)); + } + scriptTemplate = trigger.getPreInstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-preinstall-trigger", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.addTrigger(scriptFile, trigger.getPreInstallProgram(), depends, Flags.SCRIPT_TRIGGERPREIN); + } + scriptTemplate = trigger.getPostInstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-postinstall-trigger", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.addTrigger(scriptFile, trigger.getPostInstallProgram(), depends, Flags.SCRIPT_TRIGGERIN); + } + scriptTemplate = trigger.getPreUninstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-preuninstall-trigger", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.addTrigger(scriptFile, trigger.getPreUninstallProgram(), depends, Flags.SCRIPT_TRIGGERUN); + } + scriptTemplate = trigger.getPostUninstallScriptPath(); + if (null != scriptTemplate) { + Path scriptFile = Paths.get(String.format("%s-postuninstall-trigger", scriptPath)); + scriptTemplateRenderer.render(scriptTemplate, scriptFile); + builder.addTrigger(scriptFile, trigger.getPostUninstallProgram(), depends, Flags.SCRIPT_TRIGGERPOSTUN); + } + } + String keyFileName = getSigningKey(); + if (keyFileName != null) { + Path keyFile = Paths.get(keyFileName); + if (!Files.exists(keyFile)) { + throw new SigningKeyNotFoundException(keyFileName); + } + String keyPassPhrase = getSigningKeyPassPhrase(); + if (null != keyPassPhrase && !keyPassPhrase.equals("")) { + builder.setPrivateKeyPassphrase(keyPassPhrase); + } + builder.setPrivateKeyId(getSigningKeyId()); + try (InputStream inputStream = getClass().getResourceAsStream(keyFileName)) { + builder.setPrivateKeyRing(inputStream); + } + } + getLog().debug("Adding files matched from each rule"); + for (RpmPackageRule packageRule : getRules()) { + Collections.addAll(fileList, packageRule.addFiles(builder)); + } + Path rpmFileName = Paths.get(buildDirectory, getFinalName()); + getLog().info(String.format("Generating RPM file %s", rpmFileName)); + try (FileChannel fileChannel = FileChannel.open(rpmFileName, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE)) { + builder.build(fileChannel); + } + RpmMojo mojo = getMojo(); + if (mojo.getProjectPackagingType().equals("rpm") + && mojo.getProjectArtifactId().equals(getName()) + && mojo.getProjectVersion().equals(getProjectVersion())) { + getLog().info(String.format("Attaching %s as primary artifact", rpmFileName.toString())); + mojo.setPrimaryArtifact(rpmFileName, getClassifier()); + } else if (isAttach()) { + getLog().info(String.format("Attaching %s as secondary artifact", rpmFileName.toString())); + mojo.addSecondaryArtifact(rpmFileName, getName(), getProjectVersion(), getClassifier()); + } + return fileList; + } + + /** + * List files matched for the package. + * + * @return the file names included within the package + * @throws RpmException if method fails + */ + public Set listFiles() throws RpmException { + int counter = 1; + Set fileList = new HashSet<>(); + getMojo().getLog().info(String.format(" Package: %s", getFinalName())); + for (RpmPackageRule packageRule : getRules()) { + getMojo().getLog().info(String.format(" \\ Rule: %d", counter++)); + String[] packageRuleFileList = packageRule.listFiles(); + String scanPath = packageRule.getScanPath(); + for (String packageRulefileName : packageRuleFileList) { + getMojo().getLog().info(String.format(" - %s/%s", scanPath, packageRulefileName)); + } + Collections.addAll(fileList, packageRuleFileList); + } + getMojo().getLog().info(""); + return fileList; + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageAssociation.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageAssociation.java new file mode 100644 index 0000000..011ce34 --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageAssociation.java @@ -0,0 +1,124 @@ +package org.xbib.maven.plugin.rpm; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * RPM package association (i.e. obsoletes, dependencies, conflicts). + */ +public class RpmPackageAssociation { + /** + * Association name. + */ + private String name = null; + + /** + * Association maven style version. + */ + private String version = null; + + /** + * Min version requirement. + */ + private String minVersion = null; + + /** + * Max version requirement. + */ + private String maxVersion = null; + + /** + * Version requirement has range. + */ + private boolean isRange = false; + + /** + * Get name. + * + * @return Association name + */ + public String getName() { + return this.name; + } + + /** + * Set name. + * + * @param name Association name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Is version range. + * + * @return Version range, true or false + */ + public boolean isVersionRange() { + return isRange; + } + + /** + * Get version. + * + * @return Maven style version. + */ + public String getVersion() { + return version; + } + + /** + * Set version. + * + * @param version Maven style version. + */ + public void setVersion(String version) { + if (version == null || version.equals("RELEASE") || version.equals("")) { + isRange = false; + this.version = null; + minVersion = null; + maxVersion = null; + return; + } + Pattern versionPattern = Pattern.compile("\\[([0-9\\.]*),([0-9\\.]*)\\)"); + Matcher matcher = versionPattern.matcher(version); + if (matcher.matches()) { + this.isRange = true; + this.version = null; + String minVersion = matcher.group(1); + String maxVersion = matcher.group(2); + if (minVersion.equals("")) { + minVersion = null; + } + if (maxVersion.equals("")) { + maxVersion = null; + } + this.minVersion = minVersion; + this.maxVersion = maxVersion; + } else { + this.isRange = false; + this.version = version; + this.minVersion = null; + this.maxVersion = null; + } + } + + /** + * Get min version requirement. + * + * @return Min version requirement + */ + public String getMinVersion() { + return this.minVersion; + } + + /** + * Get max version requirement. + * + * @return Max version requirement + */ + public String getMaxVersion() { + return this.maxVersion; + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageRule.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageRule.java new file mode 100644 index 0000000..94d7c5e --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmPackageRule.java @@ -0,0 +1,259 @@ +package org.xbib.maven.plugin.rpm; + +import org.apache.maven.plugin.logging.Log; +import org.apache.tools.ant.DirectoryScanner; +import org.xbib.maven.plugin.rpm.mojo.RpmMojo; +import org.xbib.rpm.RpmBuilder; +import org.xbib.rpm.exception.InvalidDirectiveException; +import org.xbib.rpm.exception.InvalidPathException; +import org.xbib.rpm.exception.PathOutsideBuildPathException; +import org.xbib.rpm.exception.RpmException; +import org.xbib.rpm.payload.Directive; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * RPM rule, represents a file to be included within the RPM. + */ +public class RpmPackageRule extends RpmBaseObject { + /** + * RPM package. + */ + private RpmPackage rpmPackage = null; + + /** + * Rule base path, relative to plugin buildPath. + */ + private String base = File.separator; + + /** + * Destination path of files within RPM. + */ + private String destination = null; + + /** + * List of file include rules. + */ + private List includes = new ArrayList<>(); + + /** + * List of file exclude rules. + */ + private List excludes = new ArrayList<>(); + + /** + * File directives. + */ + private EnumSet directive; + + /** + * Get associated RPM package. + * + * @return RPM package + */ + @Override + public RpmPackage getPackage() { + return this.rpmPackage; + } + + /** + * Set associated RPM package. + * + * @param rpmPackage RPM package + */ + public void setPackage(RpmPackage rpmPackage) { + this.rpmPackage = rpmPackage; + } + + /** + * Get base path, relative to the buildPath. + * + * @return Base path + */ + public String getBase() { + return this.base; + } + + /** + * Set base path, relative to the buildPath. + * + * @param base Base path + */ + public void setBase(String base) { + if (null == base || base.equals("")) { + base = File.separator; + } + + this.base = base; + } + + /** + * Get file destination. + * + * @return File destination + */ + public String getDestination() { + return this.destination; + } + + /** + * Set file destination. + * + * @param destination File destination + */ + public void setDestination(String destination) { + if (null != destination && destination.equals("")) { + destination = null; + } + this.destination = destination; + } + + /** + * Get the file destination, or the default setting if not set. + * + * @return File destination + */ + public String getDestinationOrDefault() { + if (null == this.destination) { + return this.getPackage().getMojo().getDefaultDestination(); + } else { + return this.destination; + } + } + + /** + * Get file inclusion rules. + * + * @return File inclusion rules + */ + public List getIncludes() { + return this.includes; + } + + /** + * Set file inclusion rules. + * + * @param includes File inclusion rules + */ + public void setIncludes(List includes) { + this.includes = includes; + } + + /** + * Get file exclusion rules. + * + * @return File exclusion rules + */ + public List getExcludes() { + return this.excludes; + } + + /** + * Set file exclusion rules. + * + * @param excludes File exclusion rules + */ + public void setExcludes(List excludes) { + this.excludes = excludes; + } + + /** + * Get file directives. + * + * @return File directives + */ + public EnumSet getDirectives() { + return this.directive; + } + + /** + * Set file directives. + * + * @param directives File directives + * @throws InvalidDirectiveException if there is an invalid RPM directive + */ + public void setDirectives(List directives) throws InvalidDirectiveException { + this.directive = Directive.newDirective(directives); + } + + /** + * Get path used for scanning for files to be included by the rule. + * + * @return Scan path + * @throws InvalidPathException if path is invalid + */ + public String getScanPath() throws InvalidPathException { + String scanPath = String.format("%s%s%s", + this.rpmPackage.getMojo().getBuildPath(), File.separator, this.getBase()); + try { + return Paths.get(scanPath).toRealPath().toString(); + } catch (IOException ex) { + throw new InvalidPathException(scanPath, ex); + } + } + + /** + * Get the Maven logger. + * + * @return Maven logger + */ + public Log getLog() { + return this.getPackage().getMojo().getLog(); + } + + /** + * List all files found by the rule to the package. + * + * @return Matched file list + * @throws RpmException if files can not be listed + */ + public String[] listFiles() throws RpmException { + RpmMojo mojo = rpmPackage.getMojo(); + String buildPath = mojo.getBuildPath(); + String scanPath = getScanPath(); + if (!String.format("%s%s", scanPath, File.separator).startsWith(String.format("%s%s", buildPath, File.separator))) { + throw new PathOutsideBuildPathException(scanPath, buildPath); + } + DirectoryScanner ds = new DirectoryScanner(); + ds.setIncludes(getIncludes().toArray(new String[0])); + ds.setExcludes(getExcludes().toArray(new String[0])); + ds.setBasedir(scanPath); + ds.setFollowSymlinks(false); + ds.setCaseSensitive(true); + getLog().debug("Scanning for files for package rule"); + ds.scan(); + return ds.getIncludedFiles(); + } + + /** + * Add all files found by the rule to the package. + * + * @param builder RPM builder + * @return Matched file list + * @throws IOException if file could not be added + * @throws RpmException if RPM archive could not be listed + */ + public String[] addFiles(RpmBuilder builder) throws IOException, RpmException { + String[] includedFiles = listFiles(); + String scanPath = getScanPath(); + getLog().debug(String.format("Adding %d files found to package.", includedFiles.length)); + for (String includedFile : includedFiles) { + String destinationPath = this.getDestinationOrDefault() + File.separator + includedFile; + String sourcePath = String.format("%s%s%s", scanPath, File.separator, includedFile); + String owner = getOwnerOrDefault(); + String group = getGroupOrDefault(); + int fileMode = getPermissionsOrDefault(); + getLog().debug(String.format("Adding file: %s to path %s with owner '%s', " + + "group '%s', with file mode %o.", + sourcePath, destinationPath, owner, group, fileMode)); + builder.addFile(destinationPath, Paths.get(sourcePath), fileMode, + getDirectives(), owner, group); + } + return includedFiles; + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRenderer.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRenderer.java new file mode 100644 index 0000000..f7d0c5f --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRenderer.java @@ -0,0 +1,53 @@ +package org.xbib.maven.plugin.rpm; + +import org.mvel2.templates.TemplateRuntime; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +/** + * RPM script template renderer. + */ +public class RpmScriptTemplateRenderer { + /** + * Template parameter map. + */ + private Map parameterMap = new HashMap<>(); + + /** + * Add parameter to parameter map. + * + * @param name Parameter Name + * @param value Parameter value + */ + public void addParameter(String name, Object value) { + this.parameterMap.put(name, value); + } + + /** + * Render a script template file. + * + * @param templateFile Template file + * @param renderedFile Rendered output file + * @throws IOException if rendering fails + */ + public void render(Path templateFile, Path renderedFile) throws IOException { + char[] buffer = new char[1024]; + StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader reader = Files.newBufferedReader(templateFile)) { + int bytesRead; + while (-1 != (bytesRead = reader.read(buffer))) { + stringBuilder.append(buffer, 0, bytesRead); + } + } + String renderedTemplate = (String) TemplateRuntime.eval(stringBuilder.toString(), this.parameterMap); + try (BufferedWriter writer = Files.newBufferedWriter(renderedFile)) { + writer.write(renderedTemplate); + } + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmTrigger.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmTrigger.java new file mode 100644 index 0000000..972ae23 --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/RpmTrigger.java @@ -0,0 +1,217 @@ +package org.xbib.maven.plugin.rpm; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * RPM trigger. + */ +public class RpmTrigger { + /** + * Pre install event hook script path. + */ + private Path preInstallScriptPath = null; + + /** + * Pre install event hook program. + */ + private String preInstallProgram = null; + + /** + * Post install event hook script path. + */ + private Path postInstallScriptPath = null; + + /** + * Post install event hook program. + */ + private String postInstallProgram = null; + + /** + * Pre uninstall event hook script path. + */ + private Path preUninstallScriptPath = null; + + /** + * Pre uninstall event hook script program. + */ + private String preUninstallProgram = null; + + /** + * Post uninstall event hook script path. + */ + private Path postUninstallScriptPath = null; + + /** + * Post uninstall event hook program. + */ + private String postUninstallProgram = null; + + /** + * Trigger package associations. + */ + private List dependencies = new ArrayList<>(); + + /** + * Get pre install script path. + * + * @return Pre install script path + */ + public Path getPreInstallScriptPath() { + return this.preInstallScriptPath; + } + + /** + * Set pre install script path. + * + * @param preInstallScriptPath Pre install script path + */ + public void setPreInstallScriptPath(Path preInstallScriptPath) { + this.preInstallScriptPath = preInstallScriptPath; + } + + /** + * Get pre install program. + * + * @return Pre install program + */ + public String getPreInstallProgram() { + return this.preInstallProgram; + } + + /** + * Set pre install program. + * + * @param preInstallProgram Pre install program + */ + public void setPreInstallProgram(String preInstallProgram) { + this.preInstallProgram = preInstallProgram; + } + + /** + * Get post install script path. + * + * @return Post install script path + */ + public Path getPostInstallScriptPath() { + return this.postInstallScriptPath; + } + + /** + * Set post install script path. + * + * @param postInstallScriptPath Post install script path + */ + public void setPostInstallScriptPath(Path postInstallScriptPath) { + this.postInstallScriptPath = postInstallScriptPath; + } + + /** + * Get post install program. + * + * @return Post install program + */ + public String getPostInstallProgram() { + return this.postInstallProgram; + } + + /** + * Set post install program. + * + * @param postInstallProgram Post install program + */ + public void setPostInstallProgram(String postInstallProgram) { + this.postInstallProgram = postInstallProgram; + } + + /** + * Get pre uninstall script path. + * + * @return Pre uninstall script path + */ + public Path getPreUninstallScriptPath() { + return this.preUninstallScriptPath; + } + + /** + * Set pre uninstall script path. + * + * @param preUninstallScriptPath Pre uninstall script path + */ + public void setPreUninstallScriptPath(Path preUninstallScriptPath) { + this.preUninstallScriptPath = preUninstallScriptPath; + } + + /** + * Get pre uninstall program. + * + * @return Pre uninstall program + */ + public String getPreUninstallProgram() { + return this.preUninstallProgram; + } + + /** + * Set pre uninstall program. + * + * @param preUninstallProgram Pre uninstall program + */ + public void setPreUninstallProgram(String preUninstallProgram) { + this.preUninstallProgram = preUninstallProgram; + } + + /** + * Get post uninstall script path. + * + * @return Post uninstall script path + */ + public Path getPostUninstallScriptPath() { + return this.postUninstallScriptPath; + } + + /** + * Set post uninstall script path. + * + * @param postUninstallScriptPath Post uninstall script path + */ + public void setPostUninstallScriptPath(Path postUninstallScriptPath) { + this.postUninstallScriptPath = postUninstallScriptPath; + } + + /** + * Get post uninstall program. + * + * @return Post uninstall program + */ + public String getPostUninstallProgram() { + return this.postUninstallProgram; + } + + /** + * Set post uninstall program. + * + * @param postUninstallProgram Post uninstall program + */ + public void setPostUninstallProgram(String postUninstallProgram) { + this.postUninstallProgram = postUninstallProgram; + } + + /** + * Get trigger packages. + * + * @return Trigger packages + */ + public List getDependencies() { + return dependencies; + } + + /** + * Set trigger packages. + * + * @param dependencies Trigger packages + */ + public void setDependencies(List dependencies) { + this.dependencies = dependencies; + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojo.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojo.java new file mode 100644 index 0000000..46afd4f --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojo.java @@ -0,0 +1,407 @@ +package org.xbib.maven.plugin.rpm.mojo; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.model.License; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.tools.ant.DirectoryScanner; +import org.xbib.maven.plugin.rpm.RpmPackage; +import org.xbib.maven.plugin.rpm.RpmScriptTemplateRenderer; +import org.xbib.rpm.exception.InvalidPathException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +/** + * + */ +public abstract class AbstractRpmMojo extends AbstractMojo implements RpmMojo { + /** + * Set of master files (all files in build path). + */ + protected Set masterFiles = new HashSet<>(); + + /** + * Event hook template renderer. + */ + private RpmScriptTemplateRenderer templateRenderer = null; + + /** + * Maven project. + */ + @Parameter(defaultValue = "${project}", readonly = true) + protected MavenProject project = null; + + /** + * Build path. + */ + @Parameter(defaultValue = "${project.build.outputDirectory}") + private String buildPath = null; + + /** + * RPM package declarations from configuration. + */ + @Parameter + protected List packages = new ArrayList<>(); + + /** + * Default mode. + */ + @Parameter + private int defaultFileMode = 0644; + + /** + * Default owner. + */ + @Parameter + private String defaultOwner = "root"; + + /** + * Default group. + */ + @Parameter + private String defaultGroup = "root"; + + /** + * Default installation destination. + */ + @Parameter + private String defaultDestination = File.separator; + + /** + * List of file exclude patterns. + */ + @Parameter + protected List excludes = new ArrayList<>(); + + /** + * Perform checking for extra files not included within any packages, + * or excluded from all packages. + */ + @Parameter + private boolean performCheckingForExtraFiles = true; + + /** + * Get Maven project. + * + * @return Maven project + */ + protected MavenProject getProject() { + return project; + } + + /** + * Set Maven project. + * + * @param project Maven project + */ + public void setProject(MavenProject project) { + this.project = project; + } + + /** + * Get event hook template renderer. + * + * @return Event hook template renderer + */ + @Override + public RpmScriptTemplateRenderer getTemplateRenderer() { + if (templateRenderer == null) { + templateRenderer = new RpmScriptTemplateRenderer(); + templateRenderer.addParameter("project", getProject()); + templateRenderer.addParameter("env", System.getenv()); + Properties systemProperties = System.getProperties(); + for (String propertyName : systemProperties.stringPropertyNames()) { + templateRenderer.addParameter(propertyName, systemProperties.getProperty(propertyName)); + } + Properties projectProperties = getProject().getProperties(); + for (String propertyName : projectProperties.stringPropertyNames()) { + templateRenderer.addParameter(propertyName, projectProperties.getProperty(propertyName)); + } + } + return templateRenderer; + } + + /** + * Get the project artifact id. + * + * @return Artifact id + */ + @Override + public String getProjectArtifactId() { + return getProject().getArtifactId(); + } + + /** + * Get the project version. + * + * @return Project version + */ + @Override + public String getProjectVersion() { + return getProject().getVersion(); + } + + /** + * Get the project url. + * + * @return Project url + */ + @Override + public String getProjectUrl() { + return getProject().getUrl(); + } + + /** + * Get project packaging type. + * + * @return Packaging type + */ + @Override + public String getProjectPackagingType() { + return getProject().getPackaging(); + } + + /** + * Get collapsed project licensing. + * + * @return Project licenses, collapsed in to a single line, separated by commas. + */ + @Override + public String getCollapsedProjectLicense() { + StringBuilder collapsedLicenseList = new StringBuilder(); + for (License license : getProject().getLicenses()) { + if (collapsedLicenseList.toString().equals("")) { + collapsedLicenseList = new StringBuilder(license.getName()); + } else { + collapsedLicenseList.append(", ").append(license.getName()); + } + } + return (collapsedLicenseList.length() > 0 ? collapsedLicenseList.toString() : null); + } + + /** + * Get build output directory. + * + * @return Build output directory + */ + @Override + public String getBuildDirectory() { + return project.getBuild().getDirectory(); + } + + /** + * Set the primary artifact. + * + * @param artifactFile Primary artifact + * @param classifier Artifact classifier + */ + @Override + public void setPrimaryArtifact(Path artifactFile, String classifier) { + DefaultArtifactHandler handler = new DefaultArtifactHandler(); + handler.setExtension("rpm"); + Artifact artifact = new DefaultArtifact(getProject().getGroupId(), + getProject().getArtifactId(), + getProject().getVersion(), + null, + "rpm", + classifier, + handler + ); + artifact.setFile(artifactFile.toFile()); + getProject().setArtifact(artifact); + } + + /** + * Add a secondary artifact. + * + * @param artifactFile Secondary artifact file + * @param artifactId Artifact Id + * @param version Artifact version + * @param classifier Artifact classifier + */ + @Override + public void addSecondaryArtifact(Path artifactFile, String artifactId, String version, String classifier) { + DefaultArtifactHandler handler = new DefaultArtifactHandler(); + handler.setExtension("rpm"); + Artifact artifact = new DefaultArtifact( + getProject().getGroupId(), + artifactId, + version, + null, + "rpm", + classifier, + handler + ); + artifact.setFile(artifactFile.toFile()); + getProject().addAttachedArtifact(artifact); + } + + /** + * Get the build root path. + * + * @return Build root path + */ + @Override + public String getBuildPath() throws InvalidPathException { + try { + return new File(buildPath).getCanonicalPath(); + } catch (IOException ex) { + throw new InvalidPathException(buildPath, ex); + } + } + + /** + * Set the build root path. + * + * @param buildPath Build root path + */ + public void setBuildPath(String buildPath) { + this.buildPath = buildPath; + } + + /** + * Set the RPM packages defined by the configuration. + * + * @param packages List of RPM packages + */ + public void setPackages(List packages) { + for (RpmPackage rpmPackage : packages) { + rpmPackage.setMojo(this); + } + this.packages = packages; + } + + /** + * Get default mode. + * + * @return Default mode + */ + @Override + public int getDefaultFileMode() { + return defaultFileMode; + } + + /** + * Set default mode. + * + * @param defaultFileMode Default mode + */ + public void setDefaultFileMode(int defaultFileMode) { + this.defaultFileMode = defaultFileMode; + } + + /** + * Get default owner. + * + * @return Default owner + */ + @Override + public String getDefaultOwner() { + return defaultOwner; + } + + /** + * Set default owner. + * + * @param defaultOwner Default owner + */ + public void setDefaultOwner(String defaultOwner) { + this.defaultOwner = defaultOwner; + } + + /** + * Get default group. + * + * @return Default group + */ + @Override + public String getDefaultGroup() { + return defaultGroup; + } + + /** + * Set default group. + * + * @param defaultGroup Default group + */ + public void setDefaultGroup(String defaultGroup) { + this.defaultGroup = defaultGroup; + } + + /** + * Get default destination. + * + * @return Default destination + */ + @Override + public String getDefaultDestination() { + return defaultDestination; + } + + /** + * Set default destination. + * + * @param defaultDestination Default destination + */ + public void setDefaultDestination(String defaultDestination) { + this.defaultDestination = defaultDestination; + } + + /** + * Set the list of file exclude patterns. + * + * @param excludes List of file exclude patterns + */ + public void setExcludes(List excludes) { + this.excludes = excludes; + } + + /** + * Get ignore extra files. + * + * @return Ignore extra files + */ + public boolean isPerformCheckingForExtraFiles() { + return performCheckingForExtraFiles; + } + + /** + * Set ignore extra files. + * + * @param performCheckingForExtraFiles Ignore extra files + */ + public void setPerformCheckingForExtraFiles(boolean performCheckingForExtraFiles) { + this.performCheckingForExtraFiles = performCheckingForExtraFiles; + } + + /** + * Scan the build path for all files for inclusion in an RPM archive. + * Excludes are applied also. This is because it doesn't matter + * if a file ends up being included within an RPM as the master list + * is only for us to know which files have been missed by a packaging + * rule. + */ + protected void scanMasterFiles() { + DirectoryScanner ds = new DirectoryScanner(); + ds.setIncludes(null); + ds.setExcludes(excludes.toArray(new String[0])); + ds.setBasedir(buildPath); + ds.setFollowSymlinks(false); + ds.setCaseSensitive(true); + ds.scan(); + String[] fileMatches = ds.getIncludedFiles(); + masterFiles = new HashSet<>(fileMatches.length); + Collections.addAll(masterFiles, fileMatches); + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojo.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojo.java new file mode 100644 index 0000000..72260e4 --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojo.java @@ -0,0 +1,43 @@ +package org.xbib.maven.plugin.rpm.mojo; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.xbib.maven.plugin.rpm.RpmPackage; +import org.xbib.rpm.exception.RpmException; + +import java.util.Set; + +/** + * + */ +@Mojo(name = "listfiles", defaultPhase = LifecyclePhase.PACKAGE) +public class ListFilesRpmMojo extends AbstractRpmMojo { + + /** + * Execute goal. + * + * @throws MojoExecutionException There was a problem running the Mojo. + * Further details are available in the message and cause properties. + */ + @Override + public void execute() throws MojoExecutionException { + getLog().info("Declared packages:"); + scanMasterFiles(); + for (RpmPackage rpmPackage : this.packages) { + Set includedFiles; + try { + includedFiles = rpmPackage.listFiles(); + } catch (RpmException e) { + throw new MojoExecutionException(e.getMessage()); + } + masterFiles.removeAll(includedFiles); + } + if (masterFiles.size() > 0) { + getLog().info("Unmatched files:"); + for (String unmatchedFile : this.masterFiles) { + getLog().info(String.format(" - %s", unmatchedFile)); + } + } + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojo.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojo.java new file mode 100644 index 0000000..ee113e2 --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojo.java @@ -0,0 +1,54 @@ +package org.xbib.maven.plugin.rpm.mojo; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.xbib.maven.plugin.rpm.RpmPackage; + +import java.util.Set; + +/** + * Build an RPM using Maven, allowing for operating system agnostic RPM builds. + */ +@Mojo(name = "package", defaultPhase = LifecyclePhase.PACKAGE) +public class PackageRpmMojo extends AbstractRpmMojo { + + /** + * Execute goal. + * + * @throws MojoExecutionException There was a problem running the Mojo. + * Further details are available in the message and cause properties. + */ + public void execute() throws MojoExecutionException { + scanMasterFiles(); + long totalFilesPackaged = 0; + for (RpmPackage rpmPackage : this.packages) { + Set includedFiles; + try { + includedFiles = rpmPackage.build(); + } catch (Exception ex) { + getLog().error(String.format("Unable to build package %s", rpmPackage.getName()), ex); + throw new MojoExecutionException(String.format("Unable to build package %s", rpmPackage.getName()), ex); + } + masterFiles.removeAll(includedFiles); + totalFilesPackaged += includedFiles.size(); + } + if (isPerformCheckingForExtraFiles() && masterFiles.size() > 0) { + getLog().error(String.format("%d file(s) listed below were found in the build path that have not been " + + "included in any package or explicitly excluded. Maybe you need to exclude them?", masterFiles.size())); + for (String missedFile : this.masterFiles) { + getLog().error(String.format(" - %s", missedFile)); + } + throw new MojoExecutionException(String.format("%d file(s) were found in the build path that have not been " + + "included or explicitly excluded. Maybe you need to exclude them?", + masterFiles.size())); + } + if (0 < packages.size() && 0 == totalFilesPackaged) { + // No files were actually packaged. Perhaps something got missed. + getLog().error("No files were included when packaging RPM artifacts. " + + "Did you specify the correct output path?"); + throw new MojoExecutionException("No files were included when packaging RPM artifacts. " + + "Did you specify the correct output path?"); + } + } +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/RpmMojo.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/RpmMojo.java new file mode 100644 index 0000000..edeba5a --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/RpmMojo.java @@ -0,0 +1,122 @@ +package org.xbib.maven.plugin.rpm.mojo; + +import org.apache.maven.plugin.logging.Log; +import org.xbib.maven.plugin.rpm.RpmScriptTemplateRenderer; +import org.xbib.rpm.exception.InvalidPathException; + +import java.nio.file.Path; + +/** + * Plugin mojo implementation. + */ +public interface RpmMojo { + /** + * Get event hook template renderer. + * + * @return Event hook template renderer + */ + RpmScriptTemplateRenderer getTemplateRenderer(); + + /** + * Set the primary artifact. + * + * @param artifactFile Primary artifact + * @param classifier Artifact classifier + */ + void setPrimaryArtifact(Path artifactFile, String classifier); + + /** + * Add a secondary artifact. + * + * @param artifactFile Secondary artifact file + * @param artifactId Artifact Id + * @param version Artifact version + * @param classifier Artifact classifier + */ + void addSecondaryArtifact(Path artifactFile, String artifactId, String version, String classifier); + + /** + * Get build output directory. + * + * @return Build output directory + */ + String getBuildDirectory(); + + /** + * Get the project artifact id. + * + * @return Artifact id + */ + String getProjectArtifactId(); + + /** + * Get the project version. + * + * @return Project version + */ + String getProjectVersion(); + + /** + * Get the project url. + * + * @return Project url + */ + String getProjectUrl(); + + /** + * Get project packaging type. + * + * @return Packaging type + */ + String getProjectPackagingType(); + + /** + * Get collapsed project licensing. + * + * @return Project licenses, collapsed in to a single line, separated by commas. + */ + String getCollapsedProjectLicense(); + + /** + * Get the build root path. + * + * @return Build root path + * @throws InvalidPathException Build path is invalid and could not be retrieved + */ + String getBuildPath() throws InvalidPathException; + + /** + * Get default mode. + * + * @return Default mode + */ + int getDefaultFileMode(); + + /** + * Get default owner. + * + * @return Default owner + */ + String getDefaultOwner(); + + /** + * Get default group. + * + * @return Default group + */ + String getDefaultGroup(); + + /** + * Get default destination. + * + * @return Default destination + */ + String getDefaultDestination(); + + /** + * Get logger. + * + * @return Logger + */ + Log getLog(); +} diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/package-info.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/package-info.java new file mode 100644 index 0000000..425d0bb --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/mojo/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.maven.plugin.rpm.mojo; diff --git a/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/package-info.java b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/package-info.java new file mode 100644 index 0000000..ccdc9ea --- /dev/null +++ b/maven-plugin-rpm/src/main/java/org/xbib/maven/plugin/rpm/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.maven.plugin.rpm; diff --git a/maven-plugin-rpm/src/main/resources/META-INF/plexus/components.xml b/maven-plugin-rpm/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 0000000..0c09c42 --- /dev/null +++ b/maven-plugin-rpm/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,19 @@ + + + + org.apache.maven.lifecycle.mapping.LifecycleMapping + rpm + org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping + per-lookup + + + org.apache.maven.plugins:maven-resources-plugin:resources + org.apache.maven.plugins:maven-compiler-plugin:compile + org.xbib:maven-plugin-rpm:package + org.apache.maven.plugins:maven-install-plugin:install + org.apache.maven.plugins:maven-deploy-plugin:deploy + + + + + diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockBuilder.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockBuilder.java new file mode 100644 index 0000000..b4f1859 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockBuilder.java @@ -0,0 +1,21 @@ +package org.xbib.maven.plugin.rpm; + +import org.xbib.rpm.RpmBuilder; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Path; + +/** + * + */ +public class MockBuilder extends RpmBuilder { + + @Override + public void build(Path directory) throws IOException { + } + + @Override + public void build(SeekableByteChannel channel) throws IOException { + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockMojo.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockMojo.java new file mode 100644 index 0000000..54cfdaf --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/MockMojo.java @@ -0,0 +1,49 @@ +package org.xbib.maven.plugin.rpm; + +import org.apache.maven.plugin.MojoExecutionException; +import org.xbib.maven.plugin.rpm.mojo.AbstractRpmMojo; + +import java.util.List; +import java.util.Set; + +/** + * + */ +public class MockMojo extends AbstractRpmMojo { + + @Override + public void execute() throws MojoExecutionException { + } + + @Override + public void scanMasterFiles() { + super.scanMasterFiles(); + } + + /** + * Get master file set. + * + * @return Master file set + */ + public Set getMasterFiles() { + return this.masterFiles; + } + + /** + * Get excludes. + * + * @return Excludes + */ + public List getExcludes() { + return this.excludes; + } + + /** + * Get packages. + * + * @return Packages + */ + public List getPackages() { + return this.packages; + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmBaseObjectTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmBaseObjectTest.java new file mode 100644 index 0000000..baaf5fd --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmBaseObjectTest.java @@ -0,0 +1,46 @@ +package org.xbib.maven.plugin.rpm; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * + */ +public abstract class RpmBaseObjectTest { + + protected abstract RpmBaseObject getRpmBaseObject(); + + @Test + public void modeAccessors() { + assertEquals(0644, getRpmBaseObject().getPermissionsOrDefault()); + getRpmBaseObject().setPermissions(0755); + assertEquals(0755, getRpmBaseObject().getPermissionsOrDefault()); + } + + @Test + public void ownerAccessors() { + getRpmBaseObject().setOwner(""); + assertEquals("root", getRpmBaseObject().getOwnerOrDefault()); + getRpmBaseObject().setOwner(null); + assertEquals("root", getRpmBaseObject().getOwnerOrDefault()); + assertEquals("root", getRpmBaseObject().getOwnerOrDefault()); + getRpmBaseObject().setOwner("owner"); + assertEquals("owner", getRpmBaseObject().getOwnerOrDefault()); + getRpmBaseObject().setOwner(""); + assertEquals("root", getRpmBaseObject().getOwnerOrDefault()); + } + + @Test + public void groupAccessors() { + getRpmBaseObject().setGroup(""); + assertEquals("root", getRpmBaseObject().getGroupOrDefault()); + getRpmBaseObject().setGroup(null); + assertEquals("root", getRpmBaseObject().getGroupOrDefault()); + assertEquals("root", getRpmBaseObject().getGroupOrDefault()); + getRpmBaseObject().setGroup("group"); + assertEquals("group", getRpmBaseObject().getGroupOrDefault()); + getRpmBaseObject().setGroup(""); + assertEquals("root", getRpmBaseObject().getGroupOrDefault()); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmLinkTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmLinkTest.java new file mode 100644 index 0000000..b306983 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmLinkTest.java @@ -0,0 +1,42 @@ +package org.xbib.maven.plugin.rpm; + +import org.apache.maven.monitor.logging.DefaultLog; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.logging.console.ConsoleLogger; +import org.junit.Before; +import org.xbib.maven.plugin.rpm.mojo.PackageRpmMojo; + +import java.io.File; + +/** + * + */ +public class RpmLinkTest extends RpmBaseObjectTest { + + private RpmLink rpmLink; + + @Before + public void setUp() throws Exception { + String testOutputPath = System.getProperty("project.build.testOutputDirectory"); + PackageRpmMojo mojo = new PackageRpmMojo(); + mojo.setDefaultFileMode(0644); + mojo.setDefaultOwner("root"); + mojo.setDefaultGroup("root"); + mojo.setDefaultDestination(String.format("%svar%swww%stest", File.separator, File.separator, File.separator)); + mojo.setBuildPath(testOutputPath); + MavenProject mavenProject = new MavenProject(); + mojo.setProject(mavenProject); + Log log = new DefaultLog(new ConsoleLogger()); + mojo.setLog(log); + RpmPackage rpmPackage = new RpmPackage(); + rpmPackage.setMojo(mojo); + rpmLink = new RpmLink(); + rpmLink.setPackage(rpmPackage); + } + + @Override + public RpmBaseObject getRpmBaseObject() { + return rpmLink; + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageAssociationTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageAssociationTest.java new file mode 100644 index 0000000..9a1c36a --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageAssociationTest.java @@ -0,0 +1,81 @@ +package org.xbib.maven.plugin.rpm; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +/** + * + */ +public class RpmPackageAssociationTest { + + private RpmPackageAssociation association; + + @Before + public void setUp() { + association = new RpmPackageAssociation(); + } + + @Test + public void nameAccessors() { + assertEquals(null, association.getName()); + association.setName("testname"); + assertEquals("testname", association.getName()); + } + + @Test + public void unassignedVersion() { + assertEquals(null, association.getVersion()); + assertEquals(null, association.getMinVersion()); + assertEquals(null, association.getMaxVersion()); + } + + @Test + public void latestVersion() { + association.setVersion(null); + assertEquals(null, association.getVersion()); + assertEquals(null, association.getMinVersion()); + assertEquals(null, association.getMaxVersion()); + association.setVersion(""); + assertEquals(null, association.getVersion()); + assertEquals(null, association.getMinVersion()); + assertEquals(null, association.getMaxVersion()); + association.setVersion("RELEASE"); + assertEquals(null, association.getVersion()); + assertEquals(null, association.getMinVersion()); + assertEquals(null, association.getMaxVersion()); + } + + @Test + public void specificVersion() { + association.setVersion("1.2.3"); + assertEquals("1.2.3", association.getVersion()); + assertEquals(null, association.getMinVersion()); + assertEquals(null, association.getMaxVersion()); + } + + @Test + public void minVersionRange() { + association.setVersion("[1.2.3,)"); + assertEquals(null, association.getVersion()); + assertEquals("1.2.3", association.getMinVersion()); + assertEquals(null, association.getMaxVersion()); + } + + @Test + public void maxVersionRange() { + association.setVersion("[,1.2.3)"); + assertEquals(null, association.getVersion()); + assertEquals(null, association.getMinVersion()); + assertEquals("1.2.3", association.getMaxVersion()); + } + + @Test + public void minMaxVersionRange() { + association.setVersion("[1.2.3,1.2.5)"); + assertEquals(null, association.getVersion()); + assertEquals("1.2.3", association.getMinVersion()); + assertEquals("1.2.5", association.getMaxVersion()); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleDirectiveTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleDirectiveTest.java new file mode 100644 index 0000000..1bb601e --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleDirectiveTest.java @@ -0,0 +1,136 @@ +package org.xbib.maven.plugin.rpm; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.xbib.rpm.exception.InvalidDirectiveException; +import org.xbib.rpm.payload.Directive; + +import java.util.EnumSet; + +/** + * + */ +public class RpmPackageRuleDirectiveTest { + + @Test + public void configDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.CONFIG); + assertTrue(directiveList.contains(Directive.CONFIG)); + assertFalse(directiveList.contains(Directive.DOC)); + } + + @Test + public void docDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.DOC); + assertTrue(directiveList.contains(Directive.DOC)); + assertFalse(directiveList.contains(Directive.ICON)); + } + + @Test + public void iconDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.ICON); + assertTrue(directiveList.contains(Directive.ICON)); + assertFalse(directiveList.contains(Directive.MISSINGOK)); + } + + @Test + public void missingOkDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.MISSINGOK); + assertTrue(directiveList.contains(Directive.MISSINGOK)); + assertFalse(directiveList.contains(Directive.NOREPLACE)); + } + + @Test + public void noReplaceDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.NOREPLACE); + assertTrue(directiveList.contains(Directive.NOREPLACE)); + assertFalse(directiveList.contains(Directive.SPECFILE)); + } + + @Test + public void specFileDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.SPECFILE); + assertTrue(directiveList.contains(Directive.SPECFILE)); + assertFalse(directiveList.contains(Directive.GHOST)); + } + + @Test + public void ghostDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.GHOST); + assertTrue(directiveList.contains(Directive.GHOST)); + assertFalse(directiveList.contains(Directive.LICENSE)); + } + + @Test + public void licenseDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.LICENSE); + assertTrue(directiveList.contains(Directive.LICENSE)); + assertFalse(directiveList.contains(Directive.README)); + } + + @Test + public void readmeDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.README); + assertTrue(directiveList.contains(Directive.README)); + assertFalse(directiveList.contains(Directive.EXCLUDE)); + } + + @Test + public void excludeDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.EXCLUDE); + assertTrue(directiveList.contains(Directive.EXCLUDE)); + assertFalse(directiveList.contains(Directive.UNPATCHED)); + } + + @Test + public void unpatchedDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.UNPATCHED); + assertTrue(directiveList.contains(Directive.UNPATCHED)); + assertFalse(directiveList.contains(Directive.POLICY)); + } + + @Test + public void pubkeyDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.PUBKEY); + assertTrue(directiveList.contains(Directive.PUBKEY)); + assertFalse(directiveList.contains(Directive.POLICY)); + } + + @Test + public void policyDirective() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.POLICY); + assertTrue(directiveList.contains(Directive.POLICY)); + } + + @Test + public void multipleDirectives() throws InvalidDirectiveException { + EnumSet directiveList = EnumSet.of(Directive.NONE); + directiveList.add(Directive.CONFIG); + directiveList.add(Directive.NOREPLACE); + directiveList.add(Directive.LICENSE); + directiveList.add(Directive.README); + assertTrue(directiveList.contains(Directive.CONFIG)); + assertTrue(directiveList.contains(Directive.NOREPLACE)); + assertTrue(directiveList.contains(Directive.LICENSE)); + assertTrue(directiveList.contains(Directive.README)); + assertFalse(directiveList.contains(Directive.UNPATCHED)); + assertFalse(directiveList.contains(Directive.PUBKEY)); + assertFalse(directiveList.contains(Directive.POLICY)); + assertFalse(directiveList.contains(Directive.DOC)); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleTest.java new file mode 100644 index 0000000..46006b7 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageRuleTest.java @@ -0,0 +1,153 @@ +package org.xbib.maven.plugin.rpm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.apache.maven.monitor.logging.DefaultLog; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.logging.console.ConsoleLogger; +import org.junit.Before; +import org.junit.Test; +import org.xbib.maven.plugin.rpm.mojo.PackageRpmMojo; +import org.xbib.rpm.exception.InvalidDirectiveException; +import org.xbib.rpm.exception.InvalidPathException; +import org.xbib.rpm.exception.PathOutsideBuildPathException; +import org.xbib.rpm.exception.RpmException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class RpmPackageRuleTest extends RpmBaseObjectTest { + + private String testOutputPath; + + private Log log; + + private RpmPackageRule rpmFileRule; + + private RpmPackage rpmPackage; + + @Before + public void setUp() throws Exception { + testOutputPath = System.getProperty("project.build.testOutputDirectory"); + PackageRpmMojo mojo = new PackageRpmMojo(); + mojo.setDefaultFileMode(0644); + mojo.setDefaultOwner("root"); + mojo.setDefaultGroup("root"); + mojo.setDefaultDestination(String.format("%svar%swww%stest", File.separator, File.separator, File.separator)); + mojo.setBuildPath(testOutputPath); + MavenProject mavenProject = new MavenProject(); + mojo.setProject(mavenProject); + log = new DefaultLog(new ConsoleLogger()); + mojo.setLog(log); + rpmPackage = new RpmPackage(); + rpmPackage.setMojo(mojo); + rpmFileRule = new RpmPackageRule(); + rpmFileRule.setPackage(rpmPackage); + rpmFileRule.setBase("build"); + } + + @Override + public RpmBaseObject getRpmBaseObject() { + return rpmFileRule; + } + + @Test + public void directiveAccessors() throws InvalidDirectiveException { + List directives = new ArrayList<>(); + directives.add("noreplace"); + rpmFileRule.setDirectives(directives); + assertNotNull(rpmFileRule.getDirectives()); + } + + @Test + public void packageAccessors() { + assertEquals(rpmPackage, rpmFileRule.getPackage()); + } + + @Test + public void baseAccessors() { + rpmFileRule.setBase(""); + assertEquals(File.separator, rpmFileRule.getBase()); + rpmFileRule.setBase(null); + assertEquals(File.separator, rpmFileRule.getBase()); + rpmFileRule.setBase(String.format("%sfoo", File.separator)); + assertEquals(String.format("%sfoo", File.separator), rpmFileRule.getBase()); + } + + @Test + public void destinationAccessors() { + rpmFileRule.setDestination(""); + assertEquals(null, rpmFileRule.getDestination()); + rpmFileRule.setDestination(null); + assertEquals(null, rpmFileRule.getDestination()); + assertEquals(String.format("%svar%swww%stest", File.separator, File.separator, File.separator), + rpmFileRule.getDestinationOrDefault()); + rpmFileRule.setDestination(String.format("%sfoo", File.separator)); + assertEquals(String.format("%sfoo", File.separator), rpmFileRule.getDestination()); + assertEquals(String.format("%sfoo", File.separator), rpmFileRule.getDestinationOrDefault()); + } + + @Test + public void includeAccessors() { + List includes = new ArrayList<>(); + rpmFileRule.setIncludes(includes); + assertEquals(includes, rpmFileRule.getIncludes()); + } + + @Test + public void excludeAccessors() { + List excludes = new ArrayList<>(); + rpmFileRule.setExcludes(excludes); + assertEquals(excludes, rpmFileRule.getExcludes()); + } + + @Test + public void logAccessor() { + assertEquals(log, rpmFileRule.getLog()); + } + + @Test + public void scanPathAccessor() throws InvalidPathException { + String scanPath = String.format("%s%sbuild", new File(testOutputPath).getAbsolutePath(), File.separator); + assertEquals(scanPath, rpmFileRule.getScanPath()); + } + + @Test + public void testListFiles() throws RpmException { + List includes = new ArrayList<>(); + includes.add("**"); + List excludes = new ArrayList<>(); + excludes.add("composer.*"); + rpmFileRule.setIncludes(includes); + rpmFileRule.setExcludes(excludes); + String[] files = rpmFileRule.listFiles(); + assertEquals(63, files.length); + } + + @Test(expected = PathOutsideBuildPathException.class) + public void testListFilesOutsideBuildPath() throws RpmException { + rpmFileRule.setBase(String.format("..%s", File.separator)); + rpmFileRule.listFiles(); + } + + @Test + public void testAddFiles() throws IOException, RpmException { + MockBuilder builder = new MockBuilder(); + List includes = new ArrayList<>(); + includes.add("**"); + List excludes = new ArrayList<>(); + excludes.add("composer.*"); + rpmFileRule.setIncludes(includes); + rpmFileRule.setExcludes(excludes); + String[] files = rpmFileRule.addFiles(builder); + assertEquals(63, files.length); + assertEquals(94, builder.getContents().size()); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageTest.java new file mode 100644 index 0000000..d008e97 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmPackageTest.java @@ -0,0 +1,410 @@ +package org.xbib.maven.plugin.rpm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.maven.model.Build; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.Test; +import org.xbib.maven.plugin.rpm.mojo.PackageRpmMojo; +import org.xbib.rpm.exception.RpmException; +import org.xbib.rpm.exception.UnknownArchitectureException; +import org.xbib.rpm.exception.UnknownOperatingSystemException; +import org.xbib.rpm.lead.Architecture; +import org.xbib.rpm.lead.Os; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class RpmPackageTest { + + private String testOutputPath; + + private RpmPackage rpmPackage; + + private MavenProject project; + + @Before + public void setUp() { + testOutputPath = System.getProperty("project.build.testOutputDirectory"); + Build projectBuild = new Build(); + projectBuild.setDirectory(testOutputPath); + rpmPackage = new RpmPackage(); + project = new MavenProject(); + project.setArtifactId("test-artifact"); + project.setName("test"); + project.setVersion("1.0"); + project.setBuild(projectBuild); + PackageRpmMojo mojo = new PackageRpmMojo(); + mojo.setProject(project); + mojo.setBuildPath(String.format("%s%sbuild", testOutputPath, File.separator)); + rpmPackage.setMojo(mojo); + } + + @Test + public void nameAccessors() { + assertEquals("test-artifact", rpmPackage.getName()); + rpmPackage.setName("name"); + assertEquals("name", rpmPackage.getName()); + } + + @Test + public void versionAccessors() { + assertEquals("1.0", rpmPackage.getVersion()); + assertEquals("1.0", rpmPackage.getProjectVersion()); + rpmPackage.setVersion("2.0"); + assertEquals("2.0", rpmPackage.getVersion()); + assertEquals("2.0", rpmPackage.getProjectVersion()); + rpmPackage.setVersion("2.0-SNAPSHOT"); + assertEquals("2.0.SNAPSHOT", rpmPackage.getVersion()); + assertEquals("2.0-SNAPSHOT", rpmPackage.getProjectVersion()); + rpmPackage.setVersion(null); + assertEquals("1.0", rpmPackage.getVersion()); + assertEquals("1.0", rpmPackage.getProjectVersion()); + rpmPackage.setVersion(""); + assertEquals("1.0", rpmPackage.getVersion()); + assertEquals("1.0", rpmPackage.getProjectVersion()); + } + + @Test + public void releaseAccessors() { + assertTrue(rpmPackage.getRelease().matches("\\d+")); + rpmPackage.setRelease("release"); + assertEquals("release", rpmPackage.getRelease()); + } + + @Test + public void finalNameAccessors() { + rpmPackage.setName("name"); + rpmPackage.setVersion("1.0-SNAPSHOT"); + rpmPackage.setRelease("3"); + assertEquals("name-1.0.SNAPSHOT-3.noarch.rpm", rpmPackage.getFinalName()); + rpmPackage.setFinalName("finalname"); + assertEquals("finalname", rpmPackage.getFinalName()); + } + + @Test + public void dependenciesAccessors() { + List dependencies = new ArrayList<>(); + assertNotNull(rpmPackage.getDependencies()); + rpmPackage.setDependencies(dependencies); + assertEquals(dependencies, rpmPackage.getDependencies()); + } + + @Test + public void obsoletesAccessors() { + List obsoletes = new ArrayList<>(); + assertNotNull(rpmPackage.getObsoletes()); + rpmPackage.setObsoletes(obsoletes); + assertEquals(obsoletes, rpmPackage.getObsoletes()); + } + + @Test + public void conflictsAccessors() { + List conflicts = new ArrayList<>(); + assertNotNull(rpmPackage.getConflicts()); + rpmPackage.setConflicts(conflicts); + assertEquals(conflicts, rpmPackage.getConflicts()); + } + + @Test + public void urlAccessors() { + assertEquals(null, rpmPackage.getUrl()); + rpmPackage.setUrl("http://www.example.com/foo"); + assertEquals("http://www.example.com/foo", rpmPackage.getUrl()); + } + + @Test + public void groupAccessors() { + assertEquals(null, rpmPackage.getGroup()); + rpmPackage.setGroup("group/subgroup"); + assertEquals("group/subgroup", rpmPackage.getGroup()); + } + + @Test + public void licenseAccessors() { + assertEquals(null, rpmPackage.getLicense()); + rpmPackage.setLicense("license"); + assertEquals("license", rpmPackage.getLicense()); + } + + @Test + public void summaryAccessors() { + assertEquals(null, rpmPackage.getSummary()); + rpmPackage.setSummary("summary"); + assertEquals("summary", rpmPackage.getSummary()); + } + + @Test + public void descriptionAccessors() { + assertEquals(null, rpmPackage.getDescription()); + rpmPackage.setDescription("description"); + assertEquals("description", rpmPackage.getDescription()); + } + + @Test + public void distributionAccessors() { + assertEquals(null, rpmPackage.getDistribution()); + rpmPackage.setDistribution("distribution"); + assertEquals("distribution", rpmPackage.getDistribution()); + } + + @Test + public void architectureAccessors() throws UnknownArchitectureException { + assertEquals(Architecture.NOARCH, rpmPackage.getArchitecture()); + rpmPackage.setArchitecture("SPARC"); + assertEquals(Architecture.SPARC, rpmPackage.getArchitecture()); + } + + @Test(expected = UnknownArchitectureException.class) + public void architectureInvalidException() throws UnknownArchitectureException { + rpmPackage.setArchitecture("NONEXISTENT"); + } + + @Test(expected = UnknownArchitectureException.class) + public void architectureBlankException() throws UnknownArchitectureException { + rpmPackage.setArchitecture(""); + } + + @Test(expected = UnknownArchitectureException.class) + public void architectureNullException() throws UnknownArchitectureException { + rpmPackage.setArchitecture(null); + } + + @Test + public void operatingSystemAccessors() throws UnknownOperatingSystemException { + assertEquals(Os.LINUX, rpmPackage.getOperatingSystem()); + rpmPackage.setOperatingSystem("LINUX390"); + assertEquals(Os.LINUX390, rpmPackage.getOperatingSystem()); + } + + @Test(expected = UnknownOperatingSystemException.class) + public void operatingSystemInvalidException() throws UnknownOperatingSystemException { + rpmPackage.setOperatingSystem("NONEXISTENT"); + } + + @Test(expected = UnknownOperatingSystemException.class) + public void operatingSystemBlankException() throws UnknownOperatingSystemException { + rpmPackage.setOperatingSystem(""); + } + + @Test(expected = UnknownOperatingSystemException.class) + public void operatingSystemNullException() throws UnknownOperatingSystemException { + rpmPackage.setOperatingSystem(null); + } + + @Test + public void buildHostNameAccessors() throws Exception { + assertNotNull(rpmPackage.getBuildHostName()); + rpmPackage.setBuildHostName("buildhost"); + assertEquals("buildhost", rpmPackage.getBuildHostName()); + } + + @Test + public void packagerAccessors() { + assertEquals(null, rpmPackage.getPackager()); + rpmPackage.setPackager("packager"); + assertEquals("packager", rpmPackage.getPackager()); + } + + @Test + public void attachAccessors() { + assertEquals(true, rpmPackage.isAttach()); + rpmPackage.setAttach(false); + assertEquals(false, rpmPackage.isAttach()); + } + + @Test + public void classifierAccessors() { + assertEquals(null, rpmPackage.getClassifier()); + rpmPackage.setClassifier("classifier"); + assertEquals("classifier", rpmPackage.getClassifier()); + } + + @Test + public void rulesAccessors() { + List rules = new ArrayList<>(); + rules.add(new RpmPackageRule()); + rules.add(new RpmPackageRule()); + + rpmPackage.setRules(rules); + assertEquals(rules, rpmPackage.getRules()); + + rpmPackage.setRules(null); + assertNull(rpmPackage.getRules()); + } + + @Test + public void eventHookAccessors() { + Path scriptFile = Paths.get("samplescript.sh"); + + // pre transaction + assertEquals(null, rpmPackage.getPreTransactionScriptPath()); + rpmPackage.setPreTransactionScriptPath(scriptFile); + assertEquals(scriptFile, rpmPackage.getPreTransactionScriptPath()); + + assertEquals(null, rpmPackage.getPreTransactionProgram()); + rpmPackage.setPreTransactionProgram("/bin/sh"); + assertEquals("/bin/sh", rpmPackage.getPreTransactionProgram()); + + + // pre install + assertEquals(null, rpmPackage.getPreInstallScriptPath()); + rpmPackage.setPreInstallScriptPath(scriptFile); + assertEquals(scriptFile, rpmPackage.getPreInstallScriptPath()); + + assertEquals(null, rpmPackage.getPreInstallProgram()); + rpmPackage.setPreInstallProgram("/bin/sh"); + assertEquals("/bin/sh", rpmPackage.getPreInstallProgram()); + assertEquals(null, rpmPackage.getPostInstallScriptPath()); + rpmPackage.setPostInstallScriptPath(scriptFile); + assertEquals(scriptFile, rpmPackage.getPostInstallScriptPath()); + assertEquals(null, rpmPackage.getPostInstallProgram()); + rpmPackage.setPostInstallProgram("/bin/sh"); + assertEquals("/bin/sh", rpmPackage.getPostInstallProgram()); + assertEquals(null, rpmPackage.getPreUninstallScriptPath()); + rpmPackage.setPreUninstallScriptPath(scriptFile); + assertEquals(scriptFile, rpmPackage.getPreUninstallScriptPath()); + assertEquals(null, rpmPackage.getPreUninstallProgram()); + rpmPackage.setPreUninstallProgram("/bin/sh"); + assertEquals("/bin/sh", rpmPackage.getPreUninstallProgram()); + assertEquals(null, rpmPackage.getPostUninstallScriptPath()); + rpmPackage.setPostUninstallScriptPath(scriptFile); + assertEquals(scriptFile, rpmPackage.getPostUninstallScriptPath()); + assertEquals(null, rpmPackage.getPostUninstallProgram()); + rpmPackage.setPostUninstallProgram("/bin/sh"); + assertEquals("/bin/sh", rpmPackage.getPostUninstallProgram()); + assertEquals(null, rpmPackage.getPostTransactionScriptPath()); + rpmPackage.setPostTransactionScriptPath(scriptFile); + assertEquals(scriptFile, rpmPackage.getPostTransactionScriptPath()); + assertEquals(null, rpmPackage.getPostTransactionProgram()); + rpmPackage.setPostTransactionProgram("/bin/sh"); + assertEquals("/bin/sh", rpmPackage.getPostTransactionProgram()); + } + + @Test + public void triggerAccessors() { + List triggers = new ArrayList<>(); + assertNotNull(rpmPackage.getTriggers()); + rpmPackage.setTriggers(triggers); + assertEquals(triggers, rpmPackage.getTriggers()); + } + + @Test + public void signingKeyAccessors() { + assertEquals(null, rpmPackage.getSigningKey()); + rpmPackage.setSigningKey("key"); + assertEquals("key", rpmPackage.getSigningKey()); + assertEquals(null, rpmPackage.getSigningKeyId()); + rpmPackage.setSigningKeyId(0L); + assertEquals(new Long(0L), rpmPackage.getSigningKeyId()); + assertEquals(null, rpmPackage.getSigningKeyPassPhrase()); + rpmPackage.setSigningKeyPassPhrase("passphrase"); + assertEquals("passphrase", rpmPackage.getSigningKeyPassPhrase()); + } + + @Test + public void prefixesAccessors() { + List prefixes = new ArrayList<>(); + assertNotNull(rpmPackage.getPrefixes()); + rpmPackage.setPrefixes(null); + assertNotNull(rpmPackage.getPrefixes()); + rpmPackage.setPrefixes(prefixes); + assertEquals(prefixes, rpmPackage.getPrefixes()); + } + + @Test + public void builtinsAccessors() { + List builtins = new ArrayList<>(); + assertNotNull(rpmPackage.getBuiltins()); + rpmPackage.setBuiltins(null); + assertNotNull(rpmPackage.getBuiltins()); + rpmPackage.setBuiltins(builtins); + assertEquals(builtins, rpmPackage.getBuiltins()); + } + + @Test + public void build() throws IOException, RpmException { + project.setArtifactId("build"); + List dependencies = new ArrayList<>(); + RpmPackageAssociation dependency = new RpmPackageAssociation(); + dependency.setName("dependency"); + dependencies.add(dependency); + rpmPackage.setDependencies(dependencies); + List obsoletes = new ArrayList<>(); + RpmPackageAssociation obsolete = new RpmPackageAssociation(); + obsolete.setName("obsolete"); + obsoletes.add(obsolete); + rpmPackage.setObsoletes(obsoletes); + List conflicts = new ArrayList<>(); + RpmPackageAssociation conflict = new RpmPackageAssociation(); + conflict.setName("conflict"); + conflicts.add(conflict); + rpmPackage.setConflicts(conflicts); + List rules = new ArrayList<>(); + RpmPackageRule rule = new RpmPackageRule(); + rules.add(rule); + rpmPackage.setRules(rules); + Path scriptFile = Paths.get(String.format("%s%s/rpm/RpmPackage.sh", + testOutputPath, File.separator)); + rpmPackage.setPreTransactionScriptPath(scriptFile); + rpmPackage.setPreTransactionProgram("/bin/sh"); + rpmPackage.setPreInstallScriptPath(scriptFile); + rpmPackage.setPreInstallProgram("/bin/sh"); + rpmPackage.setPostInstallScriptPath(scriptFile); + rpmPackage.setPostInstallProgram("/bin/sh"); + rpmPackage.setPreUninstallScriptPath(scriptFile); + rpmPackage.setPreUninstallProgram("/bin/sh"); + rpmPackage.setPostUninstallScriptPath(scriptFile); + rpmPackage.setPostUninstallProgram("/bin/sh"); + rpmPackage.setPostTransactionScriptPath(scriptFile); + rpmPackage.setPostTransactionProgram("/bin/sh"); + rpmPackage.build(); + String rpmFileName = String.format("%s%s%s", testOutputPath, File.separator, rpmPackage.getFinalName()); + assertEquals(true, new File(rpmFileName).exists()); + } + + @Test + public void buildSecondaryAttachmentNameDifference() throws IOException, RpmException { + rpmPackage.setName("buildSecondaryAttachment"); + rpmPackage.build(); + String rpmFileName = String.format("%s%s%s", testOutputPath, File.separator, rpmPackage.getFinalName()); + assertEquals(true, new File(rpmFileName).exists()); + } + + @Test + public void buildSecondaryAttachmentVersionDifference() throws IOException, RpmException { + rpmPackage.setVersion("2.0"); + rpmPackage.build(); + String rpmFileName = String.format("%s%s%s", testOutputPath, File.separator, rpmPackage.getFinalName()); + assertEquals(true, new File(rpmFileName).exists()); + } + + @Test + public void buildSecondaryAttachmentNameAndVersionDifference() throws IOException, RpmException { + rpmPackage.setName("buildSecondaryAttachmentNameAndVersionDifference"); + rpmPackage.setVersion("2.0"); + rpmPackage.build(); + String rpmFileName = String.format("%s%s%s", testOutputPath, File.separator, rpmPackage.getFinalName()); + assertEquals(true, new File(rpmFileName).exists()); + } + + @Test + public void buildWithoutAttachment() throws IOException, RpmException { + project.setArtifactId("buildWithoutAttachment"); + rpmPackage.setAttach(false); + rpmPackage.build(); + String rpmFileName = String.format("%s%s%s", testOutputPath, File.separator, rpmPackage.getFinalName()); + assertEquals(true, new File(rpmFileName).exists()); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRendererTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRendererTest.java new file mode 100644 index 0000000..f621706 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmScriptTemplateRendererTest.java @@ -0,0 +1,62 @@ +package org.xbib.maven.plugin.rpm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * + */ +public class RpmScriptTemplateRendererTest { + private String testOutputPath; + + @Before + public void setUp() { + this.testOutputPath = System.getProperty("project.build.testOutputDirectory"); + } + + @Test + public void render() throws Exception { + Path templateScriptFile = Paths.get( + String.format("%s/rpm/RpmScriptTemplateRenderer-template", + this.testOutputPath)); + Path expectedScriptFile = Paths.get( + String.format("%s/rpm/RpmScriptTemplateRenderer-template-expected", + this.testOutputPath)); + Path actualScriptFile = Paths.get( + String.format("%s/rpm/RpmScriptTemplateRenderer-template-actual", + this.testOutputPath)); + RpmScriptTemplateRenderer renderer = new RpmScriptTemplateRenderer(); + renderer.addParameter("testdata1", true); + renderer.addParameter("testdata2", "test"); + renderer.addParameter("testdata3", 123); + //assertFalse(Files.exists(actualScriptFile)); + renderer.render(templateScriptFile, actualScriptFile); + assertTrue(Files.exists(actualScriptFile)); + char[] buff = new char[1024]; + StringBuilder stringBuilder; + int bytesRead; + stringBuilder = new StringBuilder(); + try (Reader reader = Files.newBufferedReader(actualScriptFile)) { + while (-1 != (bytesRead = reader.read(buff))) { + stringBuilder.append(buff, 0, bytesRead); + } + } + String actualTemplateContents = stringBuilder.toString(); + stringBuilder = new StringBuilder(); + try (Reader reader = Files.newBufferedReader(expectedScriptFile)) { + while (-1 != (bytesRead = reader.read(buff))) { + stringBuilder.append(buff, 0, bytesRead); + } + } + String expectedTemplateContents = stringBuilder.toString(); + assertEquals(expectedTemplateContents, actualTemplateContents); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmTriggerTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmTriggerTest.java new file mode 100644 index 0000000..80ee706 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/RpmTriggerTest.java @@ -0,0 +1,50 @@ +package org.xbib.maven.plugin.rpm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class RpmTriggerTest { + + @Test + public void accessors() { + List dependencies = new ArrayList<>(); + RpmTrigger trigger = new RpmTrigger(); + Path triggerScript = Paths.get("/path/to/file"); + assertNull(trigger.getPreInstallScriptPath()); + trigger.setPreInstallScriptPath(triggerScript); + assertEquals(triggerScript, trigger.getPreInstallScriptPath()); + assertNull(trigger.getPreInstallProgram()); + trigger.setPreInstallProgram("/bin/sh"); + assertEquals("/bin/sh", trigger.getPreInstallProgram()); + assertNull(trigger.getPostInstallScriptPath()); + trigger.setPostInstallScriptPath(triggerScript); + assertEquals(triggerScript, trigger.getPostInstallScriptPath()); + assertNull(trigger.getPostInstallProgram()); + trigger.setPostInstallProgram("/bin/sh"); + assertEquals("/bin/sh", trigger.getPostInstallProgram()); + assertNull(trigger.getPreUninstallScriptPath()); + trigger.setPreUninstallScriptPath(triggerScript); + assertEquals(triggerScript, trigger.getPreUninstallScriptPath()); + assertNull(trigger.getPreUninstallProgram()); + trigger.setPreUninstallProgram("/bin/sh"); + assertEquals("/bin/sh", trigger.getPreUninstallProgram()); + assertNull(trigger.getPostUninstallScriptPath()); + trigger.setPostUninstallScriptPath(triggerScript); + assertEquals(triggerScript, trigger.getPostUninstallScriptPath()); + assertNull(trigger.getPostUninstallProgram()); + trigger.setPostUninstallProgram("/bin/sh"); + assertEquals("/bin/sh", trigger.getPostUninstallProgram()); + trigger.setDependencies(dependencies); + assertEquals(dependencies, trigger.getDependencies()); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojoTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojoTest.java new file mode 100644 index 0000000..4332a81 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/AbstractRpmMojoTest.java @@ -0,0 +1,172 @@ +package org.xbib.maven.plugin.rpm.mojo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.maven.model.Build; +import org.apache.maven.model.License; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.Test; +import org.xbib.maven.plugin.rpm.MockMojo; +import org.xbib.maven.plugin.rpm.RpmPackage; +import org.xbib.maven.plugin.rpm.RpmScriptTemplateRenderer; +import org.xbib.rpm.exception.InvalidPathException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * + */ +public class AbstractRpmMojoTest { + + private String testOutputPath; + + private MockMojo mojo; + private MavenProject project; + + @Before + public void setUp() { + testOutputPath = System.getProperty("project.build.testOutputDirectory"); + Build projectBuild = new Build(); + projectBuild.setDirectory("target"); + project = new MavenProject(); + project.setGroupId("org.xbib"); + project.setArtifactId("test-artifact"); + project.setName("test"); + project.setVersion("1.0-SNAPSHOT"); + project.setUrl("http://www.example.com"); + project.setBuild(projectBuild); + List licenses = new ArrayList<>(); + License license1 = new License(); + license1.setName("GPL"); + licenses.add(license1); + License license2 = new License(); + license2.setName("LGPL"); + licenses.add(license2); + project.setLicenses(licenses); + mojo = new MockMojo(); + mojo.setProject(project); + } + + @Test + public void projectAccessors() { + mojo.setProject(null); + assertNull(mojo.getProject()); + mojo.setProject(project); + assertEquals(project, mojo.getProject()); + assertEquals("test-artifact", mojo.getProjectArtifactId()); + assertEquals("1.0-SNAPSHOT", mojo.getProjectVersion()); + assertEquals("http://www.example.com", mojo.getProjectUrl()); + assertEquals("GPL, LGPL", mojo.getCollapsedProjectLicense()); + assertEquals("target", mojo.getBuildDirectory()); + } + + @Test + public void templateRenderer() throws IOException { + RpmScriptTemplateRenderer renderer = mojo.getTemplateRenderer(); + assertNotNull(renderer); + Path templateScriptFile = + Paths.get(testOutputPath, "mojo/AbstractRpmMojo-template"); + Path expectedScriptFile = + Paths.get(testOutputPath, "mojo/AbstractRpmMojo-template-expected"); + Path actualScriptFile = + Paths.get(testOutputPath, "mojo/AbstractRpmMojo-template-actual"); + //assertFalse(Files.exists(actualScriptFile)); + renderer.render(templateScriptFile, actualScriptFile); + assertTrue(Files.exists(actualScriptFile)); + char[] buff = new char[1024]; + StringBuilder stringBuilder; + int bytesRead; + stringBuilder = new StringBuilder(); + try (BufferedReader reader = Files.newBufferedReader(actualScriptFile)) { + while (-1 != (bytesRead = reader.read(buff))) { + stringBuilder.append(buff, 0, bytesRead); + } + } + String actualTemplateContents = stringBuilder.toString(); + stringBuilder = new StringBuilder(); + try (BufferedReader reader = Files.newBufferedReader(expectedScriptFile)) { + while (-1 != (bytesRead = reader.read(buff))) { + stringBuilder.append(buff, 0, bytesRead); + } + } + String expectedTemplateContents = stringBuilder.toString(); + assertEquals(expectedTemplateContents, actualTemplateContents); + assertEquals(renderer, mojo.getTemplateRenderer()); + } + + @Test + public void projectArtifacts() { + Path artifact = Paths.get(String.format("%s/artifact.rpm", testOutputPath)); + mojo.setPrimaryArtifact(artifact, "test"); + mojo.addSecondaryArtifact(artifact, "secondary-artifact", "1.0", "test"); + mojo.addSecondaryArtifact(artifact, "secondary-artifact", "1.0", null); + assertNotNull(project.getArtifact()); + assertEquals(2, project.getAttachedArtifacts().size()); + } + + @Test + public void buildPath() throws InvalidPathException { + mojo.setBuildPath(testOutputPath); + assertEquals(testOutputPath, mojo.getBuildPath()); + } + + @Test + public void packages() { + List packages = new ArrayList<>(); + packages.add(new RpmPackage()); + mojo.setPackages(packages); + assertEquals(packages, mojo.getPackages()); + } + + @Test + public void defaults() { + assertEquals(0644, mojo.getDefaultFileMode()); + mojo.setDefaultFileMode(0755); + assertEquals(0755, mojo.getDefaultFileMode()); + assertEquals("root", mojo.getDefaultOwner()); + mojo.setDefaultOwner("nobody"); + assertEquals("nobody", mojo.getDefaultOwner()); + assertEquals("root", mojo.getDefaultGroup()); + mojo.setDefaultGroup("nobody"); + assertEquals("nobody", mojo.getDefaultGroup()); + assertEquals(File.separator, mojo.getDefaultDestination()); + mojo.setDefaultDestination(String.format("%sdestination", File.separator)); + assertEquals(String.format("%sdestination", File.separator), mojo.getDefaultDestination()); + } + + @Test + public void excludes() { + List excludes = new ArrayList<>(); + mojo.setExcludes(excludes); + assertEquals(excludes, mojo.getExcludes()); + } + + @Test + public void checkingForExtraFiles() { + mojo.setPerformCheckingForExtraFiles(true); + assertTrue(mojo.isPerformCheckingForExtraFiles()); + mojo.setPerformCheckingForExtraFiles(false); + assertFalse(mojo.isPerformCheckingForExtraFiles()); + } + + @Test + public void scanMasterFiles() { + mojo.setBuildPath(String.format("%s%sbuild", testOutputPath, File.separator)); + mojo.scanMasterFiles(); + Set masterFiles = mojo.getMasterFiles(); + assertEquals(65, masterFiles.size()); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojoTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojoTest.java new file mode 100644 index 0000000..bfb0689 --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/ListFilesRpmMojoTest.java @@ -0,0 +1,63 @@ +package org.xbib.maven.plugin.rpm.mojo; + +import org.apache.maven.model.Build; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.Test; +import org.xbib.maven.plugin.rpm.RpmPackage; +import org.xbib.maven.plugin.rpm.RpmPackageRule; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class ListFilesRpmMojoTest { + + private String testOutputPath; + private ListFilesRpmMojo mojo; + private RpmPackageRule packageRule; + + @Before + public void setUp() { + this.testOutputPath = System.getProperty("project.build.testOutputDirectory"); + Build projectBuild = new Build(); + projectBuild.setDirectory(this.testOutputPath); + MavenProject project = new MavenProject(); + project.setGroupId("uk.co.codezen"); + project.setArtifactId("listfilesmojo-artifact"); + project.setName("test"); + project.setVersion("1.0-SNAPSHOT"); + project.setUrl("http://www.example.com"); + project.setBuild(projectBuild); + this.mojo = new ListFilesRpmMojo(); + this.mojo.setProject(project); + List packageRules = new ArrayList<>(); + this.packageRule = new RpmPackageRule(); + packageRules.add(this.packageRule); + RpmPackage rpmPackage = new RpmPackage(); + rpmPackage.setRules(packageRules); + List packages = new ArrayList<>(); + packages.add(rpmPackage); + this.mojo.setPackages(packages); + this.mojo.setBuildPath(String.format("%s%sbuild", this.testOutputPath, File.separator)); + } + + @Test + public void packageRpm() throws MojoExecutionException { + List includes = new ArrayList<>(); + includes.add("**"); + packageRule.setIncludes(includes); + this.mojo.execute(); + } + + @Test + public void packageRpmMissedFiles() throws MojoExecutionException { + List includes = new ArrayList<>(); + packageRule.setIncludes(includes); + this.mojo.execute(); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojoTest.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojoTest.java new file mode 100644 index 0000000..296a85b --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/PackageRpmMojoTest.java @@ -0,0 +1,112 @@ +package org.xbib.maven.plugin.rpm.mojo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.apache.maven.model.Build; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.Test; +import org.xbib.maven.plugin.rpm.RpmPackage; +import org.xbib.maven.plugin.rpm.RpmPackageRule; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class PackageRpmMojoTest { + + private PackageRpmMojo mojo; + + private MavenProject project; + + private RpmPackageRule packageRule; + + @Before + public void setUp() { + // Test output path + String testOutputPath = System.getProperty("project.build.testOutputDirectory"); + Build projectBuild = new Build(); + projectBuild.setDirectory(testOutputPath); + project = new MavenProject(); + project.setGroupId("uk.co.codezen"); + project.setArtifactId("packagerpmmojo-artifact"); + project.setName("test"); + project.setUrl("http://www.example.com"); + project.setBuild(projectBuild); + project.setPackaging("rpm"); + mojo = new PackageRpmMojo(); + mojo.setProject(project); + List packageRules = new ArrayList<>(); + packageRule = new RpmPackageRule(); + packageRules.add(packageRule); + RpmPackage rpmPackage = new RpmPackage(); + rpmPackage.setRules(packageRules); + List packages = new ArrayList<>(); + packages.add(rpmPackage); + mojo.setPackages(packages); + mojo.setBuildPath(String.format("%s%sbuild", testOutputPath, File.separator)); + } + + @Test + public void packageRpm() throws MojoExecutionException { + project.setVersion("1.0-SNAPSHOT"); + List includes = new ArrayList<>(); + includes.add("**"); + packageRule.setIncludes(includes); + mojo.execute(); + assertEquals(true, project.getArtifact().getFile().exists()); + } + + @Test + public void packageRpmNonRpmPackagingType() throws MojoExecutionException { + project.setPackaging("jar"); + project.setVersion("1.1-SNAPSHOT"); + List includes = new ArrayList<>(); + includes.add("**"); + packageRule.setIncludes(includes); + mojo.execute(); + assertNull(project.getArtifact()); + } + + @Test(expected = MojoExecutionException.class) + public void packageRpmMissedFiles() throws MojoExecutionException { + project.setVersion("2.0-SNAPSHOT"); + List includes = new ArrayList<>(); + packageRule.setIncludes(includes); + mojo.execute(); + } + + @Test + public void packageRpmMissedFilesWithoutChecks() throws MojoExecutionException { + mojo.setPerformCheckingForExtraFiles(false); + project.setVersion("3.0-SNAPSHOT"); + List includes = new ArrayList<>(); + includes.add("**/*.php"); + packageRule.setIncludes(includes); + mojo.execute(); + } + + @Test(expected = MojoExecutionException.class) + public void packageRpmNoFilesPackaged() throws MojoExecutionException { + mojo.setPerformCheckingForExtraFiles(false); + project.setVersion("4.0-SNAPSHOT"); + List includes = new ArrayList<>(); + packageRule.setIncludes(includes); + mojo.execute(); + } + + @Test + public void packageRpmNoFilesPackagedNoPackages() throws MojoExecutionException { + mojo.setPackages(new ArrayList<>()); + mojo.setPerformCheckingForExtraFiles(false); + project.setVersion("5.0-SNAPSHOT"); + List includes = new ArrayList<>(); + packageRule.setIncludes(includes); + mojo.execute(); + } +} diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/package-info.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/package-info.java new file mode 100644 index 0000000..425d0bb --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/mojo/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.maven.plugin.rpm.mojo; diff --git a/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/package-info.java b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/package-info.java new file mode 100644 index 0000000..ccdc9ea --- /dev/null +++ b/maven-plugin-rpm/src/test/java/org/xbib/maven/plugin/rpm/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.maven.plugin.rpm; diff --git a/maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template b/maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template new file mode 100644 index 0000000..5deb982 --- /dev/null +++ b/maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template @@ -0,0 +1,2 @@ +@{project.artifactId} +@{project.version} \ No newline at end of file diff --git a/maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template-expected b/maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template-expected new file mode 100644 index 0000000..dae4def --- /dev/null +++ b/maven-plugin-rpm/src/test/resources/mojo/AbstractRpmMojo-template-expected @@ -0,0 +1,2 @@ +test-artifact +1.0-SNAPSHOT \ No newline at end of file diff --git a/maven-plugin-rpm/src/test/resources/rpm/RpmPackage.sh b/maven-plugin-rpm/src/test/resources/rpm/RpmPackage.sh new file mode 100644 index 0000000..65561e0 --- /dev/null +++ b/maven-plugin-rpm/src/test/resources/rpm/RpmPackage.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo 'test' diff --git a/maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template b/maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template new file mode 100644 index 0000000..a0b06c9 --- /dev/null +++ b/maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template @@ -0,0 +1,412 @@ +{ + "cols": [ + "names", + "numbers" + ], + "data": [ + [ + "Richardson", + "070 0141 7377" + ], + [ + "Travis", + "(01277) 41323" + ], + [ + "Hewitt", + "(01845) 75740" + ], + [ + "Powers", + "0800 955 8823" + ], + [ + "Wells", + "07134 104963" + ], + [ + "Emerson", + "(010181) 05706" + ], + [ + "Miranda", + "(015368) 43418" + ], + [ + "Chase", + "0845 46 44" + ], + [ + "Jarvis", + "0800 067 0993" + ], + [ + "Schmidt", + "0962 636 6120" + ], + [ + "Burgess", + "0800 664 3974" + ], + [ + "Head", + "(021) 3771 0662" + ], + [ + "Newman", + "070 3256 9858" + ], + [ + "Mccray", + "0800 853 1374" + ], + [ + "Barton", + "070 8984 6700" + ], + [ + "Jacobs", + "0800 462462" + ], + [ + "Parsons", + "0845 46 48" + ], + [ + "Lucas", + "0500 650777" + ], + [ + "Reynolds", + "07624 403213" + ], + [ + "Sims", + "0500 038207" + ], + [ + "Coleman", + "(01543) 63074" + ], + [ + "Mcfarland", + "0800 758 9549" + ], + [ + "Munoz", + "0800 1111" + ], + [ + "Carroll", + "070 1133 5502" + ], + [ + "Mendoza", + "0800 376 7443" + ], + [ + "Watson", + "076 6218 7445" + ], + [ + "Norton", + "070 9284 5408" + ], + [ + "Avery", + "0500 149900" + ], + [ + "Caldwell", + "0817 211 7977" + ], + [ + "Mcguire", + "070 8334 8293" + ], + [ + "Fletcher", + "(028) 9545 6817" + ], + [ + "Thornton", + "(024) 1438 7515" + ], + [ + "Johns", + "0959 575 0496" + ], + [ + "Mcbride", + "(01874) 93627" + ], + [ + "Kelly", + "0912 501 7349" + ], + [ + "Landry", + "0845 46 48" + ], + [ + "Crane", + "07349 706594" + ], + [ + "Olson", + "0800 1111" + ], + [ + "Fischer", + "0861 608 5034" + ], + [ + "Nicholson", + "0306 015 2445" + ], + [ + "Massey", + "0500 025973" + ], + [ + "Hale", + "0812 226 2347" + ], + [ + "Burke", + "(025) 1598 0821" + ], + [ + "Abbott", + "070 8982 8414" + ], + [ + "Mitchell", + "(01632) 14975" + ], + [ + "Hodge", + "(029) 2994 2625" + ], + [ + "Crane", + "056 1056 3821" + ], + [ + "Calhoun", + "0800 968 5533" + ], + [ + "Best", + "0800 038391" + ], + [ + "Riggs", + "(01029) 333344" + ], + [ + "Mayer", + "0800 861 7909" + ], + [ + "King", + "07624 146452" + ], + [ + "Baker", + "(01083) 26967" + ], + [ + "Green", + "07724 947739" + ], + [ + "Cruz", + "(011458) 70326" + ], + [ + "Allison", + "(01705) 919378" + ], + [ + "Rose", + "(01017) 49706" + ], + [ + "Shields", + "0800 1111" + ], + [ + "Huber", + "(016977) 9693" + ], + [ + "Bright", + "0800 1111" + ], + [ + "Livingston", + "0800 169193" + ], + [ + "Lancaster", + "0800 289264" + ], + [ + "Mckay", + "0800 962 3480" + ], + [ + "Blair", + "(01217) 458780" + ], + [ + "Barr", + "0386 690 7830" + ], + [ + "Anderson", + "07624 312048" + ], + [ + "Roberson", + "(028) 0111 4691" + ], + [ + "Cleveland", + "0800 505433" + ], + [ + "Park", + "055 3423 0881" + ], + [ + "Garcia", + "0969 272 6479" + ], + [ + "Wiggins", + "(015654) 89655" + ], + [ + "Russo", + "(01976) 56815" + ], + [ + "Matthews", + "(023) 2893 4085" + ], + [ + "Hunt", + "0893 822 7541" + ], + [ + "Vega", + "07085 303615" + ], + [ + "Conner", + "0955 253 7036" + ], + [ + "Bonner", + "(016426) 84607" + ], + [ + "Matthews", + "(010036) 24040" + ], + [ + "Cameron", + "0845 46 46" + ], + [ + "Camacho", + "0800 321251" + ], + [ + "Fisher", + "0845 46 47" + ], + [ + "Wilder", + "055 8041 9669" + ], + [ + "Mitchell", + "(0161) 018 0173" + ], + [ + "Dudley", + "0800 1111" + ], + [ + "Clarke", + "07624 094367" + ], + [ + "Estrada", + "(01166) 83749" + ], + [ + "Cline", + "0845 46 40" + ], + [ + "Douglas", + "0500 729606" + ], + [ + "Wagner", + "(016977) 7914" + ], + [ + "Anthony", + "0800 1111" + ], + [ + "Snider", + "076 9687 9688" + ], + [ + "Diaz", + "0500 946224" + ], + [ + "Townsend", + "0800 062954" + ], + [ + "Simmons", + "0500 485218" + ], + [ + "Evans", + "0800 412478" + ], + [ + "Walter", + "07624 770930" + ], + [ + "Porter", + "0500 705224" + ], + [ + "House", + "076 3668 6834" + ], + [ + "Rhodes", + "(01774) 45010" + ], + [ + "Park", + "0897 987 3572" + ] + ], + + @{testdata1}, + @{testdata2}, + @{testdata3} +} \ No newline at end of file diff --git a/maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template-expected b/maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template-expected new file mode 100644 index 0000000..f9aecab --- /dev/null +++ b/maven-plugin-rpm/src/test/resources/rpm/RpmScriptTemplateRenderer-template-expected @@ -0,0 +1,412 @@ +{ + "cols": [ + "names", + "numbers" + ], + "data": [ + [ + "Richardson", + "070 0141 7377" + ], + [ + "Travis", + "(01277) 41323" + ], + [ + "Hewitt", + "(01845) 75740" + ], + [ + "Powers", + "0800 955 8823" + ], + [ + "Wells", + "07134 104963" + ], + [ + "Emerson", + "(010181) 05706" + ], + [ + "Miranda", + "(015368) 43418" + ], + [ + "Chase", + "0845 46 44" + ], + [ + "Jarvis", + "0800 067 0993" + ], + [ + "Schmidt", + "0962 636 6120" + ], + [ + "Burgess", + "0800 664 3974" + ], + [ + "Head", + "(021) 3771 0662" + ], + [ + "Newman", + "070 3256 9858" + ], + [ + "Mccray", + "0800 853 1374" + ], + [ + "Barton", + "070 8984 6700" + ], + [ + "Jacobs", + "0800 462462" + ], + [ + "Parsons", + "0845 46 48" + ], + [ + "Lucas", + "0500 650777" + ], + [ + "Reynolds", + "07624 403213" + ], + [ + "Sims", + "0500 038207" + ], + [ + "Coleman", + "(01543) 63074" + ], + [ + "Mcfarland", + "0800 758 9549" + ], + [ + "Munoz", + "0800 1111" + ], + [ + "Carroll", + "070 1133 5502" + ], + [ + "Mendoza", + "0800 376 7443" + ], + [ + "Watson", + "076 6218 7445" + ], + [ + "Norton", + "070 9284 5408" + ], + [ + "Avery", + "0500 149900" + ], + [ + "Caldwell", + "0817 211 7977" + ], + [ + "Mcguire", + "070 8334 8293" + ], + [ + "Fletcher", + "(028) 9545 6817" + ], + [ + "Thornton", + "(024) 1438 7515" + ], + [ + "Johns", + "0959 575 0496" + ], + [ + "Mcbride", + "(01874) 93627" + ], + [ + "Kelly", + "0912 501 7349" + ], + [ + "Landry", + "0845 46 48" + ], + [ + "Crane", + "07349 706594" + ], + [ + "Olson", + "0800 1111" + ], + [ + "Fischer", + "0861 608 5034" + ], + [ + "Nicholson", + "0306 015 2445" + ], + [ + "Massey", + "0500 025973" + ], + [ + "Hale", + "0812 226 2347" + ], + [ + "Burke", + "(025) 1598 0821" + ], + [ + "Abbott", + "070 8982 8414" + ], + [ + "Mitchell", + "(01632) 14975" + ], + [ + "Hodge", + "(029) 2994 2625" + ], + [ + "Crane", + "056 1056 3821" + ], + [ + "Calhoun", + "0800 968 5533" + ], + [ + "Best", + "0800 038391" + ], + [ + "Riggs", + "(01029) 333344" + ], + [ + "Mayer", + "0800 861 7909" + ], + [ + "King", + "07624 146452" + ], + [ + "Baker", + "(01083) 26967" + ], + [ + "Green", + "07724 947739" + ], + [ + "Cruz", + "(011458) 70326" + ], + [ + "Allison", + "(01705) 919378" + ], + [ + "Rose", + "(01017) 49706" + ], + [ + "Shields", + "0800 1111" + ], + [ + "Huber", + "(016977) 9693" + ], + [ + "Bright", + "0800 1111" + ], + [ + "Livingston", + "0800 169193" + ], + [ + "Lancaster", + "0800 289264" + ], + [ + "Mckay", + "0800 962 3480" + ], + [ + "Blair", + "(01217) 458780" + ], + [ + "Barr", + "0386 690 7830" + ], + [ + "Anderson", + "07624 312048" + ], + [ + "Roberson", + "(028) 0111 4691" + ], + [ + "Cleveland", + "0800 505433" + ], + [ + "Park", + "055 3423 0881" + ], + [ + "Garcia", + "0969 272 6479" + ], + [ + "Wiggins", + "(015654) 89655" + ], + [ + "Russo", + "(01976) 56815" + ], + [ + "Matthews", + "(023) 2893 4085" + ], + [ + "Hunt", + "0893 822 7541" + ], + [ + "Vega", + "07085 303615" + ], + [ + "Conner", + "0955 253 7036" + ], + [ + "Bonner", + "(016426) 84607" + ], + [ + "Matthews", + "(010036) 24040" + ], + [ + "Cameron", + "0845 46 46" + ], + [ + "Camacho", + "0800 321251" + ], + [ + "Fisher", + "0845 46 47" + ], + [ + "Wilder", + "055 8041 9669" + ], + [ + "Mitchell", + "(0161) 018 0173" + ], + [ + "Dudley", + "0800 1111" + ], + [ + "Clarke", + "07624 094367" + ], + [ + "Estrada", + "(01166) 83749" + ], + [ + "Cline", + "0845 46 40" + ], + [ + "Douglas", + "0500 729606" + ], + [ + "Wagner", + "(016977) 7914" + ], + [ + "Anthony", + "0800 1111" + ], + [ + "Snider", + "076 9687 9688" + ], + [ + "Diaz", + "0500 946224" + ], + [ + "Townsend", + "0800 062954" + ], + [ + "Simmons", + "0500 485218" + ], + [ + "Evans", + "0800 412478" + ], + [ + "Walter", + "07624 770930" + ], + [ + "Porter", + "0500 705224" + ], + [ + "House", + "076 3668 6834" + ], + [ + "Rhodes", + "(01774) 45010" + ], + [ + "Park", + "0897 987 3572" + ] + ], + + true, + test, + 123 +} \ No newline at end of file diff --git a/rpm-ant/NOTICE.txt b/rpm-ant/NOTICE.txt new file mode 100644 index 0000000..c474885 --- /dev/null +++ b/rpm-ant/NOTICE.txt @@ -0,0 +1,13 @@ +This is a derived work of + +http://redline-rpm.org/index.html + +licensed under MIT License: + +Copyright (c) 2007-2015 FreeCompany + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/rpm-ant/build.gradle b/rpm-ant/build.gradle new file mode 100644 index 0000000..319054e --- /dev/null +++ b/rpm-ant/build.gradle @@ -0,0 +1,4 @@ +dependencies { + compile project(':rpm-core') + compile "org.apache.ant:ant:${project.property('ant.version')}" +} diff --git a/rpm-ant/config/checkstyle/checkstyle.xml b/rpm-ant/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..55e59d2 --- /dev/null +++ b/rpm-ant/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/BuiltIn.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/BuiltIn.java new file mode 100644 index 0000000..5e187fc --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/BuiltIn.java @@ -0,0 +1,26 @@ +package org.xbib.rpm.ant; + +import org.apache.tools.ant.Project; + +/** + * + */ +public class BuiltIn { + + private final Project project; + + private String name; + + public BuiltIn(Project project) { + this.project = project; + } + + public void addText(String text) { + this.name = project.replaceProperties(text); + } + + public String getText() { + return name; + } + +} diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/Conflicts.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/Conflicts.java new file mode 100644 index 0000000..898340b --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/Conflicts.java @@ -0,0 +1,66 @@ +package org.xbib.rpm.ant; + +import org.apache.tools.ant.types.EnumeratedAttribute; +import org.xbib.rpm.format.Flags; + +/** + * A conflict with a particular version of an RPM package. + */ +public class Conflicts { + + private String name; + + private String version = ""; + + private int comparison = 0; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getComparison() { + if (0 == comparison && 0 < version.length()) { + return Flags.GREATER | Flags.EQUAL; + } + if (0 == version.length()) { + return 0; + } + return this.comparison; + } + + public void setComparison(ComparisonEnum comparisonEnum) { + String comparisonValue = comparisonEnum.getValue(); + if ("equal".equals(comparisonValue)) { + this.comparison = Flags.EQUAL; + } else if ("greater".equals(comparisonValue)) { + this.comparison = Flags.GREATER; + } else if ("greater|equal".equals(comparisonValue)) { + this.comparison = Flags.GREATER | Flags.EQUAL; + } else if ("less".equals(comparisonValue)) { + this.comparison = Flags.LESS; + } else { + this.comparison = Flags.LESS | Flags.EQUAL; + } + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + /** + * Enumerated attribute with the values "equal", "greater", "greater|equal", "less" and "less|equal". + */ + public static class ComparisonEnum extends EnumeratedAttribute { + public String[] getValues() { + return new String[]{"equal", "greater", "greater|equal", "less", "less|equal"}; + } + } +} diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/Depends.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/Depends.java new file mode 100644 index 0000000..b66d26a --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/Depends.java @@ -0,0 +1,66 @@ +package org.xbib.rpm.ant; + +import org.apache.tools.ant.types.EnumeratedAttribute; +import org.xbib.rpm.format.Flags; + +/** + * + */ +public class Depends { + + private String name; + + private String version = ""; + + private int comparison = 0; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getComparison() { + if (0 == comparison && 0 < version.length()) { + return Flags.GREATER | Flags.EQUAL; + } + if (0 == version.length()) { + return 0; + } + return this.comparison; + } + + public void setComparison(ComparisonEnum comparisonEnum) { + String comparisonValue = comparisonEnum.getValue(); + if ("equal".equals(comparisonValue)) { + this.comparison = Flags.EQUAL; + } else if ("greater".equals(comparisonValue)) { + this.comparison = Flags.GREATER; + } else if ("greater|equal".equals(comparisonValue)) { + this.comparison = Flags.GREATER | Flags.EQUAL; + } else if ("less".equals(comparisonValue)) { + this.comparison = Flags.LESS; + } else { + this.comparison = Flags.LESS | Flags.EQUAL; + } + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + /** + * Enumerated attribute with the values "equal", "greater", "greater|equal", "less" and "less|equal". + */ + public static class ComparisonEnum extends EnumeratedAttribute { + public String[] getValues() { + return new String[]{"equal", "greater", "greater|equal", "less", "less|equal"}; + } + } +} diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/Obsoletes.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/Obsoletes.java new file mode 100644 index 0000000..c5558bd --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/Obsoletes.java @@ -0,0 +1,66 @@ +package org.xbib.rpm.ant; + +import org.apache.tools.ant.types.EnumeratedAttribute; +import org.xbib.rpm.format.Flags; + +/** + * Object describing an obsoletion of a particular version of an RPM package. + */ +public class Obsoletes { + + private String name; + + private String version = ""; + + private int comparison = 0; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getComparison() { + if (0 == comparison && 0 < version.length()) { + return Flags.GREATER | Flags.EQUAL; + } + if (0 == version.length()) { + return 0; + } + return this.comparison; + } + + public void setComparison(ComparisonEnum comparisonEnum) { + String comparisonValue = comparisonEnum.getValue(); + if ("equal".equals(comparisonValue)) { + this.comparison = Flags.EQUAL; + } else if ("greater".equals(comparisonValue)) { + this.comparison = Flags.GREATER; + } else if ("greater|equal".equals(comparisonValue)) { + this.comparison = Flags.GREATER | Flags.EQUAL; + } else if ("less".equals(comparisonValue)) { + this.comparison = Flags.LESS; + } else { // must be ( comparisonValue.equals( "less|equal")) + this.comparison = Flags.LESS | Flags.EQUAL; + } + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + /** + * Enumerated attribute with the values "equal", "greater", "greater|equal", "less" and "less|equal". + */ + public static class ComparisonEnum extends EnumeratedAttribute { + public String[] getValues() { + return new String[]{"equal", "greater", "greater|equal", "less", "less|equal"}; + } + } +} diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/Provides.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/Provides.java new file mode 100644 index 0000000..1a317a2 --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/Provides.java @@ -0,0 +1,28 @@ +package org.xbib.rpm.ant; + +/** + * Object describing a provided capability (virtual package). + */ +public class Provides { + + private String name; + + private String version = ""; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + +} diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/RpmFileSet.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/RpmFileSet.java new file mode 100644 index 0000000..07a99b5 --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/RpmFileSet.java @@ -0,0 +1,147 @@ +package org.xbib.rpm.ant; + +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.types.TarFileSet; +import org.xbib.rpm.payload.Directive; + +import java.util.EnumSet; + +/** + * A {@code RpmFileSet} is a {@link FileSet} to support RPM directives that can't be expressed + * using ant's built-in {@code FileSet} classes. + */ +public class RpmFileSet extends TarFileSet { + + private EnumSet directives = EnumSet.of(Directive.NONE); + + /** + * Constructor for {@code RpmFileSet}. + */ + public RpmFileSet() { + super(); + } + + /** + * Constructor using a fileset arguement. + * + * @param fileset the {@link FileSet} to use + */ + protected RpmFileSet(FileSet fileset) { + super(fileset); + } + + /** + * Constructor using a archive fileset argument. + * + * @param fileset the {@code RpmFileSet} to use + */ + protected RpmFileSet(RpmFileSet fileset) { + super(fileset); + directives = fileset.getDirectives(); + } + + public EnumSet getDirectives() { + return directives; + } + + /** + * Supports {@code %ghost} directive, used to flag the specified file as being a ghost file. + * By adding this directive to the line containing a file, RPM will know about the ghosted file, but will + * not add it to the package. + * Permitted values for this directive are: + *
    + *
  • {@code true} (equivalent to specifying {@code %ghost} + *
  • {@code false} (equivalent to omitting {@code %ghost}) + *
+ * + * @param ghost the ghost + */ + public void setGhost(boolean ghost) { + checkRpmFileSetAttributesAllowed(); + if (ghost) { + directives.add(Directive.GHOST); + } else { + directives.remove(Directive.GHOST); + } + } + + /** + * Supports RPM's {@code %config} directive, used to flag the specified file as being a configuration file. + * RPM performs additional processing for config files when packages are erased, and during installations + * and upgrades. + * Permitted values for this directive are: + *
    + *
  • {@code true} (equivalent to specifying {@code %config} + *
  • {@code false} (equivalent to omitting {@code %config}) + *
+ * + * @param config the config + */ + public void setConfig(boolean config) { + checkRpmFileSetAttributesAllowed(); + if (config) { + directives.add(Directive.CONFIG); + } else { + directives.remove(Directive.CONFIG); + } + } + + /** + * Supports RPM's {@code %config(noreplace)} directive. This directive modifies how RPM manages edited config + * files. + * Permitted values for this directive are: + *
    + *
  • {@code true} (equivalent to specifying {@code %noreplace} + *
  • {@code false} (equivalent to omitting {@code %noreplace}) + *
+ * + * @param noReplace the noreplace + */ + public void setNoReplace(boolean noReplace) { + checkRpmFileSetAttributesAllowed(); + if (noReplace) { + directives.add(Directive.NOREPLACE); + } else { + directives.remove(Directive.NOREPLACE); + } + } + + /** + * Supports RPM's {@code %doc} directive, which flags the files as being documentation. RPM keeps track of + * documentation files in its database, so that a user can easily find information about an installed package. + * Permitted values for this directive are: + *
    + *
  • {@code true} (equivalent to specifying {@code %doc} + *
  • {@code false} (equivalent to omitting {@code %doc}) + *
+ * + * @param doc the doc + */ + public void setDoc(boolean doc) { + checkRpmFileSetAttributesAllowed(); + if (doc) { + directives.add(Directive.DOC); + } else { + directives.remove(Directive.DOC); + } + } + + /** + * Return a ArchiveFileSet that has the same properties as this one. + * + * @return the cloned archiveFileSet + */ + public Object clone() { + if (isReference()) { + return getRef(getProject()).clone(); + } + return super.clone(); + } + + private void checkRpmFileSetAttributesAllowed() { + if (getProject() == null || + (isReference() && (getRefid().getReferencedObject(getProject()) instanceof RpmFileSet))) { + checkAttributesAllowed(); + } + } +} diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/RpmTask.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/RpmTask.java new file mode 100644 index 0000000..589a9be --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/RpmTask.java @@ -0,0 +1,476 @@ +package org.xbib.rpm.ant; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.ArchiveFileSet; +import org.apache.tools.ant.types.TarFileSet; +import org.apache.tools.ant.types.ZipFileSet; +import org.xbib.rpm.RpmBuilder; +import org.xbib.rpm.exception.RpmException; +import org.xbib.rpm.header.HeaderTag; +import org.xbib.rpm.lead.Architecture; +import org.xbib.rpm.lead.Os; +import org.xbib.rpm.lead.PackageType; +import org.xbib.rpm.payload.CpioHeader; +import org.xbib.rpm.payload.Directive; +import org.xbib.rpm.payload.EmptyDir; +import org.xbib.rpm.payload.Ghost; +import org.xbib.rpm.payload.Link; +import org.xbib.rpm.trigger.TriggerIn; +import org.xbib.rpm.trigger.TriggerPostUn; +import org.xbib.rpm.trigger.TriggerPreIn; +import org.xbib.rpm.trigger.TriggerUn; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * Ant task for creating an RPM archive. + */ +public class RpmTask extends Task { + + private String name; + + private String epoch = "0"; + + private String version; + + private String group; + + private String release = "1"; + + private String host; + + private String summary = ""; + + private String description = ""; + + private String license = ""; + + private String packager = System.getProperty("user.name", ""); + + private String distribution = ""; + + private String vendor = ""; + + private String url = ""; + + private String sourcePackage = null; + + private String provides; + + private String prefixes; + + private PackageType type = PackageType.BINARY; + + private Architecture architecture = Architecture.NOARCH; + + private Os os = Os.LINUX; + + private Path destination; + + private List filesets = new ArrayList<>(); + + private List emptyDirs = new ArrayList<>(); + + private List ghosts = new ArrayList<>(); + + private List links = new ArrayList<>(); + + List depends = new ArrayList<>(); + + private List moreProvides = new ArrayList<>(); + + private List conflicts = new ArrayList<>(); + + private List obsoletes = new ArrayList<>(); + + private List triggersPreIn = new ArrayList<>(); + + private List triggersIn = new ArrayList<>(); + + private List triggersUn = new ArrayList<>(); + + private List triggersPostUn = new ArrayList<>(); + + private List builtIns = new ArrayList<>(); + + private Path preTransScript; + + private Path preInstallScript; + + private Path postInstallScript; + + private Path preUninstallScript; + + private Path postUninstallScript; + + private Path postTransScript; + + private InputStream privateKeyRing; + + private Long privateKeyId; + + private String privateKeyPassphrase; + + private Path changeLog; + + public RpmTask() { + try { + host = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + host = ""; + } + } + + @Override + public void execute() { + if (name == null) { + throw new BuildException("attribute 'name' is required"); + } + if (version == null) { + throw new BuildException("attribute 'version' is required"); + } + if (group == null) { + throw new BuildException("attribute 'group' is required"); + } + Integer numEpoch; + try { + numEpoch = Integer.parseInt(epoch); + } catch (Exception e) { + throw new BuildException("epoch must be integer: " + epoch); + } + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage(name, version, release, numEpoch); + rpmBuilder.setType(type); + rpmBuilder.setPlatform(architecture, os); + rpmBuilder.setGroup(group); + rpmBuilder.setBuildHost(host); + rpmBuilder.setSummary(summary); + rpmBuilder.setDescription(description); + rpmBuilder.setLicense(license); + rpmBuilder.setPackager(packager); + rpmBuilder.setDistribution(distribution); + rpmBuilder.setVendor(vendor); + rpmBuilder.setUrl(url); + if (provides != null) { + rpmBuilder.setProvides(provides); + } + rpmBuilder.setPrefixes(prefixes == null ? null : prefixes.split(",")); + rpmBuilder.setPrivateKeyRing(privateKeyRing); + rpmBuilder.setPrivateKeyId(privateKeyId); + rpmBuilder.setPrivateKeyPassphrase(privateKeyPassphrase); + if (sourcePackage != null) { + rpmBuilder.addHeaderEntry(HeaderTag.SOURCERPM, sourcePackage); + } + for (BuiltIn builtIn : builtIns) { + String text = builtIn.getText(); + if (text != null && !text.trim().equals("")) { + rpmBuilder.addBuiltinDirectory(builtIn.getText()); + } + } + try { + if (preTransScript != null) { + rpmBuilder.setPreTrans(preTransScript); + } + if (preInstallScript != null) { + rpmBuilder.setPreInstall(preInstallScript); + } + if (postInstallScript != null) { + rpmBuilder.setPostInstall(postInstallScript); + } + if (preUninstallScript != null) { + rpmBuilder.setPreUninstall(preUninstallScript); + } + if (postUninstallScript != null) { + rpmBuilder.setPostUninstall(postUninstallScript); + } + if (postTransScript != null) { + rpmBuilder.setPostTrans(postTransScript); + } + if (changeLog != null) { + rpmBuilder.addChangelog(changeLog); + } + for (EmptyDir emptyDir : emptyDirs) { + rpmBuilder.addDirectory(emptyDir.getPath(), emptyDir.getDirmode(), EnumSet.of(Directive.NONE), + emptyDir.getUsername(), emptyDir.getGroup(), true); + } + for (ArchiveFileSet fileset : filesets) { + Path archive = fileset.getSrc(getProject()) != null ? + fileset.getSrc(getProject()).toPath() : null; + String prefix = CpioHeader.normalizePath(fileset.getPrefix(getProject())); + if (!prefix.endsWith("/")) { + prefix += "/"; + } + DirectoryScanner directoryScanner = fileset.getDirectoryScanner(getProject()); + Integer filemode = fileset.getFileMode(getProject()) & 4095; + Integer dirmode = fileset.getDirMode(getProject()) & 4095; + String username = null; + String group = null; + EnumSet directive = null; + if (fileset instanceof TarFileSet) { + TarFileSet tarFileSet = (TarFileSet) fileset; + username = tarFileSet.getUserName(); + group = tarFileSet.getGroup(); + if (fileset instanceof RpmFileSet) { + RpmFileSet rpmFileSet = (RpmFileSet) fileset; + directive = rpmFileSet.getDirectives(); + } + } + for (String entry : directoryScanner.getIncludedDirectories()) { + String dir = CpioHeader.normalizePath(prefix + entry); + if (!"".equals(entry)) { + rpmBuilder.addDirectory(dir, dirmode, directive, username, group, true); + } + } + for (String entry : directoryScanner.getIncludedFiles()) { + if (archive != null) { + URL url = new URL("jar:" + archive.toUri().toURL() + "!/" + entry); + rpmBuilder.addURL(prefix + entry, url, filemode, dirmode, directive, username, group); + } else { + Path path = directoryScanner.getBasedir().toPath().resolve(entry); + rpmBuilder.addFile(prefix + entry, path, filemode, dirmode, directive, username, group); + } + } + } + for (Ghost ghost : ghosts) { + rpmBuilder.addFile(ghost.getPath(), null, ghost.getFilemode(), ghost.getDirmode(), + ghost.getDirectives(), ghost.getUsername(), ghost.getGroup()); + } + for (Link link : links) { + rpmBuilder.addLink(link.getPath(), link.getTarget(), link.getPermissions()); + } + for (Depends dependency : depends) { + rpmBuilder.addDependency(dependency.getName(), dependency.getComparison(), dependency.getVersion()); + } + for (Provides provision : moreProvides) { + rpmBuilder.addProvides(provision.getName(), provision.getVersion()); + } + for (Conflicts conflict : conflicts) { + rpmBuilder.addConflicts(conflict.getName(), conflict.getComparison(), conflict.getVersion()); + } + for (Obsoletes obsoletion : obsoletes) { + rpmBuilder.addObsoletes(obsoletion.getName(), obsoletion.getComparison(), obsoletion.getVersion()); + } + for (TriggerPreIn triggerPreIn : triggersPreIn) { + rpmBuilder.addTrigger(triggerPreIn.getScript(), "", triggerPreIn.getDepends(), + triggerPreIn.getFlag()); + } + for (TriggerIn triggerIn : triggersIn) { + rpmBuilder.addTrigger(triggerIn.getScript(), "", triggerIn.getDepends(), triggerIn.getFlag()); + } + for (TriggerUn triggerUn : triggersUn) { + rpmBuilder.addTrigger(triggerUn.getScript(), "", triggerUn.getDepends(), triggerUn.getFlag()); + } + for (TriggerPostUn triggerPostUn : triggersPostUn) { + rpmBuilder.addTrigger(triggerPostUn.getScript(), "", triggerPostUn.getDepends(), + triggerPostUn.getFlag()); + } + rpmBuilder.build(destination); + } catch (IOException | RpmException e) { + throw new BuildException("error while building package", e); + } + } + + public void restrict(String name) { + depends.removeIf(dependency -> dependency.getName().equals(name)); + } + + public void setName(String name) { + this.name = name; + } + + public void setEpoch(String epoch) { + this.epoch = epoch; + } + + public void setType(String type) { + this.type = PackageType.valueOf(type); + } + + public void setArchitecture(String architecture) { + this.architecture = Architecture.valueOf(architecture); + } + + public void setOs(String os) { + this.os = Os.valueOf(os); + } + + public void setVersion(String version) { + this.version = version; + } + + public void setRelease(String release) { + this.release = release; + } + + public void setGroup(String group) { + this.group = group; + } + + public void setHost(String host) { + this.host = host; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setLicense(String license) { + this.license = license; + } + + public void setPackager(String packager) { + this.packager = packager; + } + + public void setDistribution(String distribution) { + this.distribution = distribution; + } + + public void setVendor(String vendor) { + this.vendor = vendor; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setProvides(String provides) { + this.provides = provides; + } + + public void setPrefixes(String prefixes) { + this.prefixes = prefixes; + } + + public void setDestination(Path destination) { + this.destination = destination; + } + + public void addZipfileset(ZipFileSet fileset) { + filesets.add(fileset); + } + + public void addTarfileset(TarFileSet fileset) { + filesets.add(fileset); + } + + public void addRpmfileset(RpmFileSet fileset) { + filesets.add(fileset); + } + + public void addGhost(Ghost ghost) { + ghosts.add(ghost); + } + + public void addEmptyDir(EmptyDir emptyDir) { + emptyDirs.add(emptyDir); + } + + public void addLink(Link link) { + links.add(link); + } + + public void addDepends(Depends dependency) { + depends.add(dependency); + } + + public void addProvides(Provides provision) { + moreProvides.add(provision); + } + + public void addConflicts(Conflicts conflict) { + conflicts.add(conflict); + } + + public void addObsoletes(Obsoletes obsoletion) { + obsoletes.add(obsoletion); + } + + public void addTriggerPreIn(TriggerPreIn triggerPreIn) { + triggersPreIn.add(triggerPreIn); + } + + public void addTriggerIn(TriggerIn triggerIn) { + triggersIn.add(triggerIn); + } + + public void addTriggerUn(TriggerUn triggerUn) { + triggersUn.add(triggerUn); + } + + public void addTriggerPostUn(TriggerPostUn triggerPostUn) { + triggersPostUn.add(triggerPostUn); + } + + public void setPreTransScript(Path preTransScript) { + this.preTransScript = preTransScript; + } + + public void setPreInstallScript(Path preInstallScript) { + this.preInstallScript = preInstallScript; + } + + public void setPostInstallScript(Path postInstallScript) { + this.postInstallScript = postInstallScript; + } + + public void setPreUninstallScript(Path preUninstallScript) { + this.preUninstallScript = preUninstallScript; + } + + public void setPostUninstallScript(Path postUninstallScript) { + this.postUninstallScript = postUninstallScript; + } + + public void setPostTransScript(Path postTransScript) { + this.postTransScript = postTransScript; + } + + public void setSourcePackage(String sourcePackage) { + this.sourcePackage = sourcePackage; + } + + public void setPrivateKeyRing(Path privateKeyRing) throws IOException { + this.privateKeyRing = Files.newInputStream(privateKeyRing); + } + + public void setPrivateKeyRing(InputStream privateKeyRing) { + this.privateKeyRing = privateKeyRing; + } + + public void setPrivateKeyId(Long privateKeyId) { + this.privateKeyId = privateKeyId; + } + + public void setPrivateKeyId(String privateKeyId) { + this.privateKeyId = Long.decode("0x" + privateKeyId); + } + + public void setPrivateKeyPassphrase(String privateKeyPassphrase) { + this.privateKeyPassphrase = privateKeyPassphrase; + } + + public void addBuiltin(BuiltIn builtIn) { + builtIns.add(builtIn); + } + + public void setChangeLog(Path changeLog) { + this.changeLog = changeLog; + } + +} diff --git a/rpm-ant/src/main/java/org/xbib/rpm/ant/package-info.java b/rpm-ant/src/main/java/org/xbib/rpm/ant/package-info.java new file mode 100644 index 0000000..6e5bbdb --- /dev/null +++ b/rpm-ant/src/main/java/org/xbib/rpm/ant/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm.ant; diff --git a/rpm-ant/src/main/resources/org/xbib/rpm/antlib.xml b/rpm-ant/src/main/resources/org/xbib/rpm/antlib.xml new file mode 100644 index 0000000..9727821 --- /dev/null +++ b/rpm-ant/src/main/resources/org/xbib/rpm/antlib.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/rpm-ant/src/test/java/org/xbib/rpm/ant/RpmTaskTest.java b/rpm-ant/src/test/java/org/xbib/rpm/ant/RpmTaskTest.java new file mode 100644 index 0000000..30b7bc4 --- /dev/null +++ b/rpm-ant/src/test/java/org/xbib/rpm/ant/RpmTaskTest.java @@ -0,0 +1,474 @@ +package org.xbib.rpm.ant; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.types.EnumeratedAttribute; +import org.junit.Test; +import org.xbib.rpm.RpmReader; +import org.xbib.rpm.changelog.ChangelogParser; +import org.xbib.rpm.format.Flags; +import org.xbib.rpm.format.Format; +import org.xbib.rpm.header.EntryType; +import org.xbib.rpm.header.HeaderTag; +import org.xbib.rpm.header.entry.SpecEntry; +import org.xbib.rpm.payload.Directive; +import org.xbib.rpm.signature.SignatureTag; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.EnumSet; + +/** + * + */ +public class RpmTaskTest { + + @Test + public void testBadName() throws Exception { + RpmTask task = new RpmTask(); + task.setDestination(getTargetDir()); + task.setVersion("1.0"); + task.setGroup("groupRequired"); + task.setName("test"); + task.execute(); + // NB: This is no longer a bad name, long names are truncated in the header + task.setName("ToooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooLong"); + try { + task.execute(); + } catch (BuildException e) { + fail(); + } + task.setName("test/invalid"); + try { + task.execute(); + fail(); + } catch (IllegalArgumentException iae) { + // Pass + } + task.setName("test invalid"); + try { + task.execute(); + fail(); + } catch (IllegalArgumentException iae) { + // Pass + } + task.setName("test\tinvalid"); + try { + task.execute(); + fail(); + } catch (IllegalArgumentException iae) { + // Pass + } + } + + @Test + public void testBadVersion() throws Exception { + RpmTask task = new RpmTask(); + task.setName("nameRequired"); + task.setGroup("groupRequired"); + // test version with illegal char - + task.setVersion("1.0-beta"); + try { + task.execute(); + fail(); + } catch (IllegalArgumentException iae) { + // Pass + } + + // test version with illegal char / + task.setVersion("1.0/beta"); + try { + task.execute(); + fail(); + } catch (IllegalArgumentException iae) { + // Pass + } + } + + @Test + public void testBadRelease() throws Exception { + RpmTask task = new RpmTask(); + task.setName("nameRequired"); + task.setVersion("versionRequired"); + task.setGroup("groupRequired"); + task.setRelease("2-3"); + try { + task.execute(); + fail(); + } catch (IllegalArgumentException iae) { + // Pass + } + task.setRelease("2/3"); + try { + task.execute(); + fail(); + } catch (IllegalArgumentException iae) { + // Pass + } + } + + @Test + public void testBadEpoch() throws Exception { + RpmTask task = new RpmTask(); + task.setName("nameRequired"); + task.setVersion("versionRequired"); + task.setGroup("groupRequired"); + task.setEpoch("2-3"); + try { + task.execute(); + fail(); + } catch (BuildException iae) { + // + } + task.setEpoch("2~3"); + try { + task.execute(); + fail(); + } catch (BuildException iae) { + // + } + task.setEpoch("2/3"); + try { + task.execute(); + fail(); + } catch (BuildException iae) { + // + } + task.setEpoch("abc"); + try { + task.execute(); + fail(); + } catch (BuildException iae) { + // + } + } + + @Test + public void testRestrict() throws Exception { + Depends one = new Depends(); + one.setName("one"); + one.setVersion("1.0"); + Depends two = new Depends(); + two.setName("two"); + two.setVersion("1.0"); + RpmTask task = new RpmTask(); + task.addDepends(one); + task.addDepends(two); + assertEquals(2, task.depends.size()); + assertEquals("one", task.depends.get(0).getName()); + assertEquals("two", task.depends.get(1).getName()); + task.restrict("one"); + assertEquals(1, task.depends.size()); + assertEquals("two", task.depends.get(0).getName()); + } + + @Test + public void testCapabilities() throws Exception { + Path filename = getTargetDir().resolve("rpmtest-1.0-1.noarch.rpm"); + RpmTask task = createBasicTask(getTargetDir()); + for (String[] def : new String[][]{ + {"depone", "", "1.0"}, + {"deptwo", "less", "2.0"}, + {"depthree", "", ""}, + }) { + Depends dep = new Depends(); + dep.setName(def[0]); + if (0 < def[1].length()) { + dep.setComparison((Depends.ComparisonEnum) + EnumeratedAttribute.getInstance(Depends.ComparisonEnum.class, def[1])); + } + if (0 < def[2].length()) { + dep.setVersion(def[2]); + } + task.addDepends(dep); + } + for (String[] def : new String[][]{ + {"provone", "1.1"}, + {"provtwo", "2.1"}, + {"provthree", ""}, + }) { + Provides prov = new Provides(); + prov.setName(def[0]); + if (0 < def[1].length()) { + prov.setVersion(def[1]); + } + task.addProvides(prov); + } + for (String[] def : new String[][]{ + {"conone", "", "1.2"}, + {"contwo", "less", "2.2"}, + {"conthree", "", ""}, + }) { + Conflicts con = new Conflicts(); + con.setName(def[0]); + if (0 < def[1].length()) { + con.setComparison((Conflicts.ComparisonEnum) + EnumeratedAttribute.getInstance(Conflicts.ComparisonEnum.class, def[1])); + } + if (0 < def[2].length()) { + con.setVersion(def[2]); + } + task.addConflicts(con); + } + for (String[] def : new String[][]{ + {"obsone", "", "1.3"}, + {"obstwo", "less", "2.3"}, + {"obsthree", "", ""}, + }) { + Obsoletes obs = new Obsoletes(); + obs.setName(def[0]); + if (0 < def[1].length()) { + obs.setComparison((Obsoletes.ComparisonEnum) + EnumeratedAttribute.getInstance(Obsoletes.ComparisonEnum.class, def[1])); + } + if (0 < def[2].length()) { + obs.setVersion(def[2]); + } + task.addObsoletes(obs); + } + task.execute(); + Format format = getFormat(filename); + String[] require = (String[]) format.getHeader().getEntry(HeaderTag.REQUIRENAME).getValues(); + Integer[] requireflags = (Integer[]) format.getHeader().getEntry(HeaderTag.REQUIREFLAGS).getValues(); + String[] requireversion = (String[]) format.getHeader().getEntry(HeaderTag.REQUIREVERSION).getValues(); + assertArrayEquals(new String[]{"depone", "deptwo", "depthree"}, + Arrays.copyOfRange(require, require.length - 3, require.length)); + assertArrayEquals(new Integer[]{Flags.EQUAL | Flags.GREATER, Flags.LESS, 0}, + Arrays.copyOfRange(requireflags, requireflags.length - 3, require.length)); + assertArrayEquals(new String[]{"1.0", "2.0", ""}, + Arrays.copyOfRange(requireversion, requireversion.length - 3, require.length)); + String[] provide = (String[]) format.getHeader().getEntry(HeaderTag.PROVIDENAME).getValues(); + Integer[] provideflags = (Integer[]) format.getHeader().getEntry(HeaderTag.PROVIDEFLAGS).getValues(); + String[] provideversion = (String[]) format.getHeader().getEntry(HeaderTag.PROVIDEVERSION).getValues(); + assertArrayEquals(new String[]{"rpmtest", "provone", "provtwo", "provthree"}, provide); + assertArrayEquals(new Integer[]{Flags.EQUAL, Flags.EQUAL, Flags.EQUAL, 0}, provideflags); + assertArrayEquals(new String[]{"0:1.0-1", "1.1", "2.1", ""}, provideversion); + String[] conflict = (String[]) format.getHeader().getEntry(HeaderTag.CONFLICTNAME).getValues(); + Integer[] conflictflags = (Integer[]) format.getHeader().getEntry(HeaderTag.CONFLICTFLAGS).getValues(); + String[] conflictversion = (String[]) format.getHeader().getEntry(HeaderTag.CONFLICTVERSION).getValues(); + assertArrayEquals(new String[]{"conone", "contwo", "conthree"}, conflict); + assertArrayEquals(new Integer[]{Flags.EQUAL | Flags.GREATER, Flags.LESS, 0}, conflictflags); + assertArrayEquals(new String[]{"1.2", "2.2", ""}, conflictversion); + String[] obsolete = (String[]) format.getHeader().getEntry(HeaderTag.OBSOLETENAME).getValues(); + Integer[] obsoleteflags = (Integer[]) format.getHeader().getEntry(HeaderTag.OBSOLETEFLAGS).getValues(); + String[] obsoleteversion = (String[]) format.getHeader().getEntry(HeaderTag.OBSOLETEVERSION).getValues(); + assertArrayEquals(new String[]{"obsone", "obstwo", "obsthree"}, obsolete); + assertArrayEquals(new Integer[]{Flags.EQUAL | Flags.GREATER, Flags.LESS, 0}, obsoleteflags); + assertArrayEquals(new String[]{"1.3", "2.3", ""}, obsoleteversion); + } + + @Test + public void testScripts() throws Exception { + Path filename = getTargetDir().resolve("rpmtest-1.0-1.noarch.rpm"); + RpmTask task = createBasicTask(getTargetDir()); + task.setPreInstallScript(Paths.get("src/test/resources/prein.sh")); + task.setPostInstallScript(Paths.get("src/test/resources/postin.sh")); + task.setPreUninstallScript(Paths.get("src/test/resources/preun.sh")); + task.setPostUninstallScript(Paths.get("src/test/resources/postun.sh")); + task.execute(); + Format format = getFormat(filename); + assertHeaderEquals("#!/bin/sh\n\necho Hello Pre Install!\n", format, + HeaderTag.PREINSCRIPT); + assertHeaderEquals("\n\necho Hello Post Install!\n", format, + HeaderTag.POSTINSCRIPT); + assertHeaderEquals("# comment\n\necho Hello Pre Uninstall!\n", format, + HeaderTag.PREUNSCRIPT); + assertHeaderEquals("#!/usr/bin/perl\n\nprint \"Hello Post Uninstall!\\n\";\n", format, + HeaderTag.POSTUNSCRIPT); + + assertHeaderEquals("/bin/sh", format, HeaderTag.PREINPROG); + assertHeaderEquals("/bin/sh", format, HeaderTag.POSTINPROG); + assertHeaderEquals("/bin/sh", format, HeaderTag.PREUNPROG); + assertHeaderEquals("/usr/bin/perl", format, HeaderTag.POSTUNPROG); + } + + @Test + public void testScriptsAndChangeLog() throws Exception { + Path filename = getTargetDir().resolve("rpmtest-1.0-2.noarch.rpm"); + RpmTask task = createBasicTask(getTargetDir()); + task.setRelease("2"); + task.setPreInstallScript(Paths.get("src/test/resources/prein.sh")); + task.setPostInstallScript(Paths.get("src/test/resources/postin.sh")); + task.setPreUninstallScript(Paths.get("src/test/resources/preun.sh")); + task.setPostUninstallScript(Paths.get("src/test/resources/postun.sh")); + task.setChangeLog(Paths.get("src/test/resources/org/xbib/rpm/changelog/changelog")); + task.execute(); + Format format = getFormat(filename); + assertHeaderEquals("#!/bin/sh\n\necho Hello Pre Install!\n", format, + HeaderTag.PREINSCRIPT); + assertHeaderEquals("\n\necho Hello Post Install!\n", format, + HeaderTag.POSTINSCRIPT); + assertHeaderEquals("# comment\n\necho Hello Pre Uninstall!\n", format, + HeaderTag.PREUNSCRIPT); + assertHeaderEquals("#!/usr/bin/perl\n\nprint \"Hello Post Uninstall!\\n\";\n", format, + HeaderTag.POSTUNSCRIPT); + assertHeaderEquals("/bin/sh", format, HeaderTag.PREINPROG); + assertHeaderEquals("/bin/sh", format, HeaderTag.POSTINPROG); + assertHeaderEquals("/bin/sh", format, HeaderTag.PREUNPROG); + assertHeaderEquals("/usr/bin/perl", format, HeaderTag.POSTUNPROG); + assertDateEntryHeaderEqualsAt("Tue Feb 24 2015", format, + HeaderTag.CHANGELOGTIME, 10, 0); + assertHeaderEqualsAt("Thomas Jefferson", format, + HeaderTag.CHANGELOGNAME, 10, 4); + assertHeaderEqualsAt("- Initial rpm for this package", format, + HeaderTag.CHANGELOGTEXT, 10, 9); + String expectedMultiLineDescription = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod \n" + + "tempor incididunt ut labore et dolore magna aliqua"; + assertHeaderEqualsAt(expectedMultiLineDescription, format, + HeaderTag.CHANGELOGTEXT, 10, 0); + } + + @Test + public void testFiles() throws Exception { + Path filename = getTargetDir().resolve("rpmtest-1.0-1.noarch.rpm"); + RpmTask task = createBasicTask(getTargetDir()); + RpmFileSet fs = new RpmFileSet(); + fs.setPrefix("/etc"); + fs.setFile(Paths.get("src/test/resources/prein.sh").toFile()); + fs.setConfig(true); + fs.setNoReplace(true); + fs.setDoc(true); + fs.setUserName("jabberwocky"); + fs.setGroup("vorpal"); + task.addRpmfileset(fs); + task.execute(); + Format format = getFormat(filename); + assertArrayEquals(new String[]{"jabberwocky"}, + (String[]) format.getHeader().getEntry(HeaderTag.FILEUSERNAME).getValues()); + assertArrayEquals(new String[]{"vorpal"}, + (String[]) format.getHeader().getEntry(HeaderTag.FILEGROUPNAME).getValues()); + EnumSet directives = EnumSet.of(Directive.CONFIG, Directive.DOC, Directive.NOREPLACE); + Integer expectedFlags = 0; + for (Directive d : directives) { + expectedFlags |= d.flag(); + } + assertInt32EntryHeaderEquals(new Integer[]{expectedFlags}, format, HeaderTag.FILEFLAGS); + } + + @Test + public void testSigning() throws Exception { + Path filename = getTargetDir().resolve("rpmtest-1.0-1.noarch.rpm"); + RpmTask task = createBasicTask(getTargetDir()); + task.setPrivateKeyRing(getClass().getResourceAsStream("/pgp/test-secring.gpg")); + task.setPrivateKeyPassphrase("test"); + task.execute(); + Format format = getFormat(filename); + assertNotNull(format.getSignatureHeader().getEntry(SignatureTag.RSAHEADER)); + assertNotNull(format.getSignatureHeader().getEntry(SignatureTag.LEGACY_PGP)); + } + + @Test + public void testPackageNameLength() { + Path dir = getTargetDir(); + RpmTask task = new RpmTask(); + task.setProject(createProject()); + task.setDestination(dir); + task.setName("thisfilenameislongdddddddddddddddddfddddddddddddddddddddddddddddddd"); + task.setVersion("1.0"); + task.setRelease("1"); + task.setGroup("Application/Office"); + task.setPreInstallScript(Paths.get("src/test/resources/prein.sh")); + task.setPostInstallScript(Paths.get("src/test/resources/postin.sh")); + task.setPreUninstallScript(Paths.get("src/test/resources/preun.sh")); + task.setPostUninstallScript(Paths.get("src/test/resources/postun.sh")); + RpmFileSet fs = new RpmFileSet(); + fs.setPrefix("/etc"); + fs.setFile(Paths.get("src/test/resources/prein.sh").toFile()); + fs.setConfig(true); + fs.setNoReplace(true); + fs.setDoc(true); + task.addRpmfileset(fs); + try { + task.execute(); + } catch (Exception e) { + fail("Test failed: should not be thrown: " + e.getClass().getName()); + } + task.setName("shortpackagename"); + try { + task.execute(); + } catch (Exception e) { + fail("Test failed: should not be thrown: " + e.getClass().getName()); + } + } + + private Format getFormat(Path filename) throws IOException { + RpmReader rpmReader = new RpmReader(); + return rpmReader.readHeader(filename); + } + + private RpmTask createBasicTask(Path dir) { + RpmTask task = new RpmTask(); + task.setProject(createProject()); + task.setDestination(dir); + task.setName("rpmtest"); + task.setVersion("1.0"); + task.setRelease("1"); + task.setGroup("Application/Office"); + return task; + } + + private Project createProject() { + Project project = new Project(); + project.setCoreLoader(getClass().getClassLoader()); + project.init(); + return project; + } + + private Path getTargetDir() { + return Paths.get("build"); + } + + private void assertHeaderEquals(String expected, Format format, EntryType entryType) { + assertNotNull("null format", format); + SpecEntry entry = format.getHeader().getEntry(entryType); + assertNotNull("Entry not found : " + entryType.getName(), entry); + assertEquals("Entry type : " + entryType.getName(), 6, entry.getType()); + String[] values = (String[]) entry.getValues(); + assertNotNull("null values", values); + assertEquals("Entry size : " + entryType.getName(), 1, values.length); + assertEquals("Entry value : " + entryType.getName(), expected, values[0]); + } + + private void assertHeaderEqualsAt(String expected, Format format, EntryType entryType, int size, int pos) { + assertNotNull("null format", format); + SpecEntry entry = format.getHeader().getEntry(entryType); + assertNotNull("Entry not found : " + entryType.getName(), entry); + assertEquals("Entry type : " + entryType.getName(), 8, entry.getType()); + String[] values = (String[]) entry.getValues(); + assertNotNull("null values", values); + assertEquals("Entry size : " + entryType.getName(), size, values.length); + assertEquals("Entry value : " + entryType.getName(), expected, values[pos]); + } + + private void assertInt32EntryHeaderEquals(Integer[] expected, Format format, EntryType entryType) { + assertNotNull("null format", format); + SpecEntry entry = format.getHeader().getEntry(entryType); + assertNotNull("Entry not found : " + entryType.getName(), entry); + assertEquals("Entry type : " + entryType.getName(), 4, entry.getType()); + Integer[] values = (Integer[]) entry.getValues(); + assertNotNull("null values", values); + assertEquals("Entry size : " + entryType.getName(), 1, values.length); + assertArrayEquals("Entry value : " + entryType.getName(), expected, values); + } + + private void assertDateEntryHeaderEqualsAt(String expected, Format format, EntryType entryType, int size, int pos) { + assertNotNull("null format", format); + SpecEntry entry = format.getHeader().getEntry(entryType); + assertNotNull("Entry not found : " + entryType.getName(), entry); + assertEquals("Entry type : " + entryType.getName(), 4, entry.getType()); + Integer[] values = (Integer[]) entry.getValues(); + assertNotNull("null values", values); + assertEquals("Entry size : " + entryType.getName(), size, values.length); + LocalDateTime localDate = LocalDateTime.ofEpochSecond(values[pos], 0, ZoneOffset.UTC); + assertEquals("Entry value : " + entryType.getName(), expected, ChangelogParser.CHANGELOG_FORMAT.format(localDate)); + } +} diff --git a/rpm-ant/src/test/java/org/xbib/rpm/ant/package-info.java b/rpm-ant/src/test/java/org/xbib/rpm/ant/package-info.java new file mode 100644 index 0000000..6e5bbdb --- /dev/null +++ b/rpm-ant/src/test/java/org/xbib/rpm/ant/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm.ant; diff --git a/rpm-ant/src/test/resources/org/xbib/rpm/changelog/changelog b/rpm-ant/src/test/resources/org/xbib/rpm/changelog/changelog new file mode 100644 index 0000000..6c3ab5b --- /dev/null +++ b/rpm-ant/src/test/resources/org/xbib/rpm/changelog/changelog @@ -0,0 +1,24 @@ +* Tue Feb 24 2015 George Washington +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua +* Tue Feb 10 2015 George Washington +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +* Mon Nov 17 2014 George Washington + consectetur, adipisci velit, sed quia non numquam eius modi + sunt explicabo. Nemo enim ipsam voluptatem quia vol + eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat + Excepteur sint occaecat cupidatat non proident, sunt +* Fri Mar 06 2009 John Adams +- nostrum exercitationem ullam corporis suscipit +* Thu Oct 16 2008 Thomas Jefferson +- Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +* Thu Aug 23 2007 James Madison +eaque ipsa quae ab illo inventore veritatis et quasi architecto +* Mon Jun 04 2007 James Monroe +- adipisci velit, sed q +* Tue May 08 2007 James Madison +- dolore eu fugiat nulla pariatur +* Tue Apr 10 2007 James Monroe +-+// quis nostrum exercitationem ullam corporis +* Wed Nov 08 2006 James Madison +- Initial rpm for this package diff --git a/rpm-ant/src/test/resources/pgp/test-secring.gpg b/rpm-ant/src/test/resources/pgp/test-secring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..2044048a0212d321a6926cc727415cf4da5fcf81 GIT binary patch literal 2551 zcmVAo#(g@*%9Adr8N`H!W04^zbqGp|s z*@XictD`iQ3z27Fv1sf}W1jTn2iB$^Sq8T?r;__F`>eE&Z(`xhNL4EBE(TXiRo~o# zY>3TSf&iUhPpHISL0{UszGqN^I?hO;+h85KNB|b!IQoPxV!I4~E!x{b#Z!|Q&Y$hU- zUo~!&XQI^3A@~np78SNnh>AAE69;WPX6tj9e5s?75%r=ny0;BQ2t#ma)pb5V$73CuCVEb`^|Ayr&&!^%>qw;3) zv_!~WG|m$(pXe5^#W1BDn^fJ(PkQ*2m6}~7$U%D2og>#<0UmD8fZSQQu3v~^Mk9;M z#;!wg#1}DKfxg$sFOs|k8?1U2KXN!29_*z?fo{UCJ`K+Vr!+GX)YIdwwgM`k&E-cNGfO$p7(NvZy}8U5XQS%P5ZtAA{uezQ)yhGQqw$EVR> zSvs!qu9lk&G?)cn5az%g%w)iNqwCEv%J&bSdEq@J{Kxt{#~l0vZ*cov^*1~3b<=p) ztDeziiI~dJ%F0Vohxl*n_xJXCBglCTkaQqJ5z>AKMX}WmctN%Zv=DSj(W2Ki|~xA6%%&Z7&$eSvJ%(vYi7ZVt0o=`;yizxY50*`-LdG#1G@Yy7jvZPJXmW zk^pTeW(EKZXS{dmy2*T3NooYY`0vEJD%_-qxJ_+UcUrPae8*O{FlUS(q~8ra&`5y( zb$X&t(S{hkFO;IzD?g|+6Icy?juS`z*l6TzQGk*4K6suaGu`?fS#@u`2?rliL11?K z24_C=HdQO#8KFh!juAK6c0K$MtGquGU?}*j6ulqmKQZ z1I7ed)72&c2mrj`KHvo^DCz zj^b-JB0$3FO)bq6qjF&64ZZN~sub^37!&Xife%Oz93m*y60>s7fBJ+KwkSS{qb{5{ z%{6NBIg$h)AJJdmoi9jSlNapOdIYvM6Q5q5y8=mxm`6RNvNRVe)uk6wi2s1G#H+-J zOwYzY_7}_h)UB{PE{zFqZubptV{w~@Po%h66>>}=pQhJzxz0gXNx59*iato{Yg~9e ztua4;{E+|=0RRF12?Gchy$jF`KlRJNK~8%_2;TSwdUfY&qd_D0%L@b`ucNqc?-;MCk4z1wWzvGr+w(>2eE)|; zO%f|hhgoeI==|bx*x41%k`|&$^tU_~p-bFFbbyR|R;G0FyFFxS^x2d8fDdmH7$(I; zcaE@d@Z)1PD1?299f_$5wzSL~6qJ*iu)lFLI2onQhLoonlV>3s_sd+@r3G=^~4FR$rfvQQ@4SGFlH;dc;D4~GtL&mo9JG!WiS9N(Uq6e}bD z-#sHdigow5Tr5|tT}$kyS$kg8GNh!tREkF7Bl7NX@b?gD8QtY2sjRvT9FyOWW!4e+ zi;lESrv!(pAJ}hBVTeyr9p(!DbPhaE)&zI*%|VMH(?MHq)QL0LGyuy70%uM4GET~}G)yh1qsqPTk|0wFSeGP0C=o7;; zZZ8pbiYrGnNzCo?|t4?M!8opwr zQ;B?t&ocQWTGqEWTuX-B3xWv$2PLw%csMRtklDGu|4mbJDUUyn z2@tx{NEYxcZ7d-E2mp_L6hp!C#7Xi@K_9PR;&b(XNZ1TSkfD#tziBd&x&&&lMl>8s zyOmhBpufP2??1vJ*9XmV2YCEhGkX=C)>(CYaV=^L=#!>!_>RB`9#<;0+*a-jt literal 0 HcmV?d00001 diff --git a/rpm-ant/src/test/resources/postin.sh b/rpm-ant/src/test/resources/postin.sh new file mode 100644 index 0000000..fb9746a --- /dev/null +++ b/rpm-ant/src/test/resources/postin.sh @@ -0,0 +1,3 @@ + + +echo Hello Post Install! diff --git a/rpm-ant/src/test/resources/postun.sh b/rpm-ant/src/test/resources/postun.sh new file mode 100644 index 0000000..3bbb142 --- /dev/null +++ b/rpm-ant/src/test/resources/postun.sh @@ -0,0 +1,3 @@ +#!/usr/bin/perl + +print "Hello Post Uninstall!\n"; diff --git a/rpm-ant/src/test/resources/prein.sh b/rpm-ant/src/test/resources/prein.sh new file mode 100644 index 0000000..5ea1cbd --- /dev/null +++ b/rpm-ant/src/test/resources/prein.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo Hello Pre Install! diff --git a/rpm-ant/src/test/resources/preun.sh b/rpm-ant/src/test/resources/preun.sh new file mode 100644 index 0000000..5c739e8 --- /dev/null +++ b/rpm-ant/src/test/resources/preun.sh @@ -0,0 +1,3 @@ +# comment + +echo Hello Pre Uninstall! diff --git a/rpm-core/NOTICE.txt b/rpm-core/NOTICE.txt new file mode 100644 index 0000000..c474885 --- /dev/null +++ b/rpm-core/NOTICE.txt @@ -0,0 +1,13 @@ +This is a derived work of + +http://redline-rpm.org/index.html + +licensed under MIT License: + +Copyright (c) 2007-2015 FreeCompany + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/rpm-core/build.gradle b/rpm-core/build.gradle new file mode 100644 index 0000000..783aa60 --- /dev/null +++ b/rpm-core/build.gradle @@ -0,0 +1,11 @@ +dependencies { + compile "org.bouncycastle:bcpg-jdk15on:${project.property('bouncycastle.version')}" + compile "org.xbib:archive:${project.property('xbib-archive.version')}" +} + +test { + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } +} \ No newline at end of file diff --git a/rpm-core/config/checkstyle/checkstyle.xml b/rpm-core/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..55e59d2 --- /dev/null +++ b/rpm-core/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rpm-core/src/main/java/org/xbib/rpm/RpmBuilder.java b/rpm-core/src/main/java/org/xbib/rpm/RpmBuilder.java new file mode 100644 index 0000000..e05ed7b --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/RpmBuilder.java @@ -0,0 +1,1754 @@ +package org.xbib.rpm; + +import org.xbib.io.compress.bzip2.Bzip2OutputStream; +import org.xbib.io.compress.xz.FilterOptions; +import org.xbib.io.compress.xz.LZMA2Options; +import org.xbib.io.compress.xz.X86Options; +import org.xbib.io.compress.xz.XZOutputStream; +import org.xbib.rpm.changelog.ChangelogHandler; +import org.xbib.rpm.exception.ChangelogParseException; +import org.xbib.rpm.exception.RpmException; +import org.xbib.rpm.format.Flags; +import org.xbib.rpm.format.Format; +import org.xbib.rpm.header.EntryType; +import org.xbib.rpm.header.HeaderTag; +import org.xbib.rpm.header.entry.SpecEntry; +import org.xbib.rpm.io.ChannelWrapper; +import org.xbib.rpm.io.WritableChannelWrapper; +import org.xbib.rpm.lead.Architecture; +import org.xbib.rpm.lead.Os; +import org.xbib.rpm.lead.PackageType; +import org.xbib.rpm.payload.CompressionType; +import org.xbib.rpm.payload.Contents; +import org.xbib.rpm.payload.CpioHeader; +import org.xbib.rpm.payload.Directive; +import org.xbib.rpm.security.HashAlgo; +import org.xbib.rpm.security.SignatureGenerator; +import org.xbib.rpm.signature.SignatureTag; +import org.xbib.rpm.trigger.Trigger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.GZIPOutputStream; + +/** + * + */ +public class RpmBuilder { + + private static final Integer SHASIZE = 41; + + private static final String DEFAULTSCRIPTPROG = "/bin/sh"; + + private static final char[] ILLEGAL_CHARS_VARIABLE = new char[]{'-', '/'}; + + private static final char[] ILLEGAL_CHARS_NAME = new char[]{'/', ' ', '\t', '\n', '\r'}; + + private static final String hex = "0123456789abcdef"; + + private final Format format = new Format(); + + private final List requires = new ArrayList<>(); + + private final List obsoletes = new ArrayList<>(); + + private final List conflicts = new ArrayList<>(); + + private final Map provides = new LinkedHashMap<>(); + + private final List triggerscripts = new ArrayList<>(); + + private final List triggerscriptprogs = new ArrayList<>(); + + private final List triggernames = new ArrayList<>(); + + private final List triggerversions = new ArrayList<>(); + + private final List triggerflags = new ArrayList<>(); + + private final List triggerindexes = new ArrayList<>(); + + private Contents contents = new Contents(); + + private InputStream privateKeyRing; + + private Long privateKeyId; + + private String privateKeyPassphrase; + + private int triggerCounter = 0; + + private HashAlgo hashAlgo; + + private CompressionType compressionType; + + private String packageName; + + public RpmBuilder() { + this(HashAlgo.SHA256, CompressionType.GZIP); + } + + /** + * Initializes the builder and sets some required fields to known values. + * @param hashAlgo the hash algo + * @param compressionType compression type + */ + public RpmBuilder(HashAlgo hashAlgo, CompressionType compressionType) { + this.hashAlgo = hashAlgo; + this.compressionType = compressionType; + format.getHeader().createEntry(HeaderTag.HEADERI18NTABLE, "C"); + format.getHeader().createEntry(HeaderTag.BUILDTIME, (int) (System.currentTimeMillis() / 1000)); + format.getHeader().createEntry(HeaderTag.RPMVERSION, "4.6.0"); + format.getHeader().createEntry(HeaderTag.PAYLOADFORMAT, "cpio"); + format.getHeader().createEntry(HeaderTag.PAYLOADCOMPRESSOR, compressionType.name().toLowerCase()); + addDependencyLess("rpmlib(VersionedDependencies)", "3.0.3-1"); + addDependencyLess("rpmlib(CompressedFileNames)", "3.0.4-1"); + addDependencyLess("rpmlib(PayloadFilesHavePrefix)", "4.0-1"); + addDependencyLess("rpmlib(FileDigests)", "4.6.0-1"); + addDependencyLess("rpmlib(PayloadIsBzip2)", "3.0.5-1"); + addDependencyLess("rpmlib(PayloadIsLzma)", "4.4.2-1"); + addDependencyLess("rpmlib(PayloadIsXz)", "5.2-1"); + } + + /** + * Returns an array of String with the name of every dependency from a list of dependencies. + * + * @param dependencyList List of dependencies + * @return String[] with all names of the dependencies + */ + private static String[] getArrayOfNames(List dependencyList) { + List list = new ArrayList<>(); + for (Dependency dependency : dependencyList) { + list.add(dependency.getName()); + } + return list.toArray(new String[list.size()]); + } + + /** + * Returns an array of String with the version of every dependency from a list of dependencies. + * + * @param dependencyList List of dependencies + * @return String[] with all versions of the dependencies + */ + private static String[] getArrayOfVersions(List dependencyList) { + List versionList = new ArrayList<>(); + for (Dependency dependency : dependencyList) { + versionList.add(dependency.getVersion()); + } + return versionList.toArray(new String[versionList.size()]); + } + + /** + * Returns an array of Integer with the flags of every dependency from a list of dependencies. + * + * @param dependencyList List of dependencies + * @return Integer[] with all flags of the dependencies + */ + private static Integer[] getArrayOfFlags(List dependencyList) { + List flagsList = new ArrayList<>(); + for (Dependency dependency : dependencyList) { + flagsList.add(dependency.getFlags()); + } + return flagsList.toArray(new Integer[flagsList.size()]); + } + + /** + * Returns an array of String with the name of every dependency from a list of dependencies. + * + * @param dependencies List of dependencies + * @return String[] with all names of the dependencies + */ + private static String[] getArrayOfNames(Map dependencies) { + List nameList = new ArrayList<>(); + for (Dependency dependency : dependencies.values()) { + nameList.add(dependency.getName()); + } + return nameList.toArray(new String[nameList.size()]); + } + + /** + * Returns an array of String with the version of every dependency from a list of dependencies. + * + * @param dependencies List of dependencies + * @return String[] with all versions of the dependencies + */ + private static String[] getArrayOfVersions(Map dependencies) { + List versionList = new ArrayList<>(); + for (Dependency dependency : dependencies.values()) { + versionList.add(dependency.getVersion()); + } + return versionList.toArray(new String[versionList.size()]); + } + + /** + * Returns an array of Integer with the flags of every dependency from a list of dependencies. + * + * @param dependencies List of dependencies + * @return Integer[] with all flags of the dependencies + */ + private static Integer[] getArrayOfFlags(Map dependencies) { + List flagsList = new ArrayList<>(); + for (Dependency dependency : dependencies.values()) { + flagsList.add(dependency.getFlags()); + } + return flagsList.toArray(new Integer[flagsList.size()]); + } + + private static int difference(int start, int boundary) { + return ((boundary + 1) - (start & boundary)) & boundary; + } + + private static String hex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte aByte : bytes) { + sb.append(hex.charAt(((int) aByte & 0xf0) >> 4)).append(hex.charAt((int) aByte & 0x0f)); + } + return sb.toString(); + } + + public void addBuiltinDirectory(String builtinDirectory) { + contents.addLocalBuiltinDirectory(builtinDirectory); + } + + public void addObsoletes(String name, int comparison, String version) { + obsoletes.add(new Dependency(name, version, comparison)); + } + + public void addObsoletesLess(CharSequence name, CharSequence version) { + int flag = Flags.LESS | Flags.EQUAL; + addObsoletes(name, version, flag); + } + + public void addObsoletesMore(CharSequence name, CharSequence version) { + int flag = Flags.GREATER | Flags.EQUAL; + addObsoletes(name, version, flag); + } + + public void addObsoletes(CharSequence name, CharSequence version, int flag) { + obsoletes.add(new Dependency(name.toString(), version.toString(), flag)); + } + + public void addConflicts(String name, int comparison, String version) { + conflicts.add(new Dependency(name, version, comparison)); + } + + public void addConflictsLess(CharSequence name, CharSequence version) { + int flag = Flags.LESS | Flags.EQUAL; + addConflicts(name, version, flag); + } + + public void addConflictsMore(CharSequence name, CharSequence version) { + int flag = Flags.GREATER | Flags.EQUAL; + addConflicts(name, version, flag); + } + + public void addConflicts(CharSequence name, CharSequence version, int flag) { + conflicts.add(new Dependency(name.toString(), version.toString(), flag)); + } + + public void addProvides(String name, String version) { + provides.put(name, new Dependency(name, version, version.length() > 0 ? Flags.EQUAL : 0)); + } + + public void addProvides(CharSequence name, CharSequence version, int flag) { + provides.put(name.toString(), new Dependency(name.toString(), version.toString(), flag)); + } + + /** + * Adds a dependency to the RPM package. This dependency version will be marked as the exact + * requirement, and the package will require the named dependency with exactly this version at + * install time. + * + * @param name the name of the dependency + * @param comparison the comparison flag + * @param version the version identifier + */ + public void addDependency(String name, int comparison, String version) { + requires.add(new Dependency(name, version, comparison)); + } + + /** + * Adds a dependency to the RPM package. This dependency version will be marked as the maximum + * allowed, and the package will require the named dependency with this version or lower at + * install time. + * + * @param name the name of the dependency + * @param version the version identifier + */ + public void addDependencyLess(CharSequence name, CharSequence version) { + int flag = Flags.LESS | Flags.EQUAL; + if (name.toString().startsWith("rpmlib(")) { + flag = flag | Flags.RPMLIB; + } + addDependency(name, version, flag); + } + + /** + * Adds a dependency to the RPM package. This dependency version will be marked as the minimum + * allowed, and the package will require the named dependency with this version or higher at + * install time. + * + * @param name the name of the dependency. + * @param version the version identifier. + */ + public void addDependencyMore(CharSequence name, CharSequence version) { + addDependency(name, version, Flags.GREATER | Flags.EQUAL); + } + + /** + * Adds a dependency to the RPM package. This dependency version will be marked as the exact + * requirement, and the package will require the named dependency with exactly this version at + * install time. + * + * @param name the name of the dependency. + * @param version the version identifier. + * @param flag the file flags + */ + public void addDependency(CharSequence name, CharSequence version, int flag) { + requires.add(new Dependency(name.toString(), version.toString(), flag)); + } + + /** + * Adds a header entry value to the header. For example use this to set the source RPM package + * name on your RPM + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + */ + public void addHeaderEntry(EntryType entryType, String value) { + format.getHeader().createEntry(entryType, value); + } + + /** + * Adds a header entry byte (8-bit) value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not byte[] + */ + public void addHeaderEntry(EntryType entryType, byte value) { + format.getHeader().createEntry(entryType, new byte[]{value}); + } + + /** + * Adds a header entry char (8-bit) value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not byte[] + */ + public void addHeaderEntry(EntryType entryType, char value) { + format.getHeader().createEntry(entryType, new byte[]{(byte) value}); + } + + /** + * Adds a header entry short (16-bit) value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not short[] + */ + public void addHeaderEntry(EntryType entryType, short value) { + format.getHeader().createEntry(entryType, new short[]{value}); + } + + /** + * Adds a header entry int (32-bit) value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException if the type required by tag.type() is not Integer[] + */ + public void addHeaderEntry(EntryType entryType, Integer value) { + format.getHeader().createEntry(entryType, new Integer[]{value}); + } + + /** + * Adds a header entry long (64-bit) value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not long[] + */ + public void addHeaderEntry(EntryType entryType, long value) { + format.getHeader().createEntry(entryType, new long[]{value}); + } + + /** + * Adds a header entry byte array (8-bit) value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not byte[] + */ + public void addHeaderEntry(EntryType entryType, byte[] value) { + format.getHeader().createEntry(entryType, value); + } + + /** + * Adds a header entry short array (16-bit) value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not short[] + */ + public void addHeaderEntry(EntryType entryType, short[] value) { + format.getHeader().createEntry(entryType, value); + } + + /** + * Adds a header entry int (32-bit) array value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not int[] + */ + public void addHeaderEntry(EntryType entryType, Integer[] value) { + format.getHeader().createEntry(entryType, value); + } + + /** + * Adds a header entry long (64-bit) array value to the header. + * + * @param entryType the header tag to set + * @param value the value to set the header entry with + * @throws ClassCastException - if the type required by tag.type() is not long[] + */ + public void addHeaderEntry(EntryType entryType, long[] value) { + format.getHeader().createEntry(entryType, value); + } + + /** + * @param illegalChars the illegal characters to check for. + * @param variable the character sequence to check for illegal characters. + * @param variableName the name to include in IllegalArgumentException + * @throws IllegalArgumentException if passed in character sequence contains dashes. + */ + private void checkVariableContainsIllegalChars(char[] illegalChars, CharSequence variable, String variableName) { + for (int i = 0; i < variable.length(); i++) { + char currChar = variable.charAt(i); + for (char illegalChar : illegalChars) { + if (currChar == illegalChar) { + throw new IllegalArgumentException(variableName + " with value: '" + variable + + "' contains illegal character " + currChar); + } + } + } + } + + /** + * Required Field. Sets the package information, such as the rpm name, the version, and the release number. + * + * @param name the name of the RPM package. + * @param version the version of the new package. + * @param release the release number, specified after the version, of the new RPM. + * @param epoch the epoch number of the new RPM + * @throws IllegalArgumentException if version or release contain + * dashes, as they are explicitly disallowed by RPM file format. + */ + public void setPackage(CharSequence name, CharSequence version, CharSequence release, Integer epoch) { + checkVariableContainsIllegalChars(ILLEGAL_CHARS_NAME, name, "name"); + checkVariableContainsIllegalChars(ILLEGAL_CHARS_VARIABLE, version, "version"); + checkVariableContainsIllegalChars(ILLEGAL_CHARS_VARIABLE, release, "release"); + format.getLead().setName(name + "-" + version + "-" + release); + format.getHeader().createEntry(HeaderTag.NAME, name); + format.getHeader().createEntry(HeaderTag.VERSION, version); + format.getHeader().createEntry(HeaderTag.RELEASE, release); + format.getHeader().createEntry(HeaderTag.EPOCH, epoch); + this.provides.clear(); + addProvides(String.valueOf(name), "" + epoch + ":" + version + "-" + release); + } + + public void setPackage(CharSequence name, CharSequence version, CharSequence release) { + setPackage(name, version, release, 0); + } + + /** + * Required Field. Sets the type of the RPM to be either binary or source. + * + * @param type the type of RPM to generate. + */ + public void setType(PackageType type) { + format.getLead().setType(type); + } + + /** + * Sets the platform related headers for the resulting RPM. The platform is specified as a + * combination of target architecture and OS. + * + * @param arch the target architecture. + * @param os the target operating system. + */ + public void setPlatform(Architecture arch, Os os) { + format.getLead().setArch(arch); + format.getLead().setOs(os); + CharSequence archName = arch.toString().toLowerCase(); + CharSequence osName = os.toString().toLowerCase(); + format.getHeader().createEntry(HeaderTag.ARCH, archName); + format.getHeader().createEntry(HeaderTag.OS, osName); + format.getHeader().createEntry(HeaderTag.PLATFORM, archName + "-" + osName); + format.getHeader().createEntry(HeaderTag.RHNPLATFORM, archName); + } + + /** + *Sets the platform related headers for the resulting RPM. The platform is specified as a + * combination of target architecture and OS. + * + * @param arch the target architecture. + * @param osName the non-standard target operating system. + */ + public void setPlatform(Architecture arch, CharSequence osName) { + format.getLead().setArch(arch); + format.getLead().setOs(Os.UNKNOWN); + CharSequence archName = arch.toString().toLowerCase(); + format.getHeader().createEntry(HeaderTag.ARCH, archName); + format.getHeader().createEntry(HeaderTag.OS, osName); + format.getHeader().createEntry(HeaderTag.PLATFORM, archName + "-" + osName); + format.getHeader().createEntry(HeaderTag.RHNPLATFORM, archName); + } + + /** + * Sets the summary text for the file. The summary is generally a short, one line description of the + * function of the package, and is often shown by RPM tools. + * + * @param summary summary text. + */ + public void setSummary(CharSequence summary) { + if (summary != null) { + format.getHeader().createEntry(HeaderTag.SUMMARY, summary); + } + } + + /** + * Sets the description text for the file. The description is often a paragraph describing the + * package in detail. + * + * @param description description text. + */ + public void setDescription(CharSequence description) { + if (description != null) { + format.getHeader().createEntry(HeaderTag.DESCRIPTION, description); + } + } + + /** + * Sets the build host for the RPM. This is an internal field. + * + * @param host hostname of the build machine. + */ + public void setBuildHost(CharSequence host) { + if (host != null) { + format.getHeader().createEntry(HeaderTag.BUILDHOST, host); + } + } + + /** + * Lists the license under which this software is distributed. This field may be + * displayed by RPM tools. + * + * @param license the chosen distribution license. + */ + public void setLicense(CharSequence license) { + if (license != null) { + format.getHeader().createEntry(HeaderTag.LICENSE, license); + } + } + + /** + * Software group to which this package belongs. The group describes what sort of + * function the software package provides. + * + * @param group target group. + */ + public void setGroup(CharSequence group) { + if (group != null) { + format.getHeader().createEntry(HeaderTag.GROUP, group); + } + } + + /** + * Distribution tag listing the distributable package. + * + * @param distribution the distribution. + */ + public void setDistribution(CharSequence distribution) { + if (distribution != null) { + format.getHeader().createEntry(HeaderTag.DISTRIBUTION, distribution); + } + } + + /** + * Vendor tag listing the organization providing this software package. + * + * @param vendor software vendor. + */ + public void setVendor(CharSequence vendor) { + if (vendor != null) { + format.getHeader().createEntry(HeaderTag.VENDOR, vendor); + } + } + + /** + * Build packager, usually the username of the account building this RPM. + * + * @param packager packager name. + */ + public void setPackager(CharSequence packager) { + if (packager != null) { + format.getHeader().createEntry(HeaderTag.PACKAGER, packager); + } + } + + /** + * Website URL for this package, usually a project site. + * + * @param url the URL + */ + public void setUrl(CharSequence url) { + if (url != null) { + format.getHeader().createEntry(HeaderTag.URL, url); + } + } + + /** + * Declares a dependency that this package exports, and that other packages can use to + * provide library functions. Note that this method causes the existing provides set to be + * overwritten and therefore should be called before adding any other contents via + * the addProvides() methods. + * You should use addProvides() instead. + * + * @param provides dependency provided by this package. + */ + public void setProvides(CharSequence provides) { + if (provides != null) { + this.provides.clear(); + addProvides(provides, "", Flags.EQUAL); + } + } + + /** + * Sets the group of contents to include in this RPM. Note that this method causes the existing + * file set to be overwritten and therefore should be called before adding any other contents via + * the addFile() methods. + * + * @param contents the set of contents to use in constructing this RPM. + */ + public void setFiles(Contents contents) { + this.contents = contents; + } + + public Contents getContents() { + return contents; + } + + /** + * Adds a source rpm. + * + * @param rpm name of rpm source file + */ + public void setSourceRpm(String rpm) { + if (rpm != null) { + format.getHeader().createEntry(HeaderTag.SOURCERPM, rpm); + } + } + + /** + * Sets the package prefix directories to allow any files installed under + * them to be relocatable. + * + * @param prefixes Path prefixes which may be relocated + */ + public void setPrefixes(String... prefixes) { + if (prefixes != null && 0 < prefixes.length) { + format.getHeader().createEntry(HeaderTag.PREFIXES, prefixes); + } + } + + /** + * Declares a script file to be run as part of the RPM pre-transaction. The + * script will be run using the interpreter declared with the + * {@link #setPreTransProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPreTrans(String path) throws IOException { + setPreTrans(Paths.get(path)); + } + + /** + * Declares a script file to be run as part of the RPM pre-transaction. The + * script will be run using the interpreter declared with the + * {@link #setPreTransProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPreTrans(Path path) throws IOException { + setPreTransValue(readScript(path)); + } + + /** + * Declares a script to be run as part of the RPM pre-transaction. The + * script will be run using the interpreter declared with the + * {@link #setPreTransProgram(String)} method. + * + * @param content Script contents to run (i.e. shell commands) + */ + public void setPreTransValue(String content) { + setPreTransProgram(readProgram(content)); + if (content != null) { + format.getHeader().createEntry(HeaderTag.PRETRANSSCRIPT, content); + } + } + + /** + * Declares the interpreter to be used when invoking the RPM + * pre-transaction script that can be set with the + * {@link #setPreTransValue(String)} method. + * + * @param program Path to the interpreter + */ + public void setPreTransProgram(String program) { + if (null == program) { + format.getHeader().createEntry(HeaderTag.PRETRANSPROG, DEFAULTSCRIPTPROG); + } else if (0 == program.length()) { + format.getHeader().createEntry(HeaderTag.PRETRANSPROG, DEFAULTSCRIPTPROG); + } else { + format.getHeader().createEntry(HeaderTag.PRETRANSPROG, program); + } + } + + /** + * Declares a script file to be run as part of the RPM pre-installation. The + * script will be run using the interpreter declared with the + * {@link #setPreInstallProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPreInstall(String path) throws IOException { + setPreInstall(Paths.get(path)); + } + + /** + * Declares a script file to be run as part of the RPM pre-installation. The + * script will be run using the interpreter declared with the + * {@link #setPreInstallProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPreInstall(Path path) throws IOException { + setPreInstallValue(readScript(path)); + } + + /** + * Declares a script to be run as part of the RPM pre-installation. The + * script will be run using the interpreter declared with the + * {@link #setPreInstallProgram(String)} method. + * + * @param content Script contents to run (i.e. shell commands) + */ + public void setPreInstallValue(String content) { + setPreInstallProgram(readProgram(content)); + if (content != null) { + format.getHeader().createEntry(HeaderTag.PREINSCRIPT, content); + } + } + + /** + * Declares the interpreter to be used when invoking the RPM + * pre-installation script that can be set with the + * {@link #setPreInstallValue(String)} method. + * + * @param program Path to the interpretter + */ + public void setPreInstallProgram(String program) { + if (program == null) { + format.getHeader().createEntry(HeaderTag.PREINPROG, DEFAULTSCRIPTPROG); + } else if (0 == program.length()) { + format.getHeader().createEntry(HeaderTag.PREINPROG, DEFAULTSCRIPTPROG); + } else { + format.getHeader().createEntry(HeaderTag.PREINPROG, program); + } + } + + + /** + * Declares a script file to be run as part of the RPM post-installation. The + * script will be run using the interpreter declared with the + * {@link #setPostInstallProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPostInstall(String path) throws IOException { + setPostInstall(Paths.get(path)); + } + + /** + * Declares a script file to be run as part of the RPM post-installation. The + * script will be run using the interpreter declared with the + * {@link #setPostInstallProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPostInstall(Path path) throws IOException { + setPostInstallValue(readScript(path)); + } + + /** + * Declares a script to be run as part of the RPM post-installation. The + * script will be run using the interpreter declared with the + * {@link #setPostInstallProgram(String)} method. + * + * @param script Script contents to run (i.e. shell commands) + */ + public void setPostInstallValue(String script) { + setPostInstallProgram(readProgram(script)); + if (script != null) { + format.getHeader().createEntry(HeaderTag.POSTINSCRIPT, script); + } + } + + /** + * Declares the interpreter to be used when invoking the RPM + * post-installation script that can be set with the + * {@link #setPostInstallValue(String)} method. + * + * @param program Path to the interpreter + */ + public void setPostInstallProgram(String program) { + if (program == null) { + format.getHeader().createEntry(HeaderTag.POSTINPROG, DEFAULTSCRIPTPROG); + } else if (0 == program.length()) { + format.getHeader().createEntry(HeaderTag.POSTINPROG, DEFAULTSCRIPTPROG); + } else { + format.getHeader().createEntry(HeaderTag.POSTINPROG, program); + } + } + + /** + * Declares a script file to be run as part of the RPM pre-uninstallation. The + * script will be run using the interpreter declared with the + * {@link #setPreUninstallProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPreUninstall(String path) throws IOException { + setPreUninstall(Paths.get(path)); + } + + /** + * Declares a script file to be run as part of the RPM pre-uninstallation. The + * script will be run using the interpreter declared with the + * {@link #setPreUninstallProgram(String)} method. + * + * @param path Script to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPreUninstall(Path path) throws IOException { + setPreUninstallValue(readScript(path)); + } + + /** + * Declares a script to be run as part of the RPM pre-uninstallation. The + * script will be run using the interpreter declared with the + * {@link #setPreUninstallProgram(String)} method. + * + * @param script Script contents to run (i.e. shell commands) + */ + public void setPreUninstallValue(String script) { + setPreUninstallProgram(readProgram(script)); + if (script != null) { + format.getHeader().createEntry(HeaderTag.PREUNSCRIPT, script); + } + } + + /** + * Declares the interpreter to be used when invoking the RPM + * pre-uninstallation script that can be set with the + * {@link #setPreUninstallValue(String)} method. + * + * @param program Path to the interpreter + */ + public void setPreUninstallProgram(String program) { + if (program == null) { + format.getHeader().createEntry(HeaderTag.PREUNPROG, DEFAULTSCRIPTPROG); + } else if (0 == program.length()) { + format.getHeader().createEntry(HeaderTag.PREUNPROG, DEFAULTSCRIPTPROG); + } else { + format.getHeader().createEntry(HeaderTag.PREUNPROG, program); + } + } + + + /** + * Declares a script file to be run as part of the RPM post-uninstallation. The + * script will be run using the interpreter declared with the + * {@link #setPostUninstallProgram(String)} method. + * + * @param path Script contents to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPostUninstall(String path) throws IOException { + setPostUninstall(Paths.get(path)); + } + + /** + * Declares a script file to be run as part of the RPM post-uninstallation. The + * script will be run using the interpreter declared with the + * {@link #setPostUninstallProgram(String)} method. + * + * @param path Script contents to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPostUninstall(Path path) throws IOException { + setPostUninstallValue(readScript(path)); + } + + /** + * Declares a script to be run as part of the RPM post-uninstallation. The + * script will be run using the interpreter declared with the + * {@link #setPostUninstallProgram(String)} method. + * + * @param content Script contents to run (i.e. shell commands) + */ + public void setPostUninstallValue(String content) { + setPostUninstallProgram(readProgram(content)); + if (content != null) { + format.getHeader().createEntry(HeaderTag.POSTUNSCRIPT, content); + } + } + + /** + * Declares the interpreter to be used when invoking the RPM + * post-uninstallation script that can be set with the + * {@link #setPostUninstallValue(String)} method. + * + * @param program Path to the interpreter + */ + public void setPostUninstallProgram(String program) { + if (program == null) { + format.getHeader().createEntry(HeaderTag.POSTUNPROG, DEFAULTSCRIPTPROG); + } else if (0 == program.length()) { + format.getHeader().createEntry(HeaderTag.POSTUNPROG, DEFAULTSCRIPTPROG); + } else { + format.getHeader().createEntry(HeaderTag.POSTUNPROG, program); + } + } + + /** + * Declares a script file to be run as part of the RPM post-transaction. The + * script will be run using the interpreter declared with the + * {@link #setPostTransProgram(String)} method. + * + * @param path Script contents to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPostTrans(String path) throws IOException { + setPostTrans(Paths.get(path)); + } + + /** + * Declares a script file to be run as part of the RPM post-transaction. The + * script will be run using the interpreter declared with the + * {@link #setPostTransProgram(String)} method. + * + * @param path Script contents to run (i.e. shell commands) + * @throws IOException there was an IO error + */ + public void setPostTrans(Path path) throws IOException { + setPostTransValue(readScript(path)); + } + + /** + * Declares a script to be run as part of the RPM post-transaction. The + * script will be run using the interpreter declared with the + * {@link #setPostTransProgram(String)} method. + * + * @param content Script contents to run (i.e. shell commands) + */ + public void setPostTransValue(String content) { + setPostTransProgram(readProgram(content)); + if (content != null) { + format.getHeader().createEntry(HeaderTag.POSTTRANSSCRIPT, content); + } + } + + /** + * Declares the interpreter to be used when invoking the RPM + * post-transaction script that can be set with the + * {@link #setPostTransValue(String)} method. + * + * @param program Path to the interpreter + */ + public void setPostTransProgram(String program) { + if (program == null) { + format.getHeader().createEntry(HeaderTag.POSTTRANSPROG, DEFAULTSCRIPTPROG); + } else if (0 == program.length()) { + format.getHeader().createEntry(HeaderTag.POSTTRANSPROG, DEFAULTSCRIPTPROG); + } else { + format.getHeader().createEntry(HeaderTag.POSTTRANSPROG, program); + } + } + + /** + * Adds a trigger to the RPM package. + * + * @param script the script to add. + * @param prog the interpreter with which to run the script. + * @param depends the map of rpms and versions that will trigger the script + * @param flag the trigger type (SCRIPT_TRIGGERPREIN, SCRIPT_TRIGGERIN, SCRIPT_TRIGGERUN, or SCRIPT_TRIGGERPOSTUN) + * @throws IOException there was an IO error + */ + public void addTrigger(Path script, String prog, Map depends, int flag) + throws IOException { + triggerscripts.add(readScript(script)); + if (null == prog) { + triggerscriptprogs.add(DEFAULTSCRIPTPROG); + } else if (0 == prog.length()) { + triggerscriptprogs.add(DEFAULTSCRIPTPROG); + } else { + triggerscriptprogs.add(prog); + } + for (Map.Entry depend : depends.entrySet()) { + triggernames.add(depend.getKey()); + triggerflags.add(depend.getValue().getInt() | flag); + triggerversions.add(depend.getValue().getString()); + triggerindexes.add(triggerCounter); + } + triggerCounter++; + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the path content to include in this rpm. + * @param mode the mode of the target file in standard three octet notation + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode) throws IOException { + contents.addFile(path, source, mode); + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file to include in this archive. + * @param mode the mode of the target file in standard three octet notation + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode, int dirmode) throws IOException { + contents.addFile(path, source, mode, dirmode); + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file to include in this archive. + * @param mode the mode of the target file in standard three octet notation + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @param uname user owner for the given file + * @param gname group owner for the given file + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode, int dirmode, String uname, String gname) + throws IOException { + contents.addFile(path, source, mode, null, uname, gname, dirmode); + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file to include in this rpm. + * @param mode the mode of the target file in standard three octet notation + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @param directive directive indicating special handling for this file. + * @param uname user owner for the given file + * @param gname group owner for the given file + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode, int dirmode, EnumSet directive, String uname, + String gname) throws IOException { + contents.addFile(path, source, mode, directive, uname, gname, dirmode); + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file to include in this rpm. + * @param mode the mode of the target file in standard three octet notation, or -1 for default. + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @param directive directive indicating special handling for this file. + * @param uname user owner for the given file, or null for default user. + * @param gname group owner for the given file, or null for default group. + * @param addParents whether to create parent directories for the file, defaults to true for other methods. + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode, int dirmode, EnumSet directive, String uname, + String gname, boolean addParents) throws IOException { + contents.addFile(path, source, mode, directive, uname, gname, dirmode, addParents); + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file to include in this rpm. + * @param mode the mode of the target file in standard three octet notation, or -1 for default. + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @param directive directive indicating special handling for this file. + * @param uname user owner for the given file, or null for default user. + * @param gname group owner for the given file, or null for default group. + * @param addParents whether to create parent directories for the file, defaults to true for other methods. + * @param verifyFlags verify flags + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode, int dirmode, EnumSet directive, String uname, + String gname, boolean addParents, int verifyFlags) throws IOException { + contents.addFile(path, source, mode, directive, uname, gname, dirmode, addParents, verifyFlags); + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file to include in this rpm. + * @param mode the mode of the target file in standard three octet notation + * @param directive directive indicating special handling for this file. + * @param uname user owner for the given file + * @param gname group owner for the given file + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode, EnumSet directive, String uname, String gname) + throws IOException { + contents.addFile(path, source, mode, directive, uname, gname); + } + + /** + * Add the specified file to the repository payload in order. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file content to include in this rpm. + * @param mode the mode of the target file in standard three octet notation + * @param directive directive indicating special handling for this file. + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source, int mode, EnumSet directive) + throws IOException { + contents.addFile(path, source, mode, directive); + } + + /** + * Adds the file to the repository with the default mode of 644. + * + * @param path the absolute path at which this file will be installed. + * @param source the file content to include in this archive. + * @throws IOException there was an IO error + */ + public void addFile(String path, Path source) + throws IOException { + contents.addFile(path, source); + } + + /** + * Add the specified file to the repository payload in order by URL. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file content to include in this rpm. + * @param mode the mode of the target file in standard three octet notation + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @throws IOException there was an IO error + */ + public void addURL(String path, URL source, int mode, int dirmode) throws IOException { + contents.addURL(path, source, mode, null, null, null, dirmode); + } + + /** + * Add the specified file to the repository payload in order by URL. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file content to include in this rpm. + * @param mode the mode of the target file in standard three octet notation + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @param username ownership of added file + * @param group ownership of added file + * @throws IOException there was an IO error + */ + public void addURL(String path, URL source, int mode, int dirmode, String username, String group) + throws IOException { + contents.addURL(path, source, mode, null, username, group, dirmode); + } + + /** + * Add the specified file to the repository payload in order by URL. + * The required header entries will automatically be generated + * to record the directory names and file names, as well as their + * digests. + * + * @param path the absolute path at which this file will be installed. + * @param source the file content to include in this rpm. + * @param mode the mode of the target file in standard three octet notation + * @param dirmode the mode of the parent directories in standard three octet notation, or -1 for default. + * @param directives directive indicating special handling for this file. + * @param username ownership of added file + * @param group ownership of added file + * @throws IOException there was an IO error + */ + public void addURL(String path, URL source, int mode, int dirmode, EnumSet directives, String username, + String group) throws IOException { + contents.addURL(path, source, mode, directives, username, group, dirmode); + } + + /** + * Adds the directory to the repository with the default mode of 644. + * + * @param path the absolute path to add as a directory. + * @throws IOException there was an IO error + */ + public void addDirectory(String path) throws IOException { + contents.addDirectory(path); + } + + /** + * Adds the directory to the repository. + * + * @param path the absolute path to add as a directory. + * @param permissions the mode of the directory in standard three octet notation. + * @param directives directive indicating special handling for this file. + * @param uname user owner of the directory + * @param gname group owner of the directory + * @throws IOException there was an IO error + */ + public void addDirectory(String path, int permissions, EnumSet directives, String uname, String gname) + throws IOException { + contents.addDirectory(path, permissions, directives, uname, gname); + } + + /** + * Adds the directory to the repository. + * + * @param path the absolute path to add as a directory. + * @param permissions the mode of the directory in standard three octet notation. + * @param directives directives indicating special handling for this file. + * @param uname user owner of the directory + * @param gname group owner of the directory + * @param addParents whether to add parent directories to the rpm + * @throws IOException there was an IO error + */ + public void addDirectory(String path, int permissions, EnumSet directives, String uname, String gname, + boolean addParents) throws IOException { + contents.addDirectory(path, permissions, directives, uname, gname, addParents); + } + + /** + * Adds the directory to the repository with the default mode of 644. + * + * @param path the absolute path to add as a directory. + * @param directive directive indicating special handling for this file. + * @throws IOException there was an IO error + */ + public void addDirectory(String path, EnumSet directive) throws IOException { + contents.addDirectory(path, directive); + } + + /** + * Adds a symbolic link to the repository. + * + * @param path the absolute path at which this link will be installed. + * @param target the path of the file this link will point to. + * @throws IOException there was an IO error + */ + public void addLink(String path, String target) throws IOException { + contents.addLink(path, target); + } + + /** + * Adds a symbolic link to the repository. + * + * @param path the absolute path at which this link will be installed. + * @param target the path of the file this link will point to. + * @param permissions the permissions flags + * @throws IOException there was an IO error + */ + public void addLink(String path, String target, int permissions) + throws IOException { + contents.addLink(path, target, permissions); + } + + /** + * Adds a symbolic link to the repository. + * + * @param path the absolute path at which this link will be installed. + * @param target the path of the file this link will point to. + * @param permissions the permissions flags + * @param username user owner of the link + * @param groupname group owner of the link + * @throws IOException there was an IO error + */ + public void addLink(String path, String target, int permissions, String username, String groupname) + throws IOException { + contents.addLink(path, target, permissions, username, groupname); + } + + /** + * Adds the supplied Changelog path as a Changelog to the header. + * + * @param changelogFile File containing the Changelog information + * @throws IOException if file does not exist or cannot be read + * @throws ChangelogParseException if file is not of the correct format. + */ + public void addChangelog(String changelogFile) throws IOException, ChangelogParseException { + new ChangelogHandler(format.getHeader()).addChangeLog(Paths.get(changelogFile)); + } + + /** + * Adds the supplied Changelog path as a Changelog to the header. + * + * @param changelogFile File containing the Changelog information + * @throws IOException if file does not exist or cannot be read + * @throws ChangelogParseException if file is not of the correct format. + */ + public void addChangelog(Path changelogFile) throws IOException, ChangelogParseException { + new ChangelogHandler(format.getHeader()).addChangeLog(changelogFile); + } + + /** + * Adds the supplied Changelog file as a Changelog to the header. + * + * @param changelogFile URL containing the Changelog information + * @throws IOException if file does not exist or cannot be read + * @throws ChangelogParseException if file is not of the correct format. + */ + public void addChangelog(URL changelogFile) throws IOException, ChangelogParseException { + new ChangelogHandler(format.getHeader()).addChangeLog(changelogFile); + } + + /** + * Adds the supplied Changelog file as a Changelog to the header. + * + * @param changelogFile URL containing the Changelog information + * @throws IOException if file does not exist or cannot be read + * @throws ChangelogParseException if file is not of the correct format. + */ + public void addChangelog(InputStream changelogFile) throws IOException, ChangelogParseException { + new ChangelogHandler(format.getHeader()).addChangeLog(changelogFile); + } + + /** + * Sets the PGP key ring used for header and header + payload signature. + * + * @param privateKeyRing the private key ring input stream + * @throws IOException if file does not exist or cannot be read + */ + public void setPrivateKeyRing(String privateKeyRing) throws IOException { + setPrivateKeyRing(Paths.get(privateKeyRing)); + } + + /** + * Sets the PGP key ring used for header and header + payload signature. + * + * @param privateKeyRing the private key ring input stream + * @throws IOException if file does not exist or cannot be read + */ + public void setPrivateKeyRing(Path privateKeyRing) throws IOException { + setPrivateKeyRing(Files.newInputStream(privateKeyRing)); + } + + /** + * Sets the PGP key ring used for header and header + payload signature. + * + * @param privateKeyRing the private key ring input stream + */ + public void setPrivateKeyRing(InputStream privateKeyRing) { + this.privateKeyRing = privateKeyRing; + } + + /** + * Selects a private key from the current {@link #setPrivateKeyRing(java.io.InputStream) private key ring}. + * If no key is specified, the first signing key will be selected. + * + * @param privateKeyId long value from hex key id + */ + public void setPrivateKeyId(String privateKeyId) { + setPrivateKeyId(Long.decode("0x" + privateKeyId)); + } + + /** + * Selects a private key from the current {@link #setPrivateKeyRing(java.io.InputStream) private key ring}. + * If no key is specified, the first signing key will be selected. + * + * @param privateKeyId long value from hex key id + */ + public void setPrivateKeyId(Long privateKeyId) { + this.privateKeyId = privateKeyId; + } + + /** + * Passphrase for the private key. + * + * @param privateKeyPassphrase the private key pass phrase + */ + public void setPrivateKeyPassphrase(String privateKeyPassphrase) { + this.privateKeyPassphrase = privateKeyPassphrase; + } + + /** + * Set RPM package name. + * + * @param packageName the RPM package name + */ + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + /** + * Get package name. + * @return the RPM package name or null + */ + public String getPackageName() { + return packageName; + } + + /** + * Generates an RPM with a standard name consisting of the RPM package name, version, release, + * and type in the given directory. + * + * @param directory the destination path for the new RPM file. + * @throws IOException there was an IO error + * @throws RpmException if RPM could not be generated + */ + public void build(Path directory) throws RpmException, IOException { + if (packageName == null) { + setPackageName(format.getLead().getName() + "." + format.getLead().getArch().toString().toLowerCase() + ".rpm"); + } + Path path = directory.resolve(packageName); + try (SeekableByteChannel channel = Files.newByteChannel(path, + StandardOpenOption.WRITE, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING)) { + build(channel); + } + } + + /** + * Generates the RPM archive to the provided file channel. + * + * @param channel the {@link SeekableByteChannel} to which the resulting RPM archive will be written. + * @throws IOException there was an IO error + * @throws RpmException if RPM generation fails + */ + @SuppressWarnings("unchecked") + public void build(SeekableByteChannel channel) throws RpmException, IOException { + WritableChannelWrapper output = new WritableChannelWrapper(channel); + format.getHeader().createEntry(HeaderTag.REQUIRENAME, getArrayOfNames(requires)); + format.getHeader().createEntry(HeaderTag.REQUIREVERSION, getArrayOfVersions(requires)); + format.getHeader().createEntry(HeaderTag.REQUIREFLAGS, getArrayOfFlags(requires)); + if (obsoletes.size() > 0) { + format.getHeader().createEntry(HeaderTag.OBSOLETENAME, getArrayOfNames(obsoletes)); + format.getHeader().createEntry(HeaderTag.OBSOLETEVERSION, getArrayOfVersions(obsoletes)); + format.getHeader().createEntry(HeaderTag.OBSOLETEFLAGS, getArrayOfFlags(obsoletes)); + } + if (conflicts.size() > 0) { + format.getHeader().createEntry(HeaderTag.CONFLICTNAME, getArrayOfNames(conflicts)); + format.getHeader().createEntry(HeaderTag.CONFLICTVERSION, getArrayOfVersions(conflicts)); + format.getHeader().createEntry(HeaderTag.CONFLICTFLAGS, getArrayOfFlags(conflicts)); + } + if (provides.size() > 0) { + format.getHeader().createEntry(HeaderTag.PROVIDENAME, getArrayOfNames(provides)); + format.getHeader().createEntry(HeaderTag.PROVIDEVERSION, getArrayOfVersions(provides)); + format.getHeader().createEntry(HeaderTag.PROVIDEFLAGS, getArrayOfFlags(provides)); + } + format.getHeader().createEntry(HeaderTag.SIZE, contents.getTotalSize()); + if (contents.size() > 0) { + format.getHeader().createEntry(HeaderTag.DIRNAMES, contents.getDirNames()); + format.getHeader().createEntry(HeaderTag.DIRINDEXES, contents.getDirIndexes()); + format.getHeader().createEntry(HeaderTag.BASENAMES, contents.getBaseNames()); + } + if (triggerCounter > 0) { + format.getHeader().createEntry(HeaderTag.TRIGGERSCRIPTS, + triggerscripts.toArray(new String[triggerscripts.size()])); + format.getHeader().createEntry(HeaderTag.TRIGGERNAME, + triggernames.toArray(new String[triggernames.size()])); + format.getHeader().createEntry(HeaderTag.TRIGGERVERSION, + triggerversions.toArray(new String[triggerversions.size()])); + format.getHeader().createEntry(HeaderTag.TRIGGERFLAGS, + triggerflags.toArray(new Integer[triggerflags.size()])); + format.getHeader().createEntry(HeaderTag.TRIGGERINDEX, + triggerindexes.toArray(new Integer[triggerindexes.size()])); + format.getHeader().createEntry(HeaderTag.TRIGGERSCRIPTPROG, + triggerscriptprogs.toArray(new String[triggerscriptprogs.size()])); + } + if (contents.size() > 0) { + format.getHeader().createEntry(HeaderTag.FILEDIGESTALGOS, hashAlgo.num()); + format.getHeader().createEntry(HeaderTag.FILEDIGESTS, contents.getDigests(hashAlgo)); + format.getHeader().createEntry(HeaderTag.FILESIZES, contents.getSizes()); + format.getHeader().createEntry(HeaderTag.FILEMODES, contents.getModes()); + format.getHeader().createEntry(HeaderTag.FILERDEVS, contents.getRdevs()); + format.getHeader().createEntry(HeaderTag.FILEMTIMES, contents.getMtimes()); + format.getHeader().createEntry(HeaderTag.FILELINKTOS, contents.getLinkTos()); + format.getHeader().createEntry(HeaderTag.FILEFLAGS, contents.getFlags()); + format.getHeader().createEntry(HeaderTag.FILEUSERNAME, contents.getUsers()); + format.getHeader().createEntry(HeaderTag.FILEGROUPNAME, contents.getGroups()); + format.getHeader().createEntry(HeaderTag.FILEVERIFYFLAGS, contents.getVerifyFlags()); + format.getHeader().createEntry(HeaderTag.FILEDEVICES, contents.getDevices()); + format.getHeader().createEntry(HeaderTag.FILEINODES, contents.getInodes()); + format.getHeader().createEntry(HeaderTag.FILELANGS, contents.getLangs()); + format.getHeader().createEntry(HeaderTag.FILECONTEXTS, contents.getContexts()); + } + format.getHeader().createEntry(HeaderTag.PAYLOADFLAGS, "9"); + SpecEntry sigsize = + (SpecEntry) format.getSignatureHeader().addEntry(SignatureTag.LEGACY_SIGSIZE, 1); + SpecEntry signaturHeaderPayloadEntry = + (SpecEntry) format.getSignatureHeader().addEntry(SignatureTag.PAYLOADSIZE, 1); + SpecEntry md5Entry = + (SpecEntry) format.getSignatureHeader().addEntry(SignatureTag.LEGACY_MD5, 16); + SpecEntry shaEntry = + (SpecEntry) format.getSignatureHeader().addEntry(SignatureTag.SHA1HEADER, 1); + shaEntry.setSize(SHASIZE); + + SignatureGenerator signatureGenerator = new SignatureGenerator(privateKeyRing, privateKeyId, privateKeyPassphrase); + signatureGenerator.prepare(format.getSignatureHeader(), hashAlgo); + format.getLead().write(channel); + SpecEntry signatureEntry = + (SpecEntry) format.getSignatureHeader().addEntry(SignatureTag.SIGNATURES, 16); + signatureEntry.setValues(createHeaderIndex(HeaderTag.SIGNATURES.getCode(), format.getSignatureHeader().count())); + ChannelWrapper.empty(output, ByteBuffer.allocate(format.getSignatureHeader().write(channel))); + + ChannelWrapper.Key sigsizekey = output.start(); + ChannelWrapper.Key shakey = signatureGenerator.startDigest(output, "SHA"); + ChannelWrapper.Key md5key = signatureGenerator.startDigest(output, "MD5"); + signatureGenerator.startBeforeHeader(output, hashAlgo); + // Region concept. This tag contains an index record which specifies the portion of the Header Record + // which was used for the calculation of a signature. This data shall be preserved or any header-only signature + // will be invalidated. + SpecEntry immutable = + (SpecEntry) format.getHeader().addEntry(HeaderTag.HEADERIMMUTABLE, 16); + immutable.setValues(createHeaderIndex(HeaderTag.IMMUTABLE.getCode(), format.getHeader().count())); + format.getHeader().write(output); + shaEntry.setValues(new String[]{hex(output.finish(shakey))}); + signatureGenerator.finishAfterHeader(output); + OutputStream compressedOutputStream = createCompressedStream(Channels.newOutputStream(output)); + WritableChannelWrapper compressedOutput = + new WritableChannelWrapper(Channels.newChannel(compressedOutputStream)); + ChannelWrapper.Key payloadkey = compressedOutput.start(); + int total = 0; + ByteBuffer buffer = ByteBuffer.allocate(4096); + for (CpioHeader header : contents.headers()) { + if ((header.getFlags() & Directive.GHOST.flag()) == Directive.GHOST.flag()) { + continue; + } + String path = header.getName(); + if (path.startsWith("/")) { + header.setName("." + path); + } + total = header.write(compressedOutput, total); + Object object = contents.getSource(header); + if (object instanceof Path) { + try (ReadableByteChannel readableByteChannel = Files.newByteChannel((Path) object)) { + while (readableByteChannel.read((ByteBuffer) buffer.rewind()) > 0) { + total += compressedOutput.write((ByteBuffer) buffer.flip()); + buffer.compact(); + } + total += header.skip(compressedOutput, total); + } + } else if (object instanceof URL) { + try (ReadableByteChannel in = Channels.newChannel(((URL) object).openConnection().getInputStream())) { + while (in.read((ByteBuffer) buffer.rewind()) > 0) { + total += compressedOutput.write((ByteBuffer) buffer.flip()); + buffer.compact(); + } + total += header.skip(compressedOutput, total); + } + } else if (object instanceof CharSequence) { + CharSequence target = (CharSequence) object; + total += compressedOutput.write(ByteBuffer.wrap(String.valueOf(target).getBytes(StandardCharsets.UTF_8))); + total += header.skip(compressedOutput, target.length()); + } + } + CpioHeader trailer = new CpioHeader(); + trailer.setLast(); + total = trailer.write(compressedOutput, total); + trailer.skip(compressedOutput, total); + int length = compressedOutput.finish(payloadkey); + int pad = difference(length, 3); + ChannelWrapper.empty(compressedOutput, ByteBuffer.allocate(pad)); + length += pad; + signaturHeaderPayloadEntry.setValues(new Integer[]{length}); + // flush compressed stream here + compressedOutputStream.flush(); + md5Entry.setValues(output.finish(md5key)); + sigsize.setValues(new Integer[]{output.finish(sigsizekey)}); + signatureGenerator.finishAfterPayload(output); + format.getSignatureHeader().writePending(channel); + } + + /** + * Returns the header index. + * + * @param tag the tag to get + * @param count the number to get + * @return the header bytes + */ + private byte[] createHeaderIndex(int tag, int count) { + ByteBuffer buffer = ByteBuffer.allocate(16); + buffer.putInt(tag); + buffer.putInt(0x00000007); // data type (7 = bin entry) + buffer.putInt(count * -16); // offset, where to find the data in the storage area + buffer.putInt(0x00000010); // how many data items are stored in this key + return buffer.array(); + } + + private OutputStream createCompressedStream(OutputStream outputStream) throws IOException { + switch (compressionType) { + case NONE: + return outputStream; + case GZIP: + return new GZIPOutputStream(outputStream, true); + case BZIP2: + return new Bzip2OutputStream(outputStream); + case XZ: + X86Options x86 = new X86Options(); + LZMA2Options lzma2 = new LZMA2Options(); + FilterOptions[] options = { x86, lzma2 }; + return new XZOutputStream(outputStream, options); + } + // not reached + return outputStream; + } + + + /** + * Return the content of the specified script file as a String. + * + * @param path the script file to be read + */ + private String readScript(Path path) throws IOException { + if (path == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + try (BufferedReader in = Files.newBufferedReader(path)) { + String line; + while ((line = in.readLine()) != null) { + sb.append(line).append("\n"); + } + } + return sb.toString(); + } + + /** + * Returns the program use to run the specified script (guessed by parsing + * the shebang at the beginning of the script). + * + * @param script script + */ + private String readProgram(String script) { + String program = null; + if (script != null) { + Pattern pattern = Pattern.compile("^#!(/.*)"); + Matcher matcher = pattern.matcher(script); + if (matcher.find()) { + program = matcher.group(1); + } + } + return program; + } + + + /** + * + */ + static class Dependency { + + private String name; + + private String version; + + private Integer flags; + + /** + * Creates a new dependency. + * + * @param name Name (e.g. "httpd") + * @param version Version (e.g. "1.0") + * @param flags Flags (e.g. "GREATER | Flags.EQUAL") + */ + Dependency(String name, String version, Integer flags) { + this.name = name; + this.version = version; + this.flags = flags; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public Integer getFlags() { + return flags; + } + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/RpmReader.java b/rpm-core/src/main/java/org/xbib/rpm/RpmReader.java new file mode 100644 index 0000000..392914c --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/RpmReader.java @@ -0,0 +1,134 @@ +package org.xbib.rpm; + +import org.xbib.io.compress.bzip2.Bzip2InputStream; +import org.xbib.io.compress.xz.XZInputStream; +import org.xbib.rpm.format.Format; +import org.xbib.rpm.header.Header; +import org.xbib.rpm.header.HeaderTag; +import org.xbib.rpm.header.entry.SpecEntry; +import org.xbib.rpm.io.ChannelWrapper; +import org.xbib.rpm.io.ReadableChannelWrapper; +import org.xbib.rpm.payload.CompressionType; +import org.xbib.rpm.payload.CpioHeader; +import org.xbib.rpm.signature.SignatureTag; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.GZIPInputStream; + +/** + * The RPM reader reads an archive and outputs useful information about its contents. The reader will + * output the headers of the RPM format itself, aswell as the individual headers for the particular + * packaged content. + * In addition, the reader will read through the payload and output information about each file + * contained in the embedded CPIO payload. + */ +public class RpmReader { + + public RpmReader() { + } + + public Format read(Path path) throws IOException { + return read(Files.newInputStream((path))); + } + + public Format read(InputStream inputStream) throws IOException { + try (InputStream thisInputStream = inputStream) { + return read(new ReadableChannelWrapper(Channels.newChannel(thisInputStream)), thisInputStream); + } + } + + public Format read(ReadableChannelWrapper readableChannelWrapper, InputStream inputStream) throws IOException { + Format format = readHeader(readableChannelWrapper); + Header rpmHeader = format.getHeader(); + try (InputStream uncompressed = createUncompressedStream(rpmHeader, inputStream)) { + readableChannelWrapper = new ReadableChannelWrapper(Channels.newChannel(uncompressed)); + CpioHeader header; + int total = 0; + do { + header = new CpioHeader(); + total = header.read(readableChannelWrapper, total); + final int skip = header.getFileSize(); + if (uncompressed.skip(skip) != skip) { + throw new RuntimeException(); + } + total += header.getFileSize(); + } while (!header.isLast()); + } + return format; + } + + + public Format readHeader(Path path) throws IOException { + return readHeader(Files.newInputStream((path))); + } + + public Format readHeader(InputStream inputStream) throws IOException { + try (InputStream thisInputStream = inputStream) { + return readHeader(new ReadableChannelWrapper(Channels.newChannel(thisInputStream))); + } + } + + /** + * Reads the headers of an RPM and returns a description of it and it's format. + * + * @param channelWrapper the channel wrapper to read input from + * @return information describing the RPM file + * @throws IOException if an error occurs reading the file + */ + public Format readHeader(ReadableChannelWrapper channelWrapper) throws IOException { + Format format = new Format(); + ChannelWrapper.Key headerStartKey = channelWrapper.start(); + ChannelWrapper.Key lead = channelWrapper.start(); + format.getLead().read(channelWrapper); + ChannelWrapper.Key signature = channelWrapper.start(); + int count = format.getSignatureHeader().read(channelWrapper); + SpecEntry sigEntry = format.getSignatureHeader().getEntry(SignatureTag.SIGNATURES); + int expected = sigEntry == null ? 0 : + ByteBuffer.wrap((byte[]) sigEntry.getValues(), 8, 4).getInt() / -16; + Integer headerStartPos = channelWrapper.finish(headerStartKey); + format.getHeader().setStartPos(headerStartPos); + ChannelWrapper.Key headerKey = channelWrapper.start(); + count = format.getHeader().read(channelWrapper); + SpecEntry immutableEntry = format.getHeader().getEntry(HeaderTag.HEADERIMMUTABLE); + expected = immutableEntry == null ? 0 : + ByteBuffer.wrap((byte[]) immutableEntry.getValues(), 8, 4).getInt() / -16; + Integer headerLength = channelWrapper.finish(headerKey); + format.getHeader().setEndPos(headerStartPos + headerLength); + return format; + } + + /** + * Create the proper stream wrapper to handling the payload section based on the + * payload compression header tag. + * + * @param header the header + * @param inputStream raw input stream of the rpm + * @return the "proper" input stream + * @throws IOException an IO error occurred + */ + private static InputStream createUncompressedStream(Header header, InputStream inputStream) throws IOException { + InputStream compressedInput = inputStream; + SpecEntry pcEntry = header.getEntry(HeaderTag.PAYLOADCOMPRESSOR); + String[] pc = (String[]) pcEntry.getValues(); + CompressionType compressionType = CompressionType.valueOf(pc[0].toUpperCase()); + switch (compressionType) { + case NONE: + break; + case GZIP: + compressedInput = new GZIPInputStream(inputStream); + break; + case BZIP2: + compressedInput = new Bzip2InputStream(inputStream); + break; + case XZ: + compressedInput = new XZInputStream(inputStream); + break; + } + return compressedInput; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogEntry.java b/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogEntry.java new file mode 100644 index 0000000..11a4b80 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogEntry.java @@ -0,0 +1,63 @@ +package org.xbib.rpm.changelog; + +import java.time.Instant; + +/** + * This class defines a Plain Old Java Object encapsulating one entry in a Changelog. For example: + * Wed Nov 08 2006 George Washington + * - Add the foo feature + * Add the bar feature + */ +public class ChangelogEntry { + /** + * The date portion of the Changelog entry. + * In the above Example: Wed Nov 08 2006 + */ + private Instant changeLogTime; + /** + * The "user" or "name" portion of the Changelog entry. + * In the above Example: George Washington + * in other words, the rest of the first line of the entry, + * not counting the date portion + */ + private String userMakingChange; + /** + * Freeform text on the second line and beyond of the Changelog entry. + * In the above Example: + * - Add the foo feature + * Add the bar feature + * Terminates with a line beginning with an asterisk, which defines a new Changelog entry. + */ + private String description; + + public ChangelogEntry() { + } + + public boolean isComplete() { + return changeLogTime != null && userMakingChange != null && description != null; + } + + public Instant getChangeLogTime() { + return changeLogTime; + } + + public void setChangeLogTime(Instant changeLogTime) { + this.changeLogTime = changeLogTime; + } + + public String getUserMakingChange() { + return userMakingChange; + } + + public void setUserMakingChange(String userMakingChange) { + this.userMakingChange = userMakingChange; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogHandler.java b/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogHandler.java new file mode 100644 index 0000000..15c70ca --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogHandler.java @@ -0,0 +1,69 @@ +package org.xbib.rpm.changelog; + +import org.xbib.rpm.exception.ChangelogParseException; +import org.xbib.rpm.header.Header; +import org.xbib.rpm.header.HeaderTag; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * This class manages the process of adding a Changelog to the header. + */ +public class ChangelogHandler { + private final Header header; + + public ChangelogHandler(Header header) { + this.header = header; + } + + /** + * Adds the specified file to the archive. + * + * @param changelogFile the Changelog file to be added + * @throws IOException if the specified file cannot be read + * @throws ChangelogParseException if the file violates the requirements of a Changelog + */ + public void addChangeLog(Path changelogFile) throws IOException, ChangelogParseException { + addChangeLog(Files.newInputStream(changelogFile)); + } + + /** + * Adds the specified file to the archive. + * + * @param changelogFile the Changelog URL to be added + * @throws IOException if the specified file cannot be read + * @throws ChangelogParseException if the file violates the requirements of a Changelog + */ + public void addChangeLog(URL changelogFile) throws IOException, ChangelogParseException { + addChangeLog(changelogFile.openStream()); + } + + /** + * Adds the specified file to the archive. + * + * @param changelogStream the changelog stream to be added + * @throws IOException if the specified file cannot be read + * @throws ChangelogParseException if the file violates the requirements of a Changelog + */ + public void addChangeLog(InputStream changelogStream) throws IOException, ChangelogParseException { + try (InputStream changelog = changelogStream) { + ChangelogParser parser = new ChangelogParser(); + List entries = parser.parse(changelog); + for (ChangelogEntry entry : entries) { + addChangeLogEntry(entry); + } + } + } + + private void addChangeLogEntry(ChangelogEntry entry) { + Long epochSecs = entry.getChangeLogTime().toEpochMilli() / 1000L; + header.addOrAppendEntry(HeaderTag.CHANGELOGTIME, new Integer[]{epochSecs.intValue()}); + header.addOrAppendEntry(HeaderTag.CHANGELOGNAME, new String[]{entry.getUserMakingChange()}); + header.addOrAppendEntry(HeaderTag.CHANGELOGTEXT, new String[]{entry.getDescription()}); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogParser.java b/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogParser.java new file mode 100644 index 0000000..e9420b5 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/changelog/ChangelogParser.java @@ -0,0 +1,148 @@ +package org.xbib.rpm.changelog; + +import org.xbib.rpm.exception.ChangelogParseException; +import org.xbib.rpm.exception.DatesOutOfSequenceException; +import org.xbib.rpm.exception.IncompleteChangelogEntryException; +import org.xbib.rpm.exception.InvalidChangelogDateException; +import org.xbib.rpm.exception.NoInitialAsteriskException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * + */ +public class ChangelogParser { + + public static final DateTimeFormatter CHANGELOG_FORMAT = + DateTimeFormatter.ofPattern("EEE MMM dd yyyy").withLocale(Locale.US).withZone(ZoneId.systemDefault()); + + /** + * @param lines an array of lines read from the Changelog file + * @return a List of ChangeLogEntry objects + * @throws DateTimeParseException if date could not be parsed + * @throws ChangelogParseException if any of the rules of a Changelog is violated by the input + */ + public List parse(String[] lines) throws DateTimeParseException, ChangelogParseException { + final int timeLen = 15; + List result = new ArrayList<>(); + if (lines.length == 0) { + return result; + } + ParsingState state = ParsingState.NEW; + Instant lastTime = null; + ChangelogEntry entry = new ChangelogEntry(); + String restOfLine = null; + StringBuilder descr = new StringBuilder(); + int index = 0; + String line = lines[index]; + lineloop: + while (true) { + switch (state) { + case NEW: + if (line.startsWith("#")) { + if (++index < lines.length) { + line = lines[index]; + continue; + } else { + return result; + } + } else if (!line.startsWith("*")) { + throw new NoInitialAsteriskException(); + } + restOfLine = line.substring(1).trim(); + state = ParsingState.TIME; + break; + case TIME: + if (restOfLine.length() < timeLen) { + throw new InvalidChangelogDateException(restOfLine); + } + String timestr = restOfLine.substring(0, timeLen); + try { + Instant entryTime = LocalDate.parse(timestr, CHANGELOG_FORMAT) + .atTime(LocalTime.MIDNIGHT).toInstant(ZoneOffset.UTC); + if (lastTime != null && lastTime.isBefore(entryTime)) { + throw new DatesOutOfSequenceException(); + } + entry.setChangeLogTime(entryTime); + lastTime = entryTime; + state = ParsingState.NAME; + } catch (DateTimeParseException e) { + throw new InvalidChangelogDateException(e); + } + break; + case NAME: + String name = restOfLine.substring(timeLen).trim(); + if (name.length() > 0) { + entry.setUserMakingChange(name); + } + state = ParsingState.TEXT; + break; + case TEXT: + index++; + if (index < lines.length) { + line = lines[index]; + if (line.startsWith("*")) { + if (descr.length() > 1) { + entry.setDescription(descr.toString().substring(0, descr.length() - 1)); + } + if (entry.isComplete()) { + result.add(entry); + entry = new ChangelogEntry(); + descr = new StringBuilder(); + state = ParsingState.NEW; + } else { + throw new IncompleteChangelogEntryException(); + } + } else { + descr.append(line).append('\n'); + } + } else { + break lineloop; + } + } + } + if (descr.length() > 1) { + entry.setDescription(descr.toString().substring(0, descr.length() - 1)); + } + if (entry.isComplete()) { + result.add(entry); + } else { + throw new IncompleteChangelogEntryException(); + } + return result; + } + + /** + * @param stream stream read from the Changelog file + * @return a List of ChangeLogEntry objects + * @throws IOException if the input stream cannot be read + * @throws ChangelogParseException if any of the rules of a Changelog is + * violated by the input + */ + public List parse(InputStream stream) throws IOException, ChangelogParseException { + String line; + List lines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + while ((line = reader.readLine()) != null) { + if (!line.startsWith("#")) { + lines.add(line); + } + } + } + return parse(lines.toArray(new String[0])); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/changelog/ParsingState.java b/rpm-core/src/main/java/org/xbib/rpm/changelog/ParsingState.java new file mode 100644 index 0000000..01b0a95 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/changelog/ParsingState.java @@ -0,0 +1,7 @@ +package org.xbib.rpm.changelog; + +/** + */ +public enum ParsingState { + NEW, TIME, NAME, TEXT +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/changelog/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/changelog/package-info.java new file mode 100644 index 0000000..66fe430 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/changelog/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM changelog entries. + */ +package org.xbib.rpm.changelog; diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/ChangelogParseException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/ChangelogParseException.java new file mode 100644 index 0000000..21315ba --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/ChangelogParseException.java @@ -0,0 +1,21 @@ +package org.xbib.rpm.exception; + +/** + * Exceptions thrown by the ChangeLogParser. + */ +public abstract class ChangelogParseException extends RpmException { + + private static final long serialVersionUID = 3874782829969316647L; + + public ChangelogParseException() { + super(); + } + + public ChangelogParseException(Exception e) { + super(e); + } + + public ChangelogParseException(String message) { + super(message); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/DatesOutOfSequenceException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/DatesOutOfSequenceException.java new file mode 100644 index 0000000..be73f21 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/DatesOutOfSequenceException.java @@ -0,0 +1,15 @@ +package org.xbib.rpm.exception; + +/** + * This exception is thrown when Changelog entries are not in descending order by date. + */ + +public class DatesOutOfSequenceException extends ChangelogParseException { + + private static final long serialVersionUID = -4917148052703360535L; + + public DatesOutOfSequenceException() { + super(); + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/IncompleteChangelogEntryException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/IncompleteChangelogEntryException.java new file mode 100644 index 0000000..f001f4e --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/IncompleteChangelogEntryException.java @@ -0,0 +1,14 @@ +package org.xbib.rpm.exception; + +/** + * This exception is thrown when parsing of the changelog file results in an incomplete ChangeLogEntry. + */ +public class IncompleteChangelogEntryException extends ChangelogParseException { + + private static final long serialVersionUID = -2868781181942971960L; + + public IncompleteChangelogEntryException() { + super(); + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidChangelogDateException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidChangelogDateException.java new file mode 100644 index 0000000..b966d79 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidChangelogDateException.java @@ -0,0 +1,18 @@ +package org.xbib.rpm.exception; + +/** + * This exception is thrown when the date portion of a change log can not be parsed. + */ +public class InvalidChangelogDateException extends ChangelogParseException { + + private static final long serialVersionUID = 2684845962721950707L; + + public InvalidChangelogDateException(Exception e) { + super(e); + } + + public InvalidChangelogDateException(String message) { + super(message); + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidDirectiveException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidDirectiveException.java new file mode 100644 index 0000000..de6c84d --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidDirectiveException.java @@ -0,0 +1,18 @@ +package org.xbib.rpm.exception; + +/** + * Invalid RPM directive exception. + */ +public class InvalidDirectiveException extends RpmException { + + private static final long serialVersionUID = -7183149225882563869L; + + /** + * Constructor. + * + * @param invalidDirective Invalid directive name + */ + public InvalidDirectiveException(String invalidDirective) { + super(String.format("RPM directive '%s' invalid", invalidDirective)); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidPathException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidPathException.java new file mode 100644 index 0000000..5b96da2 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/InvalidPathException.java @@ -0,0 +1,18 @@ +package org.xbib.rpm.exception; + +/** + * Invalid path. + */ +public class InvalidPathException extends RpmException { + + private static final long serialVersionUID = 8635626016855635561L; + + /** + * + * @param invalidPath Invalid path + * @param cause Exception cause + */ + public InvalidPathException(String invalidPath, Throwable cause) { + super(String.format("Path %s is invalid, causing exception", invalidPath), cause); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/NoInitialAsteriskException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/NoInitialAsteriskException.java new file mode 100644 index 0000000..71e814c --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/NoInitialAsteriskException.java @@ -0,0 +1,13 @@ +package org.xbib.rpm.exception; + +/** + * This exception is when a change log entry does not begin with an asterisk. + */ +public class NoInitialAsteriskException extends ChangelogParseException { + + private static final long serialVersionUID = -3822813453286191743L; + + public NoInitialAsteriskException() { + super(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/PathOutsideBuildPathException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/PathOutsideBuildPathException.java new file mode 100644 index 0000000..ddd4ab3 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/PathOutsideBuildPathException.java @@ -0,0 +1,18 @@ +package org.xbib.rpm.exception; + +/** + * Scan path for discovering files is not within a child path of the base build path. + */ +public class PathOutsideBuildPathException extends RpmException { + + private static final long serialVersionUID = 7028847909078304806L; + + /** + * + * @param scanPath Scan path + * @param buildPath Build path + */ + public PathOutsideBuildPathException(String scanPath, String buildPath) { + super(String.format("Scan path %s outside of build directory %s", scanPath, buildPath)); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/RpmException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/RpmException.java new file mode 100644 index 0000000..47c0726 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/RpmException.java @@ -0,0 +1,26 @@ +package org.xbib.rpm.exception; + +/** + * Exceptions thrown by the RPM builder or reader. + */ +public class RpmException extends Exception { + + private static final long serialVersionUID = -7205164781605944414L; + + public RpmException() { + super(); + } + + public RpmException(Throwable t) { + super(t); + } + + public RpmException(String message) { + super(message); + } + + public RpmException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/SigningKeyNotFoundException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/SigningKeyNotFoundException.java new file mode 100644 index 0000000..3347e2f --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/SigningKeyNotFoundException.java @@ -0,0 +1,18 @@ +package org.xbib.rpm.exception; + +/** + * Signing key file not found exception. + */ +public class SigningKeyNotFoundException extends RpmException { + + private static final long serialVersionUID = -8186767059791996113L; + + /** + * Constructor. + * + * @param signingKey Signing key + */ + public SigningKeyNotFoundException(String signingKey) { + super(String.format("Signing key %s could not be found", signingKey)); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/UnknownArchitectureException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/UnknownArchitectureException.java new file mode 100644 index 0000000..8d313e6 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/UnknownArchitectureException.java @@ -0,0 +1,28 @@ +package org.xbib.rpm.exception; + +/** + * Unknown architecture exception. + */ +public class UnknownArchitectureException extends RpmException { + + private static final long serialVersionUID = -7624086302466294147L; + + /** + * Constructor. + * + * @param unknownArchitecture Unknown architecture name + */ + public UnknownArchitectureException(String unknownArchitecture) { + super(String.format("Unknown architecture '%s'", unknownArchitecture)); + } + + /** + * Constructor. + * + * @param unknownArchitecture Unknown architecture name + * @param cause Exception cause + */ + public UnknownArchitectureException(String unknownArchitecture, Throwable cause) { + super(String.format("Unknown architecture '%s'", unknownArchitecture), cause); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/UnknownOperatingSystemException.java b/rpm-core/src/main/java/org/xbib/rpm/exception/UnknownOperatingSystemException.java new file mode 100644 index 0000000..a7c4d7f --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/UnknownOperatingSystemException.java @@ -0,0 +1,28 @@ +package org.xbib.rpm.exception; + +/** + * Unknown operating system exception. + */ +public class UnknownOperatingSystemException extends RpmException { + + private static final long serialVersionUID = 1939077648201714453L; + + /** + * Constructor. + * + * @param unknownOperatingSystem Unknown operating system + */ + public UnknownOperatingSystemException(String unknownOperatingSystem) { + super(String.format("Unknown operating system '%s'", unknownOperatingSystem)); + } + + /** + * Constructor. + * + * @param unknownOperatingSystem Unknown operating system + * @param cause Exception cause + */ + public UnknownOperatingSystemException(String unknownOperatingSystem, Throwable cause) { + super(String.format("Unknown operating system '%s'", unknownOperatingSystem), cause); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/exception/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/exception/package-info.java new file mode 100644 index 0000000..585d8e0 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/exception/package-info.java @@ -0,0 +1,4 @@ +/** + * Exceptions for RPM archives. + */ +package org.xbib.rpm.exception; diff --git a/rpm-core/src/main/java/org/xbib/rpm/format/Flags.java b/rpm-core/src/main/java/org/xbib/rpm/format/Flags.java new file mode 100644 index 0000000..65fcae1 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/format/Flags.java @@ -0,0 +1,39 @@ +package org.xbib.rpm.format; + +/** + * + */ +public interface Flags { + + int LESS = 0x02; + + int GREATER = 0x04; + + int EQUAL = 0x08; + + int SCRIPT_POSTTRANS = 0x20; + + int PREREQ = 0x40; + + int SCRIPT_PRETRANS = 0x80; + + int INTERP = 0x100; + + int SCRIPT_PRE = 0x200; + + int SCRIPT_POST = 0x400; + + int SCRIPT_PREUN = 0x800; + + int SCRIPT_POSTUN = 0x1000; + + int SCRIPT_TRIGGERIN = 0x10000; + + int SCRIPT_TRIGGERUN = 0x20000; + + int SCRIPT_TRIGGERPOSTUN = 0x40000; + + int SCRIPT_TRIGGERPREIN = 0x2000000; + + int RPMLIB = (0x1000000 | PREREQ); +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/format/Format.java b/rpm-core/src/main/java/org/xbib/rpm/format/Format.java new file mode 100644 index 0000000..c4ac71b --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/format/Format.java @@ -0,0 +1,49 @@ +package org.xbib.rpm.format; + +import org.xbib.rpm.header.Header; +import org.xbib.rpm.lead.Lead; +import org.xbib.rpm.signature.SignatureHeader; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; + +/** + * + */ +public class Format { + + private Lead lead = new Lead(); + + private Header header = new Header(); + + private SignatureHeader signatureHeader = new SignatureHeader(); + + public Lead getLead() { + return lead; + } + + public Header getHeader() { + return header; + } + + public SignatureHeader getSignatureHeader() { + return signatureHeader; + } + + public void read(ReadableByteChannel channel) throws IOException { + lead.read(channel); + signatureHeader.read(channel); + header.read(channel); + } + + public void write(FileChannel channel) throws IOException { + lead.write(channel); + signatureHeader.write(channel); + header.write(channel); + } + + public String toString() { + return lead.toString() + signatureHeader + header; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/format/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/format/package-info.java new file mode 100644 index 0000000..6b2c35f --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/format/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM format. + */ +package org.xbib.rpm.format; diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/AbstractHeader.java b/rpm-core/src/main/java/org/xbib/rpm/header/AbstractHeader.java new file mode 100644 index 0000000..f9f152d --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/AbstractHeader.java @@ -0,0 +1,380 @@ +package org.xbib.rpm.header; + +import org.xbib.rpm.header.entry.BinSpecEntry; +import org.xbib.rpm.header.entry.I18NStringSpecEntry; +import org.xbib.rpm.header.entry.Int16SpecEntry; +import org.xbib.rpm.header.entry.Int32SpecEntry; +import org.xbib.rpm.header.entry.Int64SpecEntry; +import org.xbib.rpm.header.entry.Int8SpecEntry; +import org.xbib.rpm.header.entry.SpecEntry; +import org.xbib.rpm.header.entry.StringArraySpecEntry; +import org.xbib.rpm.header.entry.StringSpecEntry; +import org.xbib.rpm.io.ChannelWrapper; +import org.xbib.rpm.lead.Lead; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * + */ +public abstract class AbstractHeader { + + private static final int HEADER_SIZE = 16; + + private static final int ENTRY_SIZE = 16; + + private static final int MAGIC_WORD = 0x8EADE801; + + protected final Map tags = new HashMap<>(); + + private final Map> entries = new TreeMap<>(); + + private final Map, Integer> pending = new LinkedHashMap<>(); + + private int startPos; + + private int endPos; + + protected abstract boolean pad(); + + /** + * Reads the entire header contents for this channel and returns the number of entries found. + * + * @param in the ReadableByteChannel to read + * @return the number read + * @throws IOException there was an IO error + */ + public int read(ReadableByteChannel in) throws IOException { + ByteBuffer header = ChannelWrapper.fill(in, HEADER_SIZE); + int magic = header.getInt(); + if (magic == 0) { + header.compact(); + ChannelWrapper.fill(in, header); + magic = header.getInt(); + } + if (MAGIC_WORD != magic) { + throw new IOException("check expected " + Integer.toHexString(0xff & MAGIC_WORD) + + ", found " + Integer.toHexString(0xff & magic)); + } + header.getInt(); + ByteBuffer index = ChannelWrapper.fill(in, header.getInt() * ENTRY_SIZE); + int total = header.getInt(); + int pad = pad() ? ((total + 7) & ~7) - total : 0; + ByteBuffer data = ChannelWrapper.fill(in, total + pad); + int count = 0; + while (index.remaining() >= ENTRY_SIZE) { + readEntry(index.getInt(), index.getInt(), index.getInt(), index.getInt(), data); + count++; + } + return count; + } + + /** + * Writes this header section to the provided file at the current position and returns the + * required padding. The caller is responsible for adding the padding immediately after + * this data. + * + * @param out the WritableByteChannel to output to + * @return the number written + * @throws IOException there was an IO error + */ + public int write(WritableByteChannel out) throws IOException { + ByteBuffer header = getHeader(); + ByteBuffer index = getIndex(); + ByteBuffer data = getData(index); + data.flip(); + int pad = pad() ? ((data.remaining() + 7) & ~7) - data.remaining() : 0; + header.putInt(data.remaining()); + ChannelWrapper.empty(out, (ByteBuffer) header.flip()); + ChannelWrapper.empty(out, (ByteBuffer) index.flip()); + ChannelWrapper.empty(out, data); + return pad; + } + + public Map> getEntries() { + return entries; + } + + public int count() { + return entries.size(); + } + + public SpecEntry getEntry(EntryType entryType) { + return getEntry(entryType.getCode()); + } + + public SpecEntry getEntry(int code) { + return entries.get(code); + } + + @SuppressWarnings("unchecked") + public void createEntry(EntryType entryType, CharSequence value) { + SpecEntry entry = (SpecEntry) makeEntry(entryType, 1); + entry.setValues(new String[]{value.toString()}); + } + + @SuppressWarnings("unchecked") + public void createEntry(EntryType entryType, Integer value) { + SpecEntry entry = (SpecEntry) makeEntry(entryType, 1); + entry.setValues(new Integer[]{value}); + } + + public void writePending(SeekableByteChannel channel) { + for (Map.Entry, Integer> entry : pending.entrySet()) { + try { + ByteBuffer data = ByteBuffer.allocate(entry.getKey().size()); + entry.getKey().write(data); + channel.position(Lead.LEAD_SIZE + HEADER_SIZE + count() * ENTRY_SIZE + entry.getValue()); + ChannelWrapper.empty(channel, (ByteBuffer) data.flip()); + } catch (Exception e) { + throw new RuntimeException("Error writing pending entry '" + entry.getKey().getEntryType() + "'.", e); + } + } + } + + /** + * This is the main entry point through which entries are created from the builder code for + * types other than String. + * + * @param type parameter + * @param entryType the Tag identifying the type of header this is bound for + * @param values the values to be stored in the entry. + * @throws ClassCastException - if the type of values is not compatible with the type + * required by tag + */ + @SuppressWarnings("unchecked") + public void createEntry(EntryType entryType, T values) { + SpecEntry entry = (SpecEntry) makeEntry(entryType, values.getClass().isArray() ? Array.getLength(values) : 1); + if (entryType instanceof HeaderTag) { + Class cl = ((HeaderTag) entryType).getTypeClass(); + if (cl.isAssignableFrom(values.getClass())) { + entry.setValues(values); + } else { + throw new ClassCastException("cl = " + cl.getName() + " values = " + values.getClass().getName()); + } + } else { + entry.setValues(values); + } + } + + /** + * This is the main entry point through which entries are created or appended to + * from the builder or from places like the ChangelogHandler. This is useful for + * header types which may have multiple components of each tag, as changelogs do. + * + * @param type parameter + * @param entryType the Tag identifying the type of header this is bound for + * @param values the values to be stored in or appended to the entry. + * @throws ClassCastException - if the type of values is not compatible with the + * type required by tag + */ + @SuppressWarnings({"unchecked"}) + public void addOrAppendEntry(EntryType entryType, T values) { + SpecEntry entry = (SpecEntry) addOrAppendEntry(entryType, + values.getClass().isArray() ? Array.getLength(values) : 1); + T existingValues = entry.getValues(); + if (existingValues == null) { + entry.setValues(values); + } else { + int oldSize = java.lang.reflect.Array.getLength(existingValues); + int newSize = values.getClass().isArray() ? Array.getLength(values) : 1; + Class elementType = existingValues.getClass().getComponentType(); + T newValues = (T) Array.newInstance(elementType, oldSize + newSize); + System.arraycopy(existingValues, 0, newValues, 0, oldSize); + System.arraycopy(values, 0, newValues, oldSize, newSize); + entry.setValues(newValues); + } + } + + /** + * Adds a pending entry to this header. This entry will have the correctly sized buffer allocated, but + * will not be written until the caller writes a value and then invokes {@link #writePending} on this + * object. + * + * @param entryType the tag + * @param count the count + * @return the entry added + */ + public SpecEntry addEntry(EntryType entryType, int count) { + return makeEntry(entryType, count); + } + + public int getEndPos() { + return endPos; + } + + public void setEndPos(int endPos) { + this.endPos = endPos; + } + + public int getStartPos() { + return startPos; + } + + public void setStartPos(int startPos) { + this.startPos = startPos; + } + + /** + * Memory maps the portion of the destination file that will contain the header structure + * header and advances the file channels position. The resulting buffer will be prefilled with + * the necesssary magic data and the correct index count, but will require an integer value to + * be written with the total data section size once data writing is complete. + * This method must be invoked before mapping the index or data sections. + * + * @return a buffer containing the header + * @throws IOException there was an IO error + */ + protected ByteBuffer getHeader() throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE); + buffer.putInt(MAGIC_WORD); + buffer.putInt(0); + buffer.putInt(count()); + return buffer; + } + + /** + * Memory maps the portion of the destination file that will contain the index structure + * header and advances the file channels position. The resulting buffer will be ready for + * writing of the entry indexes. + * This method must be invoked before mapping the data section, but after mapping the header. + * + * @return a buffer containing the header + * @throws IOException there was an IO error + */ + private ByteBuffer getIndex() throws IOException { + return ByteBuffer.allocate(count() * ENTRY_SIZE); + } + + /** + * Writes the data section of the file, starting at the current position which must be immediately + * after the header section. Each entry writes its corresponding index into the provided index buffer + * and then writes its data to the file channel. + * + * @param index ByteBuffer of the index + * @return the total number of bytes written to the data section of the file. + * @throws IOException there was an IO error + */ + private ByteBuffer getData(final ByteBuffer index) throws IOException { + int offset = 0; + List buffers = new ArrayList<>(); + Iterator i = entries.keySet().iterator(); + index.position(16); + SpecEntry first = entries.get(i.next()); + SpecEntry entry = null; + try { + while (i.hasNext()) { + entry = entries.get(i.next()); + offset = writeData(buffers, index, entry, offset); + } + index.position(0); + offset = writeData(buffers, index, first, offset); + index.position(index.limit()); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Error while writing '" + entry + "'.", e); + } + ByteBuffer data = ByteBuffer.allocate(offset); + for (ByteBuffer buffer : buffers) { + data.put(buffer); + } + return data; + } + + private int writeData(Collection buffers, ByteBuffer index, SpecEntry entry, int offset) { + int shift = entry.getOffset(offset) - offset; + if (shift > 0) { + buffers.add(ByteBuffer.allocate(shift)); + } + offset += shift; + int size = entry.size(); + ByteBuffer buffer = ByteBuffer.allocate(size); + entry.index(index, offset); + if (entry.ready()) { + entry.write(buffer); + buffer.flip(); + } else { + pending.put(entry, offset); + } + buffers.add(buffer); + return offset + size; + } + + private void readEntry(int tag, int type, int offset, int count, ByteBuffer data) { + SpecEntry entry = makeEntry(type); + entry.setEntryType(Tags.from(tag)); + entry.setCount(count); + entries.put(tag, entry); + ByteBuffer buffer = data.duplicate(); + buffer.position(offset); + entry.read(buffer); + entry.setOffset(offset); + } + + private SpecEntry makeEntry(EntryType entryType, int count) { + SpecEntry entry = makeEntry(entryType.getType()); + entry.setEntryType(entryType); + entry.setCount(count); + entries.put(entryType.getCode(), entry); + return entry; + } + + private SpecEntry addOrAppendEntry(EntryType entryType, int count) { + SpecEntry entry = entries.get(entryType.getCode()); + if (entry == null) { + entry = makeEntry(entryType.getType()); + entry.setEntryType(entryType); + entry.setCount(count); + } else { + entry.incCount(count); + } + entries.put(entryType.getCode(), entry); + return entry; + } + + private SpecEntry makeEntry(int type) { + switch (type) { + case EntryType.INT8_ENTRY: + return new Int8SpecEntry(); + case EntryType.INT16_ENTRY: + return new Int16SpecEntry(); + case EntryType.INT32_ENTRY: + return new Int32SpecEntry(); + case EntryType.INT64_ENTRY: + return new Int64SpecEntry(); + case EntryType.STRING_ENTRY: + return new StringSpecEntry(); + case EntryType.BIN_ENTRY: + return new BinSpecEntry(); + case EntryType.STRING_ARRAY_ENTRY: + return new StringArraySpecEntry(); + case EntryType.I18NSTRING_ENTRY: + return new I18NStringSpecEntry(); + default: + throw new IllegalStateException("Unknown entry type '" + type + "'."); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Header(").append(getClass()).append(")").append("\n"); + int count = 0; + for (Map.Entry> entry : entries.entrySet()) { + builder.append(count++).append(": ").append(entry.getValue()).append("\n"); + } + return builder.toString(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/EntryType.java b/rpm-core/src/main/java/org/xbib/rpm/header/EntryType.java new file mode 100644 index 0000000..c8662d1 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/EntryType.java @@ -0,0 +1,29 @@ +package org.xbib.rpm.header; + +/** + * + */ +public interface EntryType { + + int INT8_ENTRY = 2; + + int INT16_ENTRY = 3; + + int INT32_ENTRY = 4; + + int INT64_ENTRY = 5; + + int STRING_ENTRY = 6; + + int BIN_ENTRY = 7; + + int STRING_ARRAY_ENTRY = 8; + + int I18NSTRING_ENTRY = 9; + + int getCode(); + + int getType(); + + String getName(); +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/Header.java b/rpm-core/src/main/java/org/xbib/rpm/header/Header.java new file mode 100644 index 0000000..f823f7b --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/Header.java @@ -0,0 +1,18 @@ +package org.xbib.rpm.header; + +/** + * + */ +public class Header extends AbstractHeader { + + public Header() { + for (HeaderTag tag : HeaderTag.values()) { + tags.put(tag.getCode(), tag); + } + } + + @Override + protected boolean pad() { + return false; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/HeaderTag.java b/rpm-core/src/main/java/org/xbib/rpm/header/HeaderTag.java new file mode 100644 index 0000000..c77bf56 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/HeaderTag.java @@ -0,0 +1,141 @@ +package org.xbib.rpm.header; + +/** + * + */ +public enum HeaderTag implements EntryType { + + NAME(1000, STRING_ENTRY, String.class, "name"), + VERSION(1001, STRING_ENTRY, String.class, "version"), + RELEASE(1002, STRING_ENTRY, String.class, "release"), + EPOCH(1003, INT32_ENTRY, Integer[].class, "epoch"), + SUMMARY(1004, I18NSTRING_ENTRY, String.class, "summary"), + DESCRIPTION(1005, I18NSTRING_ENTRY, String.class, "description"), + BUILDTIME(1006, INT32_ENTRY, Integer[].class, "buildtime"), + BUILDHOST(1007, STRING_ENTRY, String.class, "buildhost"), + SIZE(1009, INT32_ENTRY, Integer[].class, "size"), + DISTRIBUTION(1010, STRING_ENTRY, String.class, "distribution"), + VENDOR(1011, STRING_ENTRY, String.class, "vendor"), + LICENSE(1014, STRING_ENTRY, String.class, "license"), + PACKAGER(1015, STRING_ENTRY, String.class, "packager"), + GROUP(1016, I18NSTRING_ENTRY, String.class, "group"), + CHANGELOG(1017, STRING_ARRAY_ENTRY, String[].class, "changelog"), + URL(1020, STRING_ENTRY, String.class, "url"), + OS(1021, STRING_ENTRY, String.class, "os"), + ARCH(1022, STRING_ENTRY, String.class, "arch"), + SOURCERPM(1044, STRING_ENTRY, String.class, "sourcerpm"), + FILEVERIFYFLAGS(1045, INT32_ENTRY, Integer[].class, "fileverifyflags"), + ARCHIVESIZE(1046, INT32_ENTRY, Integer[].class, "archivesize"), + RPMVERSION(1064, STRING_ENTRY, String.class, "rpmversion"), + CHANGELOGTIME(1080, INT32_ENTRY, Integer[].class, "changelogtime"), + CHANGELOGNAME(1081, STRING_ARRAY_ENTRY, String[].class, "changelogname"), + CHANGELOGTEXT(1082, STRING_ARRAY_ENTRY, String[].class, "changelogtext"), + COOKIE(1094, STRING_ENTRY, String.class, "cookie"), + OPTFLAGS(1122, STRING_ENTRY, String.class, "optflags"), + PAYLOADFORMAT(1124, STRING_ENTRY, String.class, "payloadformat"), + PAYLOADCOMPRESSOR(1125, STRING_ENTRY, String.class, "payloadcompressor"), + PAYLOADFLAGS(1126, STRING_ENTRY, String.class, "payloadflags"), + RHNPLATFORM(1131, STRING_ENTRY, String.class, "rhnplatform"), + PLATFORM(1132, STRING_ENTRY, String.class, "platform"), + FILECOLORS(1140, INT32_ENTRY, Integer[].class, "filecolors"), + FILECLASS(1141, INT32_ENTRY, Integer[].class, "fileclass"), + CLASSDICT(1142, STRING_ARRAY_ENTRY, String[].class, "classdict"), + FILEDEPENDSX(1143, INT32_ENTRY, Integer[].class, "filedependsx"), + FILEDEPENDSN(1144, INT32_ENTRY, Integer[].class, "filedependsn"), + DEPENDSDICT(1145, INT32_ENTRY, Integer[].class, "dependsdict"), + SOURCEPKGID(1146, BIN_ENTRY, Object.class, "sourcepkgid"), + FILECONTEXTS(1147, STRING_ARRAY_ENTRY, String[].class, "filecontexts"), + + HEADERIMMUTABLE(63, BIN_ENTRY, Object.class, "headerimmutable"), + HEADERI18NTABLE(100, STRING_ARRAY_ENTRY, String[].class, "headeri18ntable"), + + PREINSCRIPT(1023, STRING_ENTRY, String.class, "prein"), + POSTINSCRIPT(1024, STRING_ENTRY, String.class, "postin"), + PREUNSCRIPT(1025, STRING_ENTRY, String.class, "preun"), + POSTUNSCRIPT(1026, STRING_ENTRY, String.class, "postun"), + PREINPROG(1085, STRING_ENTRY, String.class, "preinprog"), + POSTINPROG(1086, STRING_ENTRY, String.class, "postinprog"), + PREUNPROG(1087, STRING_ENTRY, String.class, "preunprog"), + POSTUNPROG(1088, STRING_ENTRY, String.class, "postunprog"), + + PRETRANSSCRIPT(1151, STRING_ENTRY, String.class, "pretrans"), + POSTTRANSSCRIPT(1152, STRING_ENTRY, String.class, "posttrans"), + PRETRANSPROG(1153, STRING_ENTRY, String.class, "pretransprog"), + POSTTRANSPROG(1154, STRING_ENTRY, String.class, "pretransprog"), + + TRIGGERSCRIPTS(1065, STRING_ARRAY_ENTRY, String[].class, "triggerscripts"), + TRIGGERNAME(1066, STRING_ARRAY_ENTRY, String[].class, "triggername"), + TRIGGERVERSION(1067, STRING_ARRAY_ENTRY, String[].class, "triggerversion"), + TRIGGERFLAGS(1068, INT32_ENTRY, Integer[].class, "triggerflags"), + TRIGGERINDEX(1069, INT32_ENTRY, Integer[].class, "triggerindex"), + TRIGGERSCRIPTPROG(1092, STRING_ARRAY_ENTRY, String[].class, "triggerscriptprog"), + + OLDFILENAMES(1027, STRING_ARRAY_ENTRY, String[].class, "oldfilenames"), + FILESIZES(1028, INT32_ENTRY, Integer[].class, "filesizes"), + FILEMODES(1030, INT16_ENTRY, short[].class, "filemodes"), + FILERDEVS(1033, INT16_ENTRY, short[].class, "filerdevs"), + FILEMTIMES(1034, INT32_ENTRY, Integer[].class, "filemtimes"), + FILEDIGESTS(1035, STRING_ARRAY_ENTRY, String[].class, "filedigests"), + FILELINKTOS(1036, STRING_ARRAY_ENTRY, String[].class, "filelinktos"), + FILEFLAGS(1037, INT32_ENTRY, Integer[].class, "fileflags"), + FILEUSERNAME(1039, STRING_ARRAY_ENTRY, String[].class, "fileusername"), + FILEGROUPNAME(1040, STRING_ARRAY_ENTRY, String[].class, "filegroupname"), + FILEDEVICES(1095, INT32_ENTRY, Integer[].class, "filedevices"), + FILEINODES(1096, INT32_ENTRY, Integer[].class, "fileinodes"), + FILELANGS(1097, STRING_ARRAY_ENTRY, String[].class, "filelangs"), + PREFIXES(1098, STRING_ARRAY_ENTRY, String[].class, "prefixes"), + DIRINDEXES(1116, INT32_ENTRY, Integer[].class, "dirindexes"), + BASENAMES(1117, STRING_ARRAY_ENTRY, String[].class, "basenames"), + DIRNAMES(1118, STRING_ARRAY_ENTRY, String[].class, "dirnames"), + + PROVIDENAME(1047, STRING_ARRAY_ENTRY, String[].class, "providename"), + REQUIREFLAGS(1048, INT32_ENTRY, Integer[].class, "requireflags"), + REQUIRENAME(1049, STRING_ARRAY_ENTRY, String[].class, "requirename"), + REQUIREVERSION(1050, STRING_ARRAY_ENTRY, String[].class, "requireversion"), + CONFLICTFLAGS(1053, INT32_ENTRY, Integer[].class, "conflictflags"), + CONFLICTNAME(1054, STRING_ARRAY_ENTRY, String[].class, "conflictname"), + CONFLICTVERSION(1055, STRING_ARRAY_ENTRY, String[].class, "conflictversion"), + OBSOLETENAME(1090, STRING_ARRAY_ENTRY, String[].class, "obsoletename"), + PROVIDEFLAGS(1112, INT32_ENTRY, Integer[].class, "provideflags"), + PROVIDEVERSION(1113, STRING_ARRAY_ENTRY, String[].class, "provideversion"), + OBSOLETEFLAGS(1114, INT32_ENTRY, Integer[].class, "obsoleteflags"), + OBSOLETEVERSION(1115, STRING_ARRAY_ENTRY, String[].class, "obsoleteversion"), + + FILEDIGESTALGOS(1177, INT32_ENTRY, Integer[].class, "filedigestalgos"), + + /* private header tags */ + + SIGNATURES(0x0000003e, INT32_ENTRY, Integer[].class, "_signatures"), + IMMUTABLE(0x0000003f, INT32_ENTRY, Integer[].class, "_immutable"); + + private int code; + + private int type; + + private Class typeClass; + + private String name; + + HeaderTag(int code, int type, Class typeClass, String name) { + this.code = code; + this.type = type; + this.typeClass = typeClass; + this.name = name; + } + + public int getCode() { + return code; + } + + public int getType() { + return type; + } + + public String getName() { + return name; + } + + public Class getTypeClass() { + return typeClass; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/Tags.java b/rpm-core/src/main/java/org/xbib/rpm/header/Tags.java new file mode 100644 index 0000000..a380345 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/Tags.java @@ -0,0 +1,31 @@ +package org.xbib.rpm.header; + +import org.xbib.rpm.signature.SignatureTag; + +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class Tags { + + private static final Map tags = new HashMap<>(); + + static { + for (HeaderTag tag : HeaderTag.values()) { + tags.put(tag.getCode(), tag); + } + for (SignatureTag tag : SignatureTag.values()) { + tags.put(tag.getCode(), tag); + } + } + + public static EntryType from(int code) { + return tags.get(code); + } + + public static Map tags() { + return tags; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/AbstractSpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/AbstractSpecEntry.java new file mode 100644 index 0000000..3e72bea --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/AbstractSpecEntry.java @@ -0,0 +1,107 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; +import org.xbib.rpm.header.Tags; + +import java.nio.ByteBuffer; + +/** + * @param the type parameter + */ +public abstract class AbstractSpecEntry implements SpecEntry { + + protected int size; + + protected EntryType entryType; + + protected int count; + + protected int offset; + + protected T values; + + public EntryType getEntryType() { + return entryType; + } + + public void setEntryType(EntryType entryType) { + this.entryType = entryType; + } + + public void setSize(int size) { + this.size = size; + } + + public void setCount(int count) { + this.count = count; + } + + public void incCount(int count) { + this.count += count; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public T getValues() { + return values; + } + + public void setValues(T values) { + this.values = values; + } + + public int getOffset(int offset) { + return offset; + } + + public boolean ready() { + return values != null; + } + + public abstract int getType(); + + public void typeCheck() { + } + + /** + * Returns the size this entry will need in the provided data buffer to write + * it's contents, corrected for any trailing zeros to fill to a boundary. + */ + public abstract int size(); + + /** + * Reads this entries value from the provided buffer using the set count. + */ + public abstract void read(ByteBuffer buffer); + + /** + * Writes this entries index to the index buffer and its values to the output + * channel provided. + */ + public abstract void write(ByteBuffer data); + + /** + * Writes the index entry into the provided buffer at the current position. + */ + public void index(ByteBuffer index, int position) { + index.putInt(entryType.getCode()).putInt(getType()).putInt(position).putInt(count); + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + if (Tags.tags().containsKey(entryType.getCode())) { + builder.append(Tags.tags().get(entryType.getCode()).getName()); + } else { + builder.append(super.toString()); + } + builder.append("[tag=").append(entryType) + .append(",type=").append(getType()) + .append(",count=").append(count) + .append(",size=").append(size()) + .append(",offset=").append(offset) + .append("]"); + return builder.toString(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/BinSpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/BinSpecEntry.java new file mode 100644 index 0000000..4edab95 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/BinSpecEntry.java @@ -0,0 +1,33 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +import java.nio.ByteBuffer; + +/** + * + */ +public class BinSpecEntry extends AbstractSpecEntry { + + @Override + public int getType() { + return EntryType.BIN_ENTRY; + } + + @Override + public int size() { + return count; + } + + @Override + public void read(ByteBuffer buffer) { + byte[] values = new byte[count]; + buffer.get(values); + setValues(values); + } + + @Override + public void write(ByteBuffer data) { + data.put(values); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/I18NStringSpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/I18NStringSpecEntry.java new file mode 100644 index 0000000..34f104c --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/I18NStringSpecEntry.java @@ -0,0 +1,14 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +/** + * + */ +public class I18NStringSpecEntry extends StringSpecEntry { + + @Override + public int getType() { + return EntryType.I18NSTRING_ENTRY; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int16SpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int16SpecEntry.java new file mode 100644 index 0000000..a3b9293 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int16SpecEntry.java @@ -0,0 +1,52 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +import java.nio.ByteBuffer; + +/** + * + */ +public class Int16SpecEntry extends AbstractSpecEntry { + + @Override + public int getOffset(int offset) { + return (offset + 1) & ~1; + } + + @Override + public int getType() { + return EntryType.INT16_ENTRY; + } + + @Override + public int size() { + return count * (Short.SIZE / 8); + } + + @Override + public void read(ByteBuffer buffer) { + short[] values = new short[count]; + for (int x = 0; x < count; x++) { + values[x] = buffer.getShort(); + } + setValues(values); + } + + @Override + public void write(ByteBuffer data) { + for (short s : values) { + data.putShort(s); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + builder.append("\n\t"); + for (short s : values) { + builder.append(s & 0xFFFF).append(", "); + } + return builder.toString(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int32SpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int32SpecEntry.java new file mode 100644 index 0000000..f970eef --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int32SpecEntry.java @@ -0,0 +1,58 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +import java.nio.ByteBuffer; + +/** + * + */ +public class Int32SpecEntry extends AbstractSpecEntry { + + @Override + public int getOffset(int offset) { + return (offset + 3) & ~3; + } + + @Override + public int getType() { + return EntryType.INT32_ENTRY; + } + + @Override + public int size() { + return count * (Integer.SIZE / 8); + } + + @Override + public void read(ByteBuffer buffer) { + Integer[] values = new Integer[count]; + for (int x = 0; x < count; x++) { + values[x] = buffer.getInt(); + } + setValues(values); + } + + @Override + public void write(ByteBuffer buffer) { + for (int x = 0; x < count; x++) { + Integer i = values[x]; + buffer.putInt(i); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + builder.append("\n\t"); + if (values != null) { + for (Integer i : values) { + builder.append(i); + if (values.length > 1) { + builder.append(", "); + } + } + } + return builder.toString(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int64SpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int64SpecEntry.java new file mode 100644 index 0000000..f38f8d6 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int64SpecEntry.java @@ -0,0 +1,52 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +import java.nio.ByteBuffer; + +/** + * + */ +public class Int64SpecEntry extends AbstractSpecEntry { + + @Override + public int getOffset(int offset) { + return (offset + 7) & ~7; + } + + @Override + public int getType() { + return EntryType.INT64_ENTRY; + } + + @Override + public int size() { + return count * (Long.SIZE / 8); + } + + @Override + public void read(ByteBuffer buffer) { + long[] values = new long[count]; + for (int x = 0; x < count; x++) { + values[x] = buffer.getLong(); + } + setValues(values); + } + + @Override + public void write(ByteBuffer data) { + for (long l : values) { + data.putLong(l); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + builder.append("\n\t"); + for (long l : values) { + builder.append(l).append(", "); + } + return builder.toString(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int8SpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int8SpecEntry.java new file mode 100644 index 0000000..9ab952e --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/Int8SpecEntry.java @@ -0,0 +1,47 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +import java.nio.ByteBuffer; + +/** + * + */ +public class Int8SpecEntry extends AbstractSpecEntry { + + @Override + public int getType() { + return EntryType.INT8_ENTRY; + } + + @Override + public int size() { + return count; + } + + @Override + public void read(ByteBuffer buffer) { + byte[] values = new byte[count]; + for (int x = 0; x < count; x++) { + values[x] = buffer.get(); + } + setValues(values); + } + + @Override + public void write(ByteBuffer data) { + for (byte b : values) { + data.put(b); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + builder.append("\n\t"); + for (byte b : values) { + builder.append(b).append(", "); + } + return builder.toString(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/SpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/SpecEntry.java new file mode 100644 index 0000000..2c87ddf --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/SpecEntry.java @@ -0,0 +1,41 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +import java.nio.ByteBuffer; + +/** + * @param the type parameter + */ +public interface SpecEntry { + + void setSize(int size); + + void setCount(int count); + + void incCount(int count); + + void setOffset(int offset); + + T getValues(); + + void setValues(T values); + + EntryType getEntryType(); + + void setEntryType(EntryType entryType); + + int getType(); + + int getOffset(int offset); + + int size(); + + boolean ready(); + + void read(ByteBuffer buffer); + + void write(ByteBuffer buffer); + + void index(ByteBuffer buffer, int position); +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/StringArraySpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/StringArraySpecEntry.java new file mode 100644 index 0000000..ffe8427 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/StringArraySpecEntry.java @@ -0,0 +1,14 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +/** + * + */ +public class StringArraySpecEntry extends StringSpecEntry { + + @Override + public int getType() { + return EntryType.STRING_ARRAY_ENTRY; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/StringSpecEntry.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/StringSpecEntry.java new file mode 100644 index 0000000..2c6c245 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/StringSpecEntry.java @@ -0,0 +1,63 @@ +package org.xbib.rpm.header.entry; + +import org.xbib.rpm.header.EntryType; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * + */ +public class StringSpecEntry extends AbstractSpecEntry { + + @Override + public int getType() { + return EntryType.STRING_ENTRY; + } + + @Override + public int size() { + if (size != 0) { + return size; + } + for (String s : values) { + size += StandardCharsets.UTF_8.encode(s).remaining() + 1; + } + return size; + } + + @Override + public void read(ByteBuffer buffer) { + String[] values = new String[count]; + for (int x = 0; x < count; x++) { + int length = 0; + while (buffer.get(buffer.position() + length) != 0) { + length++; + } + ByteBuffer slice = buffer.slice(); + buffer.position(buffer.position() + length + 1); + slice.limit(length); + values[x] = StandardCharsets.UTF_8.decode(slice).toString(); + } + setValues(values); + } + + @Override + public void write(ByteBuffer data) { + for (String s : values) { + data.put(StandardCharsets.UTF_8.encode(s)).put((byte) 0); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + if (values != null) { + for (String s : values) { + builder.append("\n\t"); + builder.append(s); + } + } + return builder.toString(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/entry/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/header/entry/package-info.java new file mode 100644 index 0000000..2b0170f --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/entry/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM header entries. + */ +package org.xbib.rpm.header.entry; diff --git a/rpm-core/src/main/java/org/xbib/rpm/header/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/header/package-info.java new file mode 100644 index 0000000..55d978b --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/header/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM headers. + */ +package org.xbib.rpm.header; diff --git a/rpm-core/src/main/java/org/xbib/rpm/io/ChannelWrapper.java b/rpm-core/src/main/java/org/xbib/rpm/io/ChannelWrapper.java new file mode 100644 index 0000000..c9c0d1f --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/io/ChannelWrapper.java @@ -0,0 +1,166 @@ +package org.xbib.rpm.io; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.HashMap; +import java.util.Map; + +/** + * Wraps an IO channel so that bytes may be observed during transmission. Wrappers around IO channels are + * used for a variety of purposes, including counting byte output for use in generating headers, calculating + * a signature across output bytes, and digesting output bytes using a one-way secure hash. + */ +public abstract class ChannelWrapper { + + final Map, Consumer> consumers = new HashMap<>(); + + public Key start(WritableByteChannel output) { + Key object = new Key<>(); + consumers.put(object, new Consumer() { + int count; + + @Override + public void consume(ByteBuffer buffer) { + try { + count += output.write(buffer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Integer finish() { + return count; + } + }); + return object; + } + + /** + * Initializes a byte counter on this channel. + * + * @return reference to the new key added to the consumers + */ + public Key start() { + Key object = new Key<>(); + consumers.put(object, new Consumer() { + int count; + + @Override + public void consume(ByteBuffer buffer) { + count += buffer.remaining(); + } + + @Override + public Integer finish() { + return count; + } + }); + return object; + } + + /** + * Add a new consumer to this channel. + * + * @param consumer the channel consumer + * @return reference to the new key added to the consumers + */ + public Key start(Consumer consumer) { + Key object = new Key<>(); + consumers.put(object, consumer); + return object; + } + + @SuppressWarnings("unchecked") + public T finish(Key object) { + return (T) consumers.remove(object).finish(); + } + + public void close() throws IOException { + if (!consumers.isEmpty()) { + throw new IOException("there are '" + consumers.size() + "' unfinished consumers"); + } + } + + /** + * Creates a new buffer and fills it with bytes from the + * provided channel. The amount of data to read is specified + * in the arguments. + * + * @param in the channel to read from + * @param size the number of bytes to read into a new buffer + * @return a new buffer containing the bytes read + * @throws IOException if an IO error occurs + */ + public static ByteBuffer fill(ReadableByteChannel in, int size) throws IOException { + return fill(in, ByteBuffer.allocate(size)); + } + + /** + * Fills the provided buffer it with bytes from the + * provided channel. The amount of data to read is + * dependant on the available space in the provided + * buffer. + * + * @param in the channel to read from + * @param buffer the buffer to read into + * @return the provided buffer + * @throws IOException if an IO error occurs + */ + public static ByteBuffer fill(ReadableByteChannel in, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + if (in.read(buffer) == -1) { + throw new BufferUnderflowException(); + } + } + buffer.flip(); + return buffer; + } + + /** + * Empties the contents of the given buffer into the + * writable channel provided. The buffer will be copied + * to the channel in it's entirety. + * + * @param out the channel to write to + * @param buffer the buffer to write out to the channel + * @throws IOException if an IO error occurs + */ + public static void empty(WritableByteChannel out, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + out.write(buffer); + } + } + + /** + * Interface describing an object that consumes data from a byte buffer. + * + * @param the consumer type + */ + public interface Consumer { + + /** + * Consume some input from the given buffer. + * + * @param buffer the buffer to consume + */ + void consume(ByteBuffer buffer); + + /** + * Complete and optionally return a value to the holder of the key. + * + * @return reference to the object + */ + T finish(); + } + + /** + * @param key type for stream processing + */ + public static class Key { + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/io/ReadableChannelWrapper.java b/rpm-core/src/main/java/org/xbib/rpm/io/ReadableChannelWrapper.java new file mode 100644 index 0000000..7b53dab --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/io/ReadableChannelWrapper.java @@ -0,0 +1,55 @@ +package org.xbib.rpm.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +/** + * Wrapper for observing data read from a NIO channel. This wrapper is + * used for operations that must be notified of incoming IO data. + */ +public class ReadableChannelWrapper extends ChannelWrapper implements ReadableByteChannel { + + protected ReadableByteChannel channel; + + public ReadableChannelWrapper(ReadableByteChannel channel) { + this.channel = channel; + } + + /** + * Reads data from the channel and passes it to the consumer. This method + * does not mutate the acutal data in the provided buffer, but makes it's + * own copy to pass to the consumer. + * + * @param buffer the buffer to read into + * @return the number of bytes read from the underlying channel + * @throws IOException if an IO error occurrs + */ + public int read(final ByteBuffer buffer) throws IOException { + final int read = channel.read(buffer); + for (Consumer consumer : consumers.values()) { + consumer.consume((ByteBuffer) buffer.duplicate().flip()); + } + return read; + } + + /** + * Close the underlying read channel and complete any operations in the + * consumer. + * + * @throws IOException if an IO error occurrs + */ + public void close() throws IOException { + channel.close(); + super.close(); + } + + /** + * Boolean flag indicating whether the channel is open or closed. + * + * @return true if the channel is open, false if not + */ + public boolean isOpen() { + return channel.isOpen(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/io/WritableChannelWrapper.java b/rpm-core/src/main/java/org/xbib/rpm/io/WritableChannelWrapper.java new file mode 100644 index 0000000..5951a40 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/io/WritableChannelWrapper.java @@ -0,0 +1,54 @@ +package org.xbib.rpm.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; + +/** + * Wrapper around a writable channel that allows + * for observing written data. + */ +public class WritableChannelWrapper extends ChannelWrapper implements WritableByteChannel { + + protected WritableByteChannel channel; + + public WritableChannelWrapper(final WritableByteChannel channel) { + this.channel = channel; + } + + /** + * Writes data to the wrapped channel, while passing an + * exact copy to the registered consumers. + * + * @param buffer the buffer to write to the wrapped channel + * @return the number of bytes written + * @throws IOException if an IO error occurs + */ + public int write(final ByteBuffer buffer) throws IOException { + for (Consumer consumer : consumers.values()) { + consumer.consume(buffer.duplicate()); + } + return channel.write(buffer); + } + + /** + * Closes the underlying channel and completes + * any outstanding operations in the consumers. + * + * @throws IOException if an IO error occurs + */ + public void close() throws IOException { + channel.close(); + super.close(); + } + + /** + * Flag indicating whether the underlying channel + * is open. + * + * @return true if it is open, false otherwise + */ + public boolean isOpen() { + return channel.isOpen(); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/io/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/io/package-info.java new file mode 100644 index 0000000..ecf9910 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/io/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM archive input/output. + */ +package org.xbib.rpm.io; diff --git a/rpm-core/src/main/java/org/xbib/rpm/lead/Architecture.java b/rpm-core/src/main/java/org/xbib/rpm/lead/Architecture.java new file mode 100644 index 0000000..b2ee91a --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/lead/Architecture.java @@ -0,0 +1,29 @@ +package org.xbib.rpm.lead; + +/** + * + */ +public enum Architecture { + + NOARCH, + I386, + ALPHA, + SPARC, + MIPS, + PPC, + M68K, + IP, + RS6000, + IA64, + SPARC64, + MIPSEL, + ARM, + MK68KMINT, + S390, + S390X, + PPC64, + SH, + XTENSA, + X86_64, + PPC64LE +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/lead/Lead.java b/rpm-core/src/main/java/org/xbib/rpm/lead/Lead.java new file mode 100644 index 0000000..4c5f1ae --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/lead/Lead.java @@ -0,0 +1,143 @@ +package org.xbib.rpm.lead; + +import org.xbib.rpm.io.ChannelWrapper; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; + +/** + * + */ +public class Lead { + + public static final int LEAD_SIZE = 96; + + private static final int MAGIC_WORD = 0xEDABEEDB; + + private byte major; + + private byte minor; + + private PackageType type; + + private Architecture arch; + + private String name; + + private Os os; + + private short sigtype; + + public Lead() { + this.major = 4; + this.minor = 0; + this.type = PackageType.BINARY; + this.arch = Architecture.NOARCH; + this.os = Os.LINUX; + this.name = "default"; + this.sigtype = 5; /* 5 = header-style signature */ + } + + public CharSequence getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Architecture getArch() { + return arch; + } + + public void setArch(Architecture arch) { + this.arch = arch; + } + + public void setMajor(byte major) { + this.major = major; + } + + public void setMinor(byte minor) { + this.minor = minor; + } + + public void setType(PackageType type) { + this.type = type; + } + + public void setOs(Os os) { + this.os = os; + } + + public void setSigtype(short sigtype) { + this.sigtype = sigtype; + } + + public void read(ReadableByteChannel channel) throws IOException { + ByteBuffer lead = ChannelWrapper.fill(channel, LEAD_SIZE); + int magic = lead.getInt(); + if (MAGIC_WORD != magic) { + throw new IOException("check expected " + + Integer.toHexString(0xff & MAGIC_WORD) + ", found " + Integer.toHexString(0xff & magic)); + } + major = lead.get(); + minor = lead.get(); + type = PackageType.values()[lead.getShort()]; + final short tmp = lead.getShort(); + if (tmp < Architecture.values().length) { + arch = Architecture.values()[tmp]; + } + ByteBuffer data = ByteBuffer.allocate(66); + lead.get(data.array()); + StringBuilder builder = new StringBuilder(); + byte b; + while ((data.hasRemaining() && (b = data.get()) != 0)) { + builder.append((char) b); + } + name = builder.toString(); + short o = lead.getShort(); + if (o != 0xFF) { + os = Os.values()[o]; + } else { + os = Os.UNKNOWN; + } + sigtype = lead.getShort(); + if (lead.remaining() != 16) { + throw new IllegalStateException("Expected 16 remaining, found '" + lead.remaining() + "'."); + } + } + + public void write(WritableByteChannel channel) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(LEAD_SIZE); + buffer.putInt(MAGIC_WORD); + buffer.put(major); + buffer.put(minor); + buffer.putShort((short) type.ordinal()); + buffer.putShort((short) arch.ordinal()); + byte[] data = new byte[66]; + byte[] encoded = name.getBytes(StandardCharsets.UTF_8); + System.arraycopy(encoded, 0, data, 0, data.length > encoded.length ? encoded.length : data.length); + buffer.put(data); + buffer.putShort((short) (os.ordinal() != 0 ? os.ordinal() : 0xFF)); + buffer.putShort(sigtype); + buffer.position(buffer.position() + 16); + buffer.flip(); + if (buffer.remaining() != LEAD_SIZE) { + throw new IllegalStateException("Invalid lead size generated with '" + buffer.remaining() + "' bytes."); + } + ChannelWrapper.empty(channel, buffer); + } + + public String toString() { + return "Version: " + major + "." + minor + "\n" + + "Type: " + type + "\n" + + "Arch: " + arch + "\n" + + "Name: " + name + "\n" + + "OS: " + os + "\n" + + "Sig type: " + sigtype + "\n"; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/lead/Os.java b/rpm-core/src/main/java/org/xbib/rpm/lead/Os.java new file mode 100644 index 0000000..5c0a377 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/lead/Os.java @@ -0,0 +1,31 @@ +package org.xbib.rpm.lead; + +/** + * + */ +public enum Os { + + UNKNOWN, + LINUX, + IRIX, + SOLARIS, + SUNOS, + AMIGAOS, + AIX, + HPUX10, + OSF1, + FREEBSD, + SCO, + IRIX64, + NEXTSTEP, + BSDI, + MACHTEN, + CYGWINNT, + CYGWIN95, + UNIXSV, + MINT, + OS390, + VMESA, + LINUX390, + MACOSX +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/lead/PackageType.java b/rpm-core/src/main/java/org/xbib/rpm/lead/PackageType.java new file mode 100644 index 0000000..2b69b49 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/lead/PackageType.java @@ -0,0 +1,10 @@ +package org.xbib.rpm.lead; + +/** + * + */ +public enum PackageType { + + BINARY, + SOURCE +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/lead/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/lead/package-info.java new file mode 100644 index 0000000..615f884 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/lead/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM lead. + */ +package org.xbib.rpm.lead; diff --git a/rpm-core/src/main/java/org/xbib/rpm/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/package-info.java new file mode 100644 index 0000000..264fb63 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM building and reading. + */ +package org.xbib.rpm; diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/CompressionType.java b/rpm-core/src/main/java/org/xbib/rpm/payload/CompressionType.java new file mode 100644 index 0000000..2763d69 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/CompressionType.java @@ -0,0 +1,9 @@ +package org.xbib.rpm.payload; + +/** + * + */ +public enum CompressionType { + + NONE, GZIP, BZIP2, XZ +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/Contents.java b/rpm-core/src/main/java/org/xbib/rpm/payload/Contents.java new file mode 100644 index 0000000..2132025 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/Contents.java @@ -0,0 +1,957 @@ +package org.xbib.rpm.payload; + +import org.xbib.rpm.exception.RpmException; +import org.xbib.rpm.io.ChannelWrapper; +import org.xbib.rpm.io.ChannelWrapper.Key; +import org.xbib.rpm.io.ReadableChannelWrapper; +import org.xbib.rpm.security.HashAlgo; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The contents of an RPM archive. These entries define the files and links that + * the RPM archive contains as well as headers those files require. + * Note that the RPM format requires that files in the archive be naturally ordered. + */ +public class Contents { + + private static final Logger logger = Logger.getLogger(Contents.class.getName()); + + private static final Set BUILTIN = new LinkedHashSet<>(); + + private static final String hex = "0123456789abcdef"; + + static { + BUILTIN.add("/"); + BUILTIN.add("/bin"); + BUILTIN.add("/dev"); + BUILTIN.add("/etc"); + BUILTIN.add("/etc/bash_completion.d"); + BUILTIN.add("/etc/cron.d"); + BUILTIN.add("/etc/cron.daily"); + BUILTIN.add("/etc/cron.hourly"); + BUILTIN.add("/etc/cron.monthly"); + BUILTIN.add("/etc/cron.weekly"); + BUILTIN.add("/etc/default"); + BUILTIN.add("/etc/init.d"); + BUILTIN.add("/etc/logrotate.d"); + BUILTIN.add("/lib"); + BUILTIN.add("/usr"); + BUILTIN.add("/usr/bin"); + BUILTIN.add("/usr/lib"); + BUILTIN.add("/usr/lib64"); + BUILTIN.add("/usr/local"); + BUILTIN.add("/usr/local/bin"); + BUILTIN.add("/usr/local/lib"); + BUILTIN.add("/usr/sbin"); + BUILTIN.add("/usr/share"); + BUILTIN.add("/usr/share/applications"); + BUILTIN.add("/root"); + BUILTIN.add("/sbin"); + BUILTIN.add("/opt"); + BUILTIN.add("/srv"); + BUILTIN.add("/tmp"); + BUILTIN.add("/var"); + BUILTIN.add("/var/cache"); + BUILTIN.add("/var/lib"); + BUILTIN.add("/var/log"); + BUILTIN.add("/var/run"); + BUILTIN.add("/var/spool"); + /* + DOC_DIRS.add("/usr/doc"); + DOC_DIRS.add("/usr/man"); + DOC_DIRS.add("/usr/X11R6/man"); + DOC_DIRS.add("/usr/share/doc"); + DOC_DIRS.add("/usr/share/man"); + DOC_DIRS.add("/usr/share/info"); + */ + } + + private final Set headers = + new TreeSet<>(Comparator.comparing(CpioHeader::getName)); + + private final Set files = new LinkedHashSet<>(); + + private final Map sources = new LinkedHashMap<>(); + + private final Set builtins = new LinkedHashSet<>(); + + private int inode = 1; + + public Contents() { + builtins.addAll(BUILTIN); + } + + private static String hex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte aByte : bytes) { + sb.append(hex.charAt(((int) aByte & 0xf0) >> 4)).append(hex.charAt((int) aByte & 0x0f)); + } + return sb.toString(); + } + + /** + * Adds a directory entry to the archive with the default permissions of 644. + * + * @param path the destination path for the installed file. + * @param target the target string + */ + public void addLink(String path, String target) { + addLink(path, target, -1); + } + + /** + * Adds a directory entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param target the target string + * @param permissions the permissions flags. + */ + public void addLink(String path, String target, int permissions) { + addLink(path, target, permissions, null, null); + } + + /** + * Adds a directory entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param target the target string + * @param permissions the permissions flags. + * @param uname user owner for the given link + * @param gname group owner for the given link + */ + public void addLink(String path, String target, int permissions, String uname, String gname) { + if (files.contains(path)) { + return; + } + files.add(path); + logger.log(Level.FINE, "adding link ''{0}''.", path); + CpioHeader header = new CpioHeader(path); + header.setType(CpioHeader.SYMLINK); + header.setFileSize(target.length()); + header.setMtime(System.currentTimeMillis()); + header.setUname(getDefaultIfMissing(uname, CpioHeader.DEFAULT_USERNAME)); + header.setGname(getDefaultIfMissing(gname, CpioHeader.DEFAULT_GROUP)); + if (permissions != -1) { + header.setPermissions(permissions); + } + headers.add(header); + sources.put(header, target); + } + + private String getDefaultIfMissing(String value, String defaultValue) { + return value == null || value.isEmpty() ? defaultValue : value; + } + + /** + * Adds a directory entry to the archive with the default permissions of 644. + * + * @param path the destination path for the installed file. + */ + public void addDirectory(String path) { + addDirectory(path, -1); + } + + /** + * Adds a directory entry to the archive with the default permissions of 644. + * + * @param path the destination path for the installed file. + * @param directive directive indicating special handling for this directory. + */ + public void addDirectory(String path, EnumSet directive) { + addDirectory(path, -1, directive, null, null); + } + + /** + * Adds a directory entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param permissions the permissions flags. + */ + public void addDirectory(String path, int permissions) { + addDirectory(path, permissions, null, null, null); + } + + /** + * Adds a directory entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param permissions the permissions flags. + * @param directive directive indicating special handling for this directory. + * @param uname user owner for the given file + * @param gname group owner for the given file + */ + public void addDirectory(String path, int permissions, EnumSet directive, String uname, String gname) { + addDirectory(path, permissions, directive, uname, gname, true); + } + + /** + * Adds a directory entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param permissions the permissions flags. + * @param directive directive indicating special handling for this directory. + * @param uname user owner for the given file + * @param gname group owner for the given file + * @param addParents whether to add parent directories to the rpm + */ + public void addDirectory(String path, int permissions, EnumSet directive, String uname, + String gname, boolean addParents) { + if (files.contains(path)) { + return; + } + if (addParents) { + addParents(Paths.get(path), permissions, uname, gname); + } + files.add(path); + logger.log(Level.FINE, "adding directory ''{0}''.", path); + CpioHeader header = new CpioHeader(path); + header.setType(CpioHeader.DIR); + header.setInode(inode++); + if (uname == null) { + header.setUname(CpioHeader.DEFAULT_USERNAME); + } else if (0 == uname.length()) { + header.setUname(CpioHeader.DEFAULT_USERNAME); + } else { + header.setUname(uname); + } + if (gname == null) { + header.setGname(CpioHeader.DEFAULT_GROUP); + } else if (0 == gname.length()) { + header.setGname(CpioHeader.DEFAULT_GROUP); + } else { + header.setGname(gname); + } + header.setMtime(System.currentTimeMillis()); + if (-1 == permissions) { + header.setPermissions(CpioHeader.DEFAULT_DIRECTORY_PERMISSION); + } else { + header.setPermissions(permissions); + } + headers.add(header); + sources.put(header, ""); + if (directive != null) { + int flag = Directive.NONE.flag(); + for (Directive d : directive) { + flag = flag | d.flag(); + } + header.setFlags(flag); + } + } + + /** + * Adds a file entry to the archive with the default permissions of 644. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @throws IOException file wasn't found + */ + public void addFile(String path, Path source) throws IOException { + addFile(path, source, -1); + } + + /** + * Adds a file entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @param permissions the permissions flags. + * @throws IOException file wasn't found + */ + public void addFile(String path, Path source, int permissions) throws IOException { + addFile(path, source, permissions, null, null, null); + } + + /** + * Adds a file entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @param permissions the permissions flags. + * @param dirmode permission flags for parent directories, use -1 to leave as default. + * @throws IOException file wasn't found + */ + public void addFile(String path, Path source, int permissions, int dirmode) throws IOException { + addFile(path, source, permissions, null, null, null, dirmode); + } + + /** + * Adds a file entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @param permissions the permissions flags. + * @param directive directive indicating special handling for this file. + * @throws IOException file wasn't found + */ + public void addFile(String path, Path source, int permissions, EnumSet directive) + throws IOException { + addFile(path, source, permissions, directive, null, null); + } + + /** + * Adds a file entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @param permissions the permissions flags. + * @param directive directive indicating special handling for this file. + * @param uname user owner for the given file + * @param gname group owner for the given file + * @throws IOException file wasn't found + */ + public void addFile(String path, Path source, int permissions, EnumSet directive, String uname, + String gname) throws IOException { + addFile(path, source, permissions, directive, uname, gname, -1); + } + + /** + * Adds a file entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @param permissions the permissions flags. + * @param directives directives indicating special handling for this file. + * @param uname user owner for the given file + * @param gname group owner for the given file + * @param dirmode permission flags for parent directories, use -1 to leave as default. + * @throws IOException file wasn't found + */ + public void addFile(String path, Path source, int permissions, EnumSet directives, String uname, + String gname, int dirmode) throws IOException { + addFile(path, source, permissions, directives, uname, gname, dirmode, true); + } + + /** + * Adds a file entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @param permissions the permissions flags, use -1 to leave as default. + * @param directives directives indicating special handling for this file, use null to ignore. + * @param uname user owner for the given file, use null for default user. + * @param gname group owner for the given file, use null for default group. + * @param dirmode permission flags for parent directories, use -1 to leave as default. + * @param addParents whether to create parent directories for the file, defaults to true for other methods. + * @throws IOException file wasn't found + */ + public void addFile(String path, Path source, int permissions, EnumSet directives, String uname, + String gname, int dirmode, boolean addParents) throws IOException { + addFile(path, source, permissions, directives, uname, gname, dirmode, addParents, -1); + } + + /** + * Adds a file entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the local file to be included in the package. + * @param permissions the permissions flags, use -1 to leave as default. + * @param directives directives indicating special handling for this file, use null to ignore. + * @param uname user owner for the given file, use null for default user. + * @param gname group owner for the given file, use null for default group. + * @param dirmode permission flags for parent directories, use -1 to leave as default. + * @param addParents whether to create parent directories for the file, defaults to true for other methods. + * @param verifyFlags verify flags + * @throws java.io.FileNotFoundException file wasn't found + */ + public void addFile(String path, Path source, int permissions, EnumSet directives, String uname, + String gname, int dirmode, boolean addParents, int verifyFlags) throws IOException { + if (files.contains(path)) { + return; + } + if (addParents) { + addParents(Paths.get(path), dirmode, uname, gname); + } + files.add(path); + logger.log(Level.FINE, "adding file ''{0}''.", path); + CpioHeader header; + if (directives != null && directives.contains(Directive.GHOST)) { + header = new CpioHeader(path); + } else { + header = new CpioHeader(path, source); + } + header.setType(CpioHeader.FILE); + header.setInode(inode++); + if (uname == null) { + header.setUname(CpioHeader.DEFAULT_USERNAME); + } else if (0 == uname.length()) { + header.setUname(CpioHeader.DEFAULT_USERNAME); + } else { + header.setUname(uname); + } + if (gname == null) { + header.setGname(CpioHeader.DEFAULT_GROUP); + } else if (0 == gname.length()) { + header.setGname(CpioHeader.DEFAULT_GROUP); + } else { + header.setGname(gname); + } + if (-1 == permissions) { + header.setPermissions(CpioHeader.DEFAULT_FILE_PERMISSION); + } else { + header.setPermissions(permissions); + } + header.setVerifyFlags(verifyFlags); + headers.add(header); + sources.put(header, source); + if (directives != null) { + int flag = Directive.NONE.flag(); + for (Directive d : directives) { + flag = flag | d.flag(); + } + header.setFlags(flag); + } + } + + /** + * Adds a URL entry to the archive with the specified permissions. + * + * @param path the destination path for the installed file. + * @param source the URL with the data to be added + * @param permissions the permissions flags. + * @param directive directive indicating special handling for this file. + * @param uname user owner for the given file + * @param gname group owner for the given file + * @param dirmode permission flags for parent directories, use -1 to leave as default. + * @throws IOException file wasn't found + */ + public void addURL(String path, URL source, int permissions, EnumSet directive, String uname, + String gname, int dirmode) throws IOException { + if (files.contains(path)) { + return; + } + addParents(Paths.get(path), dirmode, uname, gname); + files.add(path); + logger.log(Level.FINE, "adding file ''{0}''.", path); + CpioHeader header = new CpioHeader(path, source); + header.setType(CpioHeader.FILE); + header.setInode(inode++); + if (uname != null) { + header.setUname(uname); + } + if (gname != null) { + header.setGname(gname); + } + if (permissions != -1) { + header.setPermissions(permissions); + } + headers.add(header); + sources.put(header, source); + if (directive != null) { + int flag = Directive.NONE.flag(); + for (Directive d : directive) { + flag = flag | d.flag(); + } + header.setFlags(flag); + } + } + + /** + * Adds entries for parent directories of this file, so that they may be cleaned up when + * removing the package. + * + * @param path the file to add parent directories of + * @param permissions the permissions flags + * @param uname user owner for the given file + * @param gname group owner for the given file + */ + private void addParents(Path path, int permissions, String uname, String gname) { + List parents = new ArrayList<>(); + listParents(parents, path); + for (String parent : parents) { + addDirectory(parent, permissions, null, uname, gname); + } + } + + /** + * Add additional directory that is assumed to already exist on system where the RPM will be installed + * (e.g. /etc) and should not have an entry in the RPM. + *

+ * The builtin will only be added to this instance of Contents. + * + * @param directory the directory to add + */ + public void addLocalBuiltinDirectory(String directory) { + builtins.add(directory); + } + + /** + * Retrieve the size of this archive in number of files. This count includes both directory entries and + * soft links. + * + * @return the number of files in this archive + */ + public int size() { + return headers.size(); + } + + /** + * Retrieve the archive headers. The returned {@link Iterable} will iterate in the correct order for + * the archive. + * + * @return the headers + */ + public Iterable headers() { + return headers; + } + + /** + * Retrieves the content for this archive entry, which may be a {@link Path} if the entry is a regular file or + * a {@link CharSequence} containing the name of the target path if the entry is a link. This is the value to + * be written to the archive as the body of the entry. + * + * @param header the header to get the content from + * @return the content + */ + public Object getSource(CpioHeader header) { + return sources.get(header); + } + + /** + * Accumulated size of all files included in the archive. + * + * @return the size of all files included in the archive + */ + public int getTotalSize() { + int total = 0; + try { + for (Object object : sources.values()) { + if (object instanceof Path) { + total += Files.size((Path) object); + } else if (object instanceof URL) { + URLConnection urlConnection = null; + try { + urlConnection = ((URL) object).openConnection(); + total += urlConnection.getContentLength(); + } catch (IOException e) { + // + } finally { + if (urlConnection != null && urlConnection.getInputStream() != null) { + urlConnection.getInputStream().close(); + } + if (urlConnection != null && urlConnection.getOutputStream() != null) { + urlConnection.getOutputStream().close(); + } + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return total; + } + + /** + * Gets the dirnames headers values. + * + * @return the dirnames headers values + */ + public String[] getDirNames() { + Set set = new LinkedHashSet<>(); + for (CpioHeader header : headers) { + Path path = Paths.get(header.getName()).getParent(); + if (path == null) { + continue; + } + String parent = CpioHeader.normalizePath(path.toString()); + if (!parent.endsWith("/")) { + parent += "/"; + } + set.add(parent); + } + return set.toArray(new String[set.size()]); + } + + /** + * Gets the dirindexes headers values. + * + * @return the dirindexes + */ + public Integer[] getDirIndexes() { + List dirs = Arrays.asList(getDirNames()); + Integer[] array = new Integer[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + Path path = Paths.get(header.getName()).getParent(); + if (path == null) { + array[x++] = 0; // dummy value, required when including non-existent directories + continue; + } + String parent = CpioHeader.normalizePath(path.toString()); + if (!parent.endsWith("/")) { + parent += "/"; + } + array[x++] = dirs.indexOf(parent); + } + return array; + } + + /** + * Gets the basenames header values. + * + * @return the basename header values + */ + public String[] getBaseNames() { + String[] array = new String[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + Path path = Paths.get(header.getName()).getFileName(); + array[x++] = path != null ? CpioHeader.normalizePath(path.toString()) : ""; + } + return array; + } + + /** + * Gets the sizes header values. + * + * @return the sizes header values + */ + public Integer[] getSizes() { + Integer[] array = new Integer[headers.size()]; + int x = 0; + try { + for (CpioHeader header : headers) { + Object object = sources.get(header); + if (object instanceof Path) { + array[x] = (int) Files.size((Path) object); + } else if (object instanceof URL) { + array[x] = ((URL) object).openConnection().getContentLength(); + } else if (header.getType() == CpioHeader.DIR) { + array[x] = 4096; + } else if (header.getType() == CpioHeader.SYMLINK) { + array[x] = ((String) object).length(); + } + ++x; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return array; + } + + /** + * Gets the modes header values. + * + * @return the modes header values + */ + public short[] getModes() { + short[] array = new short[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = (short) header.getMode(); + } + return array; + } + + /** + * Gets the rdevs header values. + * + * @return the rdevs header values + */ + public short[] getRdevs() { + short[] array = new short[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = (short) ((header.getRdevMajor() << 8) + header.getRdevMinor()); + } + return array; + } + + /** + * Gets the mtimes header values. + * + * @return the mtimes header values + */ + public Integer[] getMtimes() { + Integer[] array = new Integer[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = header.getMtime(); + } + return array; + } + + /** + * Caclulates a digest hash for each file in the archive. + * + * @param hashAlgo the hash algo + * @return the digest hashes + * @throws RpmException if the algorithm isn't supported + * @throws IOException there was an IO error + */ + public String[] getDigests(HashAlgo hashAlgo) throws IOException, RpmException { + ByteBuffer buffer = ByteBuffer.allocate(4096); + String[] array = new String[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + Object object = sources.get(header); + String value = ""; + if (object instanceof Path) { + try (ReadableByteChannel readableByteChannel = FileChannel.open((Path) object)) { + try (ReadableChannelWrapper input = new ReadableChannelWrapper(readableByteChannel)) { + Key key = startDigest(input, MessageDigest.getInstance(hashAlgo.algo())); + while (input.read(buffer) != -1) { + buffer.rewind(); + } + value = hex(input.finish(key)); + } catch (NoSuchAlgorithmException e) { + throw new RpmException(e); + } + } + } else if (object instanceof URL) { + URL url = (URL) object; + try (InputStream inputStream = url.openStream()) { + try (ReadableByteChannel readableByteChannel = Channels.newChannel(inputStream)) { + try (ReadableChannelWrapper input = new ReadableChannelWrapper(readableByteChannel)) { + Key key = startDigest(input, MessageDigest.getInstance(hashAlgo.algo())); + while (input.read(buffer) != -1) { + buffer.rewind(); + } + value = hex(input.finish(key)); + } + } catch (NoSuchAlgorithmException e) { + throw new RpmException(e); + } + } + } + array[x++] = value; + } + return array; + } + + /** + * Start a digest. + * + * @param input the input channel + * @return reference to the new key added to the consumers + */ + private ChannelWrapper.Key startDigest(ReadableChannelWrapper input, MessageDigest digest) { + ChannelWrapper.Consumer consumer = new ChannelWrapper.Consumer() { + @Override + public void consume(ByteBuffer buffer) { + try { + digest.update(buffer); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] finish() { + try { + return digest.digest(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + return input.start(consumer); + } + + /** + * Get the linktos header. + * + * @return the linktos header + */ + public String[] getLinkTos() { + String[] array = new String[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + Object object = sources.get(header); + String value = ""; + if (object instanceof String) { + value = String.valueOf(object); + } + array[x++] = value; + } + return array; + } + + /** + * Gets the flags header values. + * + * @return the flags header values + */ + public Integer[] getFlags() { + Integer[] array = new Integer[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = header.getFlags(); + } + return array; + } + + /** + * Gets the users header values. + * + * @return the users header values + */ + public String[] getUsers() { + String[] array = new String[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = header.getUname() == null ? "root" : header.getUname(); + } + return array; + } + + /** + * Gets the groups header values. + * + * @return the groups header values + */ + public String[] getGroups() { + String[] array = new String[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = header.getGname() == null ? "root" : header.getGname(); + } + return array; + } + + /** + * Gets the colors header values. + * + * @return the colors header values + */ + public Integer[] getColors() { + return new Integer[headers.size()]; + } + + /** + * Gets the verifyflags header values. + * + * @return the verifyflags header values + */ + public Integer[] getVerifyFlags() { + Integer[] array = new Integer[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = header.getVerifyFlags(); + } + return array; + } + + /** + * Gets the classes header values. + * + * @return the classes header values + */ + public Integer[] getClasses() { + Integer[] array = new Integer[headers.size()]; + Arrays.fill(array, 1); + return array; + } + + /** + * Gets the devices header values. + * + * @return the devices header values + */ + public Integer[] getDevices() { + Integer[] array = new Integer[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = (header.getDevMajor() << 8) + header.getDevMinor(); + } + return array; + } + + /** + * Gets the inodes header values. + * + * @return the iNodes header values + */ + public Integer[] getInodes() { + Integer[] array = new Integer[headers.size()]; + int x = 0; + for (CpioHeader header : headers) { + array[x++] = header.getInode(); + } + return array; + } + + /** + * Gets the langs header values. + * + * @return the langs header values + */ + public String[] getLangs() { + String[] array = new String[headers.size()]; + Arrays.fill(array, ""); + return array; + } + + /** + * Gets the dependsx header values. + * + * @return the dependsx header values + */ + public Integer[] getDependsX() { + return new Integer[headers.size()]; + } + + /** + * Gets the dependsn header values. + * + * @return the dependsn header values + */ + public Integer[] getDependsN() { + return new Integer[headers.size()]; + } + + /** + * Gets the contexts header values. + * + * @return the contexts header values + */ + public String[] getContexts() { + String[] array = new String[headers.size()]; + Arrays.fill(array, "<>"); + return array; + } + + /** + * Generates a list of parent paths given a starting path. + * + * @param parents the list to add the parents to + * @param path the file to search for parents of + */ + protected void listParents(List parents, Path path) { + Path parent = path.getParent(); + if (parent == null) { + return; + } + String s = CpioHeader.normalizePath(parent.toString()); + if (builtins.contains(s)) { + return; + } + parents.add(s); + listParents(parents, parent); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/CpioHeader.java b/rpm-core/src/main/java/org/xbib/rpm/payload/CpioHeader.java new file mode 100644 index 0000000..78750bd --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/CpioHeader.java @@ -0,0 +1,373 @@ +package org.xbib.rpm.payload; + +import org.xbib.rpm.io.ChannelWrapper; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; + +/** + * This class provides a means to read file content from the compressed CPIO stream + * that is the body of an RPM distributable. Iterative calls to to read header will + * result in a header description being returned which includes a count of how many bytes + * to read from the channel for the file content. + */ +public class CpioHeader { + + public static final int DEFAULT_FILE_PERMISSION = 0644; + + public static final int DEFAULT_DIRECTORY_PERMISSION = 0755; + + public static final String DEFAULT_USERNAME = "root"; + + public static final String DEFAULT_GROUP = "root"; + + public static final int FIFO = 1; + + public static final int CDEV = 2; + + public static final int DIR = 4; + + public static final int BDEV = 6; + + public static final int FILE = 8; + + public static final int SYMLINK = 10; + + public static final int SOCKET = 12; + + private static final int CPIO_HEADER = 110; + + private static final String MAGIC = "070701"; + + private static final String TRAILER = "TRAILER!!!"; + + private Charset charset = StandardCharsets.UTF_8; + + private int inode; + + protected int type; + + protected int permissions = DEFAULT_FILE_PERMISSION; + + private int uid; + + private String uname; + + private int gid; + + private String gname; + + private int nlink = 1; + + private long mtime; + + private int filesize; + + private int devMinor = 1; + + private int devMajor = 9; + + private int rdevMinor; + + private int rdevMajor; + + private int checksum; + + protected String name; + + protected int flags; + + private int verifyFlags = -1; + + public CpioHeader() { + } + + public CpioHeader(String name) { + this.name = name; + } + + public CpioHeader(String name, URL url) { + try { + URLConnection connection = url.openConnection(); + mtime = connection.getLastModified(); + filesize = connection.getContentLength(); + this.name = normalizePath(name); + setType(FILE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public CpioHeader(String name, Path path) throws IOException { + mtime = Files.getLastModifiedTime(path).toMillis(); + filesize = (int) Files.size(path); + this.name = normalizePath(name); + setType(Files.isDirectory(path) ? DIR : FILE); + } + + public static String normalizePath(String path) { + return path.replace('\\', '/'); + } + + public static int difference(int start, int boundary) { + return ((boundary + 1) - (start & boundary)) & boundary; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public int getPermissions() { + return permissions; + } + + public void setPermissions(int permissions) { + this.permissions = permissions; + } + + public int getRdevMajor() { + return rdevMajor; + } + + public int getRdevMinor() { + return rdevMinor; + } + + public int getDevMajor() { + return devMajor; + } + + public int getDevMinor() { + return devMinor; + } + + public int getMtime() { + return (int) (mtime / 1000L); + } + + public void setMtime(long mtime) { + this.mtime = mtime; + } + + public int getInode() { + return inode; + } + + public void setInode(int inode) { + this.inode = inode; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getFlags() { + return flags; + } + + public void setFlags(int flags) { + this.flags = flags; + } + + public int getVerifyFlags() { + return verifyFlags; + } + + public void setVerifyFlags(int verifyFlags) { + this.verifyFlags = verifyFlags; + } + + public int getMode() { + return (type << 12) | (permissions & 07777); + } + + public String getUname() { + return this.uname; + } + + public void setUname(String uname) { + this.uname = uname; + } + + public String getGname() { + return this.gname; + } + + public void setGname(String gname) { + this.gname = gname; + } + + /** + * Test to see if this is the last header, and is therefore the end of the + * archive. Uses the CPIO magic trailer value to denote the last header of + * the stream. + * + * @return true if last, false if not + */ + public boolean isLast() { + return TRAILER.equals(name); + } + + public void setLast() { + name = TRAILER; + } + + public int getFileSize() { + return filesize; + } + + public void setFileSize(int filesize) { + this.filesize = filesize; + } + + protected ByteBuffer writeSix(CharSequence data) { + return charset.encode(pad(data, 6)); + } + + protected ByteBuffer writeEight(int data) { + return charset.encode(pad(Integer.toHexString(data), 8)); + } + + protected CharSequence readSix(CharBuffer buffer) { + return readChars(buffer, 6); + } + + protected int readEight(CharBuffer buffer) { + return Integer.parseInt(readChars(buffer, 8).toString(), 16); + } + + protected CharSequence readChars(CharBuffer buffer, int length) { + if (buffer.remaining() < length) { + throw new IllegalStateException("Buffer has '" + buffer.remaining() + "' bytes but '" + length + "' are needed."); + } + try { + return buffer.subSequence(0, length); + } finally { + buffer.position(buffer.position() + length); + } + } + + protected String pad(CharSequence sequence, int length) { + StringBuilder sequenceBuilder = new StringBuilder(sequence); + while (sequenceBuilder.length() < length) { + sequenceBuilder.insert(0, "0"); + } + sequence = sequenceBuilder.toString(); + return sequence.toString(); + } + + protected int skip(ReadableByteChannel channel, int total) throws IOException { + int skipped = difference(total, 3); + ChannelWrapper.fill(channel, skipped); + return skipped; + } + + public int skip(WritableByteChannel channel, int total) throws IOException { + int skipped = difference(total, 3); + ChannelWrapper.empty(channel, ByteBuffer.allocate(skipped)); + return skipped; + } + + public int read(ReadableByteChannel channel, int total) throws IOException { + total += skip(channel, total); + ByteBuffer descriptor = ChannelWrapper.fill(channel, CPIO_HEADER); + CharBuffer buffer = charset.decode(descriptor); + CharSequence magic = readSix(buffer); + if (!MAGIC.equals(magic.toString())) { + throw new IllegalStateException("Invalid magic number '" + magic + "' of length '" + magic.length() + "'."); + } + inode = readEight(buffer); + int mode = readEight(buffer); + permissions = mode & 07777; + type = mode >>> 12; + uid = readEight(buffer); + gid = readEight(buffer); + nlink = readEight(buffer); + mtime = 1000L * readEight(buffer); + filesize = readEight(buffer); + devMajor = readEight(buffer); + devMinor = readEight(buffer); + rdevMajor = readEight(buffer); + rdevMinor = readEight(buffer); + int namesize = readEight(buffer); + checksum = readEight(buffer); + total += CPIO_HEADER; + name = charset.decode(ChannelWrapper.fill(channel, namesize - 1)).toString(); + ChannelWrapper.fill(channel, 1); + total += namesize; + total += skip(channel, total); + return total; + } + + /** + * Write the content for the CPIO header, including the name immediately following. The name data is rounded + * to the nearest 2 byte boundary as CPIO requires by appending a null when needed. + * + * @param channel which channel to write on + * @param total current size of header? + * @return total written and skipped + * @throws IOException there was an IO error + */ + public int write(WritableByteChannel channel, int total) throws IOException { + ByteBuffer buffer = charset.encode(CharBuffer.wrap(name)); + int length = buffer.remaining() + 1; + ByteBuffer descriptor = ByteBuffer.allocate(CPIO_HEADER); + descriptor.put(writeSix(MAGIC)); + descriptor.put(writeEight(inode)); + descriptor.put(writeEight(getMode())); + descriptor.put(writeEight(uid)); + descriptor.put(writeEight(gid)); + descriptor.put(writeEight(nlink)); + descriptor.put(writeEight((int) (mtime / 1000))); + descriptor.put(writeEight(filesize)); + descriptor.put(writeEight(devMajor)); + descriptor.put(writeEight(devMinor)); + descriptor.put(writeEight(rdevMajor)); + descriptor.put(writeEight(rdevMinor)); + descriptor.put(writeEight(length)); + descriptor.put(writeEight(checksum)); + descriptor.flip(); + total += CPIO_HEADER + length; + ChannelWrapper.empty(channel, descriptor); + ChannelWrapper.empty(channel, buffer); + ChannelWrapper.empty(channel, ByteBuffer.allocate(1)); + return total + skip(channel, total); + } + + public String toString() { + return "Inode: " + inode + "\n" + + "Permission: " + Integer.toString(permissions, 8) + "\n" + + "Type: " + type + "\n" + + "UID: " + uid + "\n" + + "GID: " + gid + "\n" + + "UserName: " + uname + "\n" + + "GroupName: " + gname + "\n" + + "Nlink: " + nlink + "\n" + + "MTime: " + new Date(mtime) + "\n" + + "FileSize: " + filesize + "\n" + + "DevMinor: " + devMinor + "\n" + + "DevMajor: " + devMajor + "\n" + + "RDevMinor: " + rdevMinor + "\n" + + "RDevMajor: " + rdevMajor + "\n" + + "NameSize: " + (name.length() + 1) + "\n" + + "Name: " + name + "\n"; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/Directive.java b/rpm-core/src/main/java/org/xbib/rpm/payload/Directive.java new file mode 100644 index 0000000..e1352a8 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/Directive.java @@ -0,0 +1,104 @@ +package org.xbib.rpm.payload; + +import org.xbib.rpm.exception.InvalidDirectiveException; + +import java.util.EnumSet; +import java.util.List; + +/** + * Directive. + */ +public enum Directive { + + NONE(0), + + CONFIG(1), + + DOC(1 << 1), + + ICON(1 << 2), + + MISSINGOK(1 << 3), + + NOREPLACE(1 << 4), + + SPECFILE(1 << 5), + + GHOST(1 << 6), + + LICENSE(1 << 7), + + README(1 << 8), + + EXCLUDE(1 << 9), + + UNPATCHED(1 << 10), + + PUBKEY(1 << 11), + + POLICY(1 << 12); + + private int flag; + + Directive(final int flag) { + this.flag = flag; + } + + public int flag() { + return flag; + } + + /** + * Return a new directive set. + * + * @param directiveList directive list + * @return set of directives + * @throws InvalidDirectiveException if a directive is unknown + */ + public static EnumSet newDirective(List directiveList) throws InvalidDirectiveException { + EnumSet rpmDirective = EnumSet.of(Directive.NONE); + for (String directive : directiveList) { + switch (directive.toLowerCase()) { + case "config": + rpmDirective.add(Directive.CONFIG); + break; + case "doc": + rpmDirective.add(Directive.DOC); + break; + case "icon": + rpmDirective.add(Directive.ICON); + break; + case "missingok": + rpmDirective.add(Directive.MISSINGOK); + break; + case "noreplace": + rpmDirective.add(Directive.NOREPLACE); + break; + case "specfile": + rpmDirective.add(Directive.SPECFILE); + break; + case "ghost": + rpmDirective.add(Directive.GHOST); + break; + case "license": + rpmDirective.add(Directive.LICENSE); + break; + case "readme": + rpmDirective.add(Directive.README); + break; + case "unpatched": + rpmDirective.add(Directive.UNPATCHED); + break; + case "pubkey": + rpmDirective.add(Directive.PUBKEY); + break; + case "policy": + rpmDirective.add(Directive.POLICY); + break; + default: + throw new InvalidDirectiveException(directive); + } + } + return rpmDirective; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/EmptyDir.java b/rpm-core/src/main/java/org/xbib/rpm/payload/EmptyDir.java new file mode 100644 index 0000000..7b171a6 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/EmptyDir.java @@ -0,0 +1,61 @@ +package org.xbib.rpm.payload; + +/** + * + */ +public class EmptyDir { + + private static final int FILE_FLAG = 0100000; + + private static final int DIR_FLAG = 040000; + + private String path; + + private String username; + + private String group; + + private int filemode = -1; + + private int dirmode = -1; + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getGroup() { + return this.group; + } + + public void setGroup(String group) { + this.group = group; + } + + public int getFilemode() { + return this.filemode; + } + + public void setFilemode(String filemode) { + this.filemode = FILE_FLAG | Integer.parseInt(filemode, 8); + } + + public int getDirmode() { + return this.dirmode; + } + + public void setDirmode(String dirmode) { + this.dirmode = DIR_FLAG | Integer.parseInt(dirmode, 8); + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/Ghost.java b/rpm-core/src/main/java/org/xbib/rpm/payload/Ghost.java new file mode 100644 index 0000000..a1b3e13 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/Ghost.java @@ -0,0 +1,80 @@ +package org.xbib.rpm.payload; + +import java.util.EnumSet; + +/** + * Object describing a "ghost" file to be added to the rpm archive without the file needing to exist beforehand. + */ +public class Ghost { + + private static final int FILE_FLAG = 0100000; + + private static final int DIR_FLAG = 040000; + + private String path; + + private String username; + + private String group; + + private int filemode = -1; + + private int dirmode = -1; + + private EnumSet directives = EnumSet.of(Directive.GHOST); + + public Ghost() { + } + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getGroup() { + return this.group; + } + + public void setGroup(String group) { + this.group = group; + } + + public int getFilemode() { + return this.filemode; + } + + public void setFilemode(String filemode) { + this.filemode = FILE_FLAG | Integer.parseInt(filemode, 8); + } + + public int getDirmode() { + return this.dirmode; + } + + public void setDirmode(String dirmode) { + this.dirmode = DIR_FLAG | Integer.parseInt(dirmode, 8); + } + + public EnumSet getDirectives() { + return directives; + } + + public void setConfig(boolean config) { + if (config) { + directives.add(Directive.GHOST); + } else { + directives.remove(Directive.GHOST); + } + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/Link.java b/rpm-core/src/main/java/org/xbib/rpm/payload/Link.java new file mode 100644 index 0000000..4c5ac76 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/Link.java @@ -0,0 +1,37 @@ +package org.xbib.rpm.payload; + +/** + * Object describing a symbolic link to be generated on the target machine during installation of the RPM archive. + */ +public class Link { + + private String path; + + private String target; + + private int permissions = -1; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + + public int getPermissions() { + return permissions; + } + + public void setPermissions(int permissions) { + this.permissions = permissions; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/payload/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/payload/package-info.java new file mode 100644 index 0000000..84eeccc --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/payload/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM payload. + */ +package org.xbib.rpm.payload; diff --git a/rpm-core/src/main/java/org/xbib/rpm/security/HashAlgo.java b/rpm-core/src/main/java/org/xbib/rpm/security/HashAlgo.java new file mode 100644 index 0000000..45c9047 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/security/HashAlgo.java @@ -0,0 +1,39 @@ +package org.xbib.rpm.security; + + +/** + * Enumeration of hash algorithms as of RFC 4880. + * + * See also {@link org.bouncycastle.bcpg.HashAlgorithmTags} + */ +public enum HashAlgo { + + MD5("MD5", 1), + SHA1("SHA", 2), + RIPEMD160("RIPE-MD160", 3), + DOUBLESHA("Double-SHA", 4), + MD2("MD2", 5), + TIGER192("Tiger-192", 6), + HAVAL_5_160("Haval-5-160", 7), + SHA256("SHA-256", 8), + SHA384("SHA-384", 9), + SHA512("SHA-512", 10), + SHA224("SHA-224", 11); + + String algo; + + int num; + + HashAlgo(String algo, int num) { + this.algo = algo; + this.num = num; + } + + public String algo() { + return algo; + } + + public int num() { + return num; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/security/KeyDumper.java b/rpm-core/src/main/java/org/xbib/rpm/security/KeyDumper.java new file mode 100644 index 0000000..25bbac2 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/security/KeyDumper.java @@ -0,0 +1,51 @@ +package org.xbib.rpm.security; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Iterator; + +/** + * + */ +public class KeyDumper { + + public void asciiArmor(Long keyID, InputStream publicKeyRingStream, + OutputStream armoredOutputStream) throws PGPException, IOException { + PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection( + PGPUtil.getDecoderStream(publicKeyRingStream), new JcaKeyFingerprintCalculator()); + PGPPublicKey publicKey = findMatchingPublicKey(pgpPub, keyID); + ArmoredOutputStream armored = new ArmoredOutputStream(armoredOutputStream); + publicKey.encode(armored); + armored.close(); + } + + private PGPPublicKey findMatchingPublicKey(PGPPublicKeyRingCollection keyRings, Long privateKeyId) { + Iterator iter = keyRings.getKeyRings(); + while (iter.hasNext()) { + PGPPublicKeyRing keyRing = iter.next(); + @SuppressWarnings("unchecked") + Iterator keyIter = keyRing.getPublicKeys(); + while (keyIter.hasNext()) { + PGPPublicKey key = keyIter.next(); + if (key.isEncryptionKey() && isMatchingKeyId(key, privateKeyId)) { + return key; + } + } + } + throw new IllegalArgumentException("can't find signing key in key rings"); + } + + private boolean isMatchingKeyId(PGPPublicKey key, Long privateKeyId) { + return privateKeyId == null || Long.toHexString(key.getKeyID()).endsWith(Long.toHexString(privateKeyId)); + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/security/KeyGenerator.java b/rpm-core/src/main/java/org/xbib/rpm/security/KeyGenerator.java new file mode 100644 index 0000000..b79234a --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/security/KeyGenerator.java @@ -0,0 +1,130 @@ +package org.xbib.rpm.security; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.crypto.generators.RSAKeyPairGenerator; +import org.bouncycastle.crypto.params.RSAKeyGenerationParameters; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPKeyRingGenerator; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Date; + +/** + * + */ +public class KeyGenerator { + + private PGPPublicKeyRing pgpPublicKeyRing; + + private PGPSecretKeyRing pgpSecretKeyRing; + + public void generate(String id, String password, + OutputStream publicKeyRingStream, + OutputStream secretKeyRingStream) throws PGPException, IOException { + char[] pass = password.toCharArray(); + PGPKeyRingGenerator pgpKeyRingGenerator = generateKeyRingGenerator(id, pass, 0xc0); + pgpPublicKeyRing = pgpKeyRingGenerator.generatePublicKeyRing(); + pgpPublicKeyRing.encode(publicKeyRingStream); + publicKeyRingStream.close(); + pgpSecretKeyRing = pgpKeyRingGenerator.generateSecretKeyRing(); + pgpSecretKeyRing.encode(secretKeyRingStream); + secretKeyRingStream.close(); + } + + public PGPPublicKeyRing getPgpPublicKeyRing() { + return pgpPublicKeyRing; + } + + public PGPSecretKeyRing getPgpSecretKeyRing() { + return pgpSecretKeyRing; + } + + /** + * @param id ID + * @param pass password + * @param s2kcount is a number between 0 and 0xff that controls the + * number of times to iterate the password hash before use. More + * iterations are useful against offline attacks, as it takes more + * time to check each password. The actual number of iterations is + * rather complex, and also depends on the hash function in use. + * Refer to Section 3.7.1.3 in rfc4880.txt. Bigger numbers give + * you more iterations. As a rough rule of thumb, when using + * SHA256 as the hashing function, 0x10 gives you about 64 + * iterations, 0x20 about 128, 0x30 about 256 and so on till 0xf0, + * or about 1 million iterations. The maximum you can go to is + * 0xff, or about 2 million iterations. I'll use 0xc0 as a + * default -- about 130,000 iterations. + * @return PGP key ring generator + */ + private final PGPKeyRingGenerator generateKeyRingGenerator(String id, char[] pass, int s2kcount) throws PGPException { + RSAKeyPairGenerator rsaKeyPairGenerator = new RSAKeyPairGenerator(); + // Boilerplate RSA parameters, no need to change anything + // except for the RSA key-size (2048). You can use whatever + // key-size makes sense for you -- 4096, etc. + rsaKeyPairGenerator.init(new RSAKeyGenerationParameters(BigInteger.valueOf(0x10001), + new SecureRandom(), 2048, 12)); + // First create the master key with the generator. + PGPKeyPair pgpKeyPairMaster = + new BcPGPKeyPair(PGPPublicKey.RSA_GENERAL, rsaKeyPairGenerator.generateKeyPair(), new Date()); + // Then an encryption subkey. + PGPKeyPair pgpKeyPairSub = + new BcPGPKeyPair(PGPPublicKey.RSA_GENERAL, rsaKeyPairGenerator.generateKeyPair(), new Date()); + // Add a self-signature on the id + PGPSignatureSubpacketGenerator pgpSignatureSubpacketGenerator = new PGPSignatureSubpacketGenerator(); + // Add signed metadata on the signature: + // 1) Declare its purpose + pgpSignatureSubpacketGenerator.setKeyFlags(false, KeyFlags.SIGN_DATA | KeyFlags.CERTIFY_OTHER); + // 2) Set preferences for secondary crypto algorithms to use when sending messages to this key + pgpSignatureSubpacketGenerator.setPreferredSymmetricAlgorithms(false, new int[] { + SymmetricKeyAlgorithmTags.AES_256, + SymmetricKeyAlgorithmTags.AES_192, + SymmetricKeyAlgorithmTags.AES_128 + }); + pgpSignatureSubpacketGenerator.setPreferredHashAlgorithms(false, new int[] { + HashAlgorithmTags.SHA256, + HashAlgorithmTags.SHA1, + HashAlgorithmTags.SHA384, + HashAlgorithmTags.SHA512, + HashAlgorithmTags.SHA224 + }); + // 3) Request senders add additional checksums to the message (useful when verifying unsigned messages) + pgpSignatureSubpacketGenerator.setFeature(false, Features.FEATURE_MODIFICATION_DETECTION); + // Create a signature on the encryption subkey + PGPSignatureSubpacketGenerator signatureSubpacketGenerator = new PGPSignatureSubpacketGenerator(); + // Add metadata to declare its purpose + signatureSubpacketGenerator.setKeyFlags(false, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE); + // Objects used to encrypt the secret key + PGPDigestCalculator sha1DigestCalculator = new BcPGPDigestCalculatorProvider().get(HashAlgorithmTags.SHA1); + PGPDigestCalculator sha256DigestCalculator = new BcPGPDigestCalculatorProvider().get(HashAlgorithmTags.SHA256); + PBESecretKeyEncryptor pbeSecretKeyEncryptor = new BcPBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_256, + sha256DigestCalculator, s2kcount).build(pass); + // Finally, create the keyring itself. + // The constructor takes parameters that allow it to generate the self signature. + PGPKeyRingGenerator keyRingGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, + pgpKeyPairMaster, id, sha1DigestCalculator, pgpSignatureSubpacketGenerator.generate(), null, + new BcPGPContentSignerBuilder(pgpKeyPairMaster.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1), + pbeSecretKeyEncryptor); + // Add our encryption subkey, together with its signature + keyRingGen.addSubKey(pgpKeyPairSub, signatureSubpacketGenerator.generate(), null); + return keyRingGen; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/security/SignatureGenerator.java b/rpm-core/src/main/java/org/xbib/rpm/security/SignatureGenerator.java new file mode 100644 index 0000000..15464d7 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/security/SignatureGenerator.java @@ -0,0 +1,266 @@ +package org.xbib.rpm.security; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.xbib.rpm.exception.RpmException; +import org.xbib.rpm.header.entry.SpecEntry; +import org.xbib.rpm.io.ChannelWrapper; +import org.xbib.rpm.io.WritableChannelWrapper; +import org.xbib.rpm.signature.SignatureHeader; +import org.xbib.rpm.signature.SignatureTag; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.Iterator; +import java.util.logging.Logger; + +/** + * To verify the authenticity of the package, the SIGTAG_PGP tag holds a + * Version 3 OpenPGP Signature Packet RSA signature of the header and payload areas. + * The SIGTAG_GPG tag holds a Version 3 OpenPGP Signature Packet DSA signature of the header and payload areas. + * The SIGTAG_DSAHEADER holds a DSA signature of just the header section. + * If the SIGTAG_DSAHEADER tag is included, the SIGTAG_GPG tag must also be present. + * The SIGTAG_ RSAHEADER holds an RSA signature of just the header section. + * If the SIGTAG_ RSAHEADER tag is included, the SIGTAG_PGP tag must also be present. + */ +public class SignatureGenerator { + + private static final Logger logger = Logger.getLogger(SignatureGenerator.class.getName()); + + private final boolean enabled; + + private PGPPrivateKey privateKey; + + private SpecEntry headerOnlyEntry; + + private SpecEntry headerAndPayloadEntry; + + private ChannelWrapper.Key headerOnlyKey; + + private ChannelWrapper.Key headerAndPayloadKey; + + public SignatureGenerator(InputStream privateKeyRing, Long privateKeyId, String privateKeyPassphrase) { + if (privateKeyRing != null) { + PGPSecretKeyRingCollection keyRings = readKeyRing(privateKeyRing); + PGPSecretKey secretKey = findMatchingSecretKey(keyRings, privateKeyId); + this.privateKey = extractPrivateKey(secretKey, privateKeyPassphrase);; + this.enabled = privateKey != null; + } else { + this.enabled = false; + } + } + + public PGPPrivateKey getPrivateKey() { + return privateKey; + } + + @SuppressWarnings("unchecked") + public void prepare(SignatureHeader signature, HashAlgo algo) { + if (enabled) { + int count = 287; + switch (algo) { + case SHA256: + // V3 RSA/SHA256 signature + headerOnlyEntry = (SpecEntry) signature.addEntry(SignatureTag.RSAHEADER, count); + headerAndPayloadEntry = (SpecEntry) signature.addEntry(SignatureTag.LEGACY_PGP, count); + break; + default: + // RSA/SHA1 signature + headerOnlyEntry = (SpecEntry) signature.addEntry(SignatureTag.RSAHEADER, count); + headerAndPayloadEntry = (SpecEntry) signature.addEntry(SignatureTag.LEGACY_PGP, count); + break; + } + } + } + + public void startBeforeHeader(WritableChannelWrapper output, HashAlgo algo) throws RpmException { + if (enabled) { + try { + headerOnlyKey = output.start(new SignatureConsumer(algo.num())); + headerAndPayloadKey = output.start(new SignatureConsumer(algo.num())); + } catch (PGPException e) { + throw new RpmException(e); + } + } + } + + /** + * Start a digest. + * + * @param output the output channel + * @param digest the message digest + * @throws RpmException if digest could not be generated + * @return reference to the new key added to the consumers + */ + public ChannelWrapper.Key startDigest(WritableChannelWrapper output, String digest) throws RpmException { + try { + MessageDigest messageDigest = MessageDigest.getInstance(digest); + ChannelWrapper.Consumer consumer = new ChannelWrapper.Consumer() { + @Override + public void consume(ByteBuffer buffer) { + try { + messageDigest.update(buffer); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] finish() { + try { + return messageDigest.digest(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + return output.start(consumer); + } catch (NoSuchAlgorithmException e) { + throw new RpmException(e); + } + } + + private static int getTempArraySize(int totalSize) { + return Math.min(4096, totalSize); + } + + public void finishAfterHeader(WritableChannelWrapper output) { + finishEntry(output, headerOnlyEntry, headerOnlyKey); + } + + public void finishAfterPayload(WritableChannelWrapper output) { + finishEntry(output, headerAndPayloadEntry, headerAndPayloadKey); + } + + public boolean isEnabled() { + return enabled; + } + + private PGPSecretKeyRingCollection readKeyRing(InputStream privateKeyRing) { + try { + InputStream keyInputStream = new BufferedInputStream(privateKeyRing); + try (InputStream decoderStream = PGPUtil.getDecoderStream(keyInputStream)) { + return new PGPSecretKeyRingCollection(decoderStream, new JcaKeyFingerprintCalculator()); + } + } catch (IOException e) { + throw new IllegalArgumentException("Could not read key ring", e); + } catch (PGPException e) { + throw new IllegalArgumentException("Could not extract key ring", e); + } + } + + private PGPSecretKey findMatchingSecretKey(PGPSecretKeyRingCollection keyRings, Long privateKeyId) { + Iterator iter = keyRings.getKeyRings(); + while (iter.hasNext()) { + PGPSecretKeyRing keyRing = iter.next(); + @SuppressWarnings("unchecked") + Iterator keyIter = keyRing.getSecretKeys(); + while (keyIter.hasNext()) { + PGPSecretKey key = keyIter.next(); + if (key.isSigningKey() && isMatchingKeyId(key, privateKeyId)) { + return key; + } + } + } + throw new IllegalArgumentException("can't find signing key in key rings"); + } + + private boolean isMatchingKeyId(PGPSecretKey key, Long privateKeyId) { + return privateKeyId == null || Long.toHexString(key.getKeyID()).endsWith(Long.toHexString(privateKeyId)); + } + + private PGPPrivateKey extractPrivateKey(PGPSecretKey secretKey, String privateKeyPassphrase) { + try { + PBESecretKeyDecryptor secretKeyDecryptor = + new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) + .build(privateKeyPassphrase.toCharArray()); + return secretKey.extractPrivateKey(secretKeyDecryptor); + } catch (Exception e) { + throw new IllegalArgumentException("could not extract private key from key ring", e); + } + } + + private void finishEntry(WritableChannelWrapper output, SpecEntry entry, ChannelWrapper.Key key) { + if (enabled) { + if (key == null) { + throw new IllegalStateException("key is not initialized"); + } + if (entry == null) { + throw new IllegalStateException("entry not initialized"); + } + byte[] b = output.finish(key); + entry.setCount(b.length); + entry.setValues(b); + } + } + + class SignatureConsumer implements ChannelWrapper.Consumer { + + PGPSignatureGenerator pgpSignatureGenerator; + + SignatureConsumer(int hashAlgorithm) throws PGPException { + int keyAlgorithm = privateKey.getPublicKeyPacket().getAlgorithm(); + BcPGPContentSignerBuilder contentSignerBuilder = + new BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); + this.pgpSignatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); + pgpSignatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); + } + + @Override + public void consume(ByteBuffer buffer) { + if (!buffer.hasRemaining()) { + return; + } + try { + write(buffer); + } catch (SignatureException e) { + throw new RuntimeException("could not update signature generator", e); + } + } + + private void write(ByteBuffer buffer) throws SignatureException { + if (buffer.hasArray()) { + byte[] bufferBytes = buffer.array(); + int offset = buffer.arrayOffset(); + int position = buffer.position(); + int limit = buffer.limit(); + pgpSignatureGenerator.update(bufferBytes, offset + position, limit - position); + buffer.position(limit); + } else { + int length = buffer.remaining(); + byte[] bytes = new byte[getTempArraySize(length)]; + while (length > 0) { + int chunk = Math.min(length, bytes.length); + buffer.get(bytes, 0, chunk); + pgpSignatureGenerator.update(bytes, 0, chunk); + length -= chunk; + } + } + } + + @Override + public byte[] finish() { + try { + return pgpSignatureGenerator.generate().getEncoded(); + } catch (Exception e) { + throw new RuntimeException("could not generate signature", e); + } + } + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/security/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/security/package-info.java new file mode 100644 index 0000000..3906133 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/security/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for security in RPM archives. + */ +package org.xbib.rpm.security; diff --git a/rpm-core/src/main/java/org/xbib/rpm/signature/SignatureHeader.java b/rpm-core/src/main/java/org/xbib/rpm/signature/SignatureHeader.java new file mode 100644 index 0000000..1835738 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/signature/SignatureHeader.java @@ -0,0 +1,21 @@ +package org.xbib.rpm.signature; + +import org.xbib.rpm.header.AbstractHeader; + +/** + * + */ +public class SignatureHeader extends AbstractHeader { + + public SignatureHeader() { + for (SignatureTag tag : SignatureTag.values()) { + tags.put(tag.getCode(), tag); + } + } + + @Override + protected boolean pad() { + return true; + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/signature/SignatureTag.java b/rpm-core/src/main/java/org/xbib/rpm/signature/SignatureTag.java new file mode 100644 index 0000000..db87bef --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/signature/SignatureTag.java @@ -0,0 +1,63 @@ +package org.xbib.rpm.signature; + +import org.xbib.rpm.header.EntryType; + +/** + * + */ +public enum SignatureTag implements EntryType { + + SIGNATURES(62, BIN_ENTRY, "signatures"), + + SIGSIZE(257, INT32_ENTRY, "sigsize"), + // 258 is obsolete + PGP(259, BIN_ENTRY, "pgp"), + // 260 is obsolete + MD5(261, BIN_ENTRY, "md5"), + GPG(262, BIN_ENTRY, "gpg"), + // 263, 264, 265 are obsolete + PUBKEYS(266, STRING_ARRAY_ENTRY, "pubkeys"), + DSAHEADER(267, BIN_ENTRY, "dsaheader"), + RSAHEADER(268, BIN_ENTRY, "rsaheader"), + SHA1HEADER(269, STRING_ENTRY, "sha1header"), + LONGSIGSIZE(270, INT64_ENTRY, "longsigsize"), + LONGARCHIVESIZE(271, INT64_ENTRY, "longarchivesize"), + // 272 is reserved + SHA256(273, BIN_ENTRY, "sha256"), + + LEGACY_SIGSIZE(1000, INT32_ENTRY, "sigsize"), + LEGACY_PGP(1002, BIN_ENTRY, "pgp"), + LEGACY_MD5(1004, BIN_ENTRY, "md5"), + LEGACY_GPG(1005, BIN_ENTRY, "gpg"), + PAYLOADSIZE(1007, INT32_ENTRY, "payloadsize"), + LEGACY_SHA1HEADER(1010, STRING_ENTRY, "sha1header"), + LEGACY_DSAHEADER(1011, BIN_ENTRY, "dsaheader"), + LEGACY_RSAHEADER(1012, BIN_ENTRY, "rsaheader") + + ; + + private int code; + + private int type; + + private String name; + + SignatureTag(final int code, final int type, final String name) { + this.code = code; + this.type = type; + this.name = name; + } + + public int getCode() { + return code; + } + + public int getType() { + return type; + } + + public String getName() { + return name; + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/signature/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/signature/package-info.java new file mode 100644 index 0000000..ffeb4ac --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/signature/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for signatures in RPM archives. + */ +package org.xbib.rpm.signature; diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/AbstractTrigger.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/AbstractTrigger.java new file mode 100644 index 0000000..c048a87 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/AbstractTrigger.java @@ -0,0 +1,44 @@ +package org.xbib.rpm.trigger; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Base class for an RPM Trigger. + */ +public abstract class AbstractTrigger implements Trigger { + + protected Path script; + + protected List depends = new ArrayList<>(); + + @Override + public Path getScript() { + return script; + } + + @Override + public void setScript(Path script) { + this.script = script; + } + + @Override + public void addDepends(Depends depends) { + this.depends.add(depends); + } + + @Override + public Map getDepends() { + Map dependsMap = new HashMap<>(); + for (Depends d : this.depends) { + dependsMap.put(d.getName(), new IntString(d.getComparison(), d.getVersion())); + } + return dependsMap; + } + + @Override + public abstract int getFlag(); +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/Depends.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/Depends.java new file mode 100644 index 0000000..470bd62 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/Depends.java @@ -0,0 +1,42 @@ +package org.xbib.rpm.trigger; + +import org.xbib.rpm.format.Flags; + +/** + * A dependency on a particular version of an RPM package. + */ +public class Depends { + + protected String name; + + protected String version = ""; + + private int comparison = 0; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getComparison() { + if (0 == comparison && 0 < version.length()) { + return Flags.GREATER | Flags.EQUAL; + } + if (0 == version.length()) { + return 0; + } + return comparison; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/Trigger.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/Trigger.java new file mode 100644 index 0000000..2e7ec8d --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/Trigger.java @@ -0,0 +1,52 @@ +package org.xbib.rpm.trigger; + +import java.nio.file.Path; +import java.util.Map; + +/** + * + */ +public interface Trigger { + + Path getScript(); + + void setScript(Path script); + + void addDepends(Depends depends); + + Map getDepends(); + + int getFlag(); + + /** + * Simple class to pair an int and a String with each other. + */ + class IntString { + + private int theInt = 0; + + private String theString = ""; + + public IntString(int theInt, String theString) { + this.theInt = theInt; + this.theString = theString; + } + + public int getInt() { + return this.theInt; + } + + public void setInt(int theInt) { + this.theInt = theInt; + } + + public String getString() { + return this.theString; + } + + public void setString(String theString) { + this.theString = theString; + } + + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerIn.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerIn.java new file mode 100644 index 0000000..f26750b --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerIn.java @@ -0,0 +1,14 @@ +package org.xbib.rpm.trigger; + +import org.xbib.rpm.format.Flags; + +/** + * A TriggerIn. + */ +public class TriggerIn extends AbstractTrigger implements Trigger { + + @Override + public int getFlag() { + return Flags.SCRIPT_TRIGGERIN; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPostUn.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPostUn.java new file mode 100644 index 0000000..4193040 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPostUn.java @@ -0,0 +1,14 @@ +package org.xbib.rpm.trigger; + +import org.xbib.rpm.format.Flags; + +/** + * A TriggerPostUn. + */ +public class TriggerPostUn extends AbstractTrigger implements Trigger { + + @Override + public int getFlag() { + return Flags.SCRIPT_TRIGGERPOSTUN; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPreIn.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPreIn.java new file mode 100644 index 0000000..5b9ea82 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerPreIn.java @@ -0,0 +1,14 @@ +package org.xbib.rpm.trigger; + +import org.xbib.rpm.format.Flags; + +/** + * A TriggerPreIn. + */ +public class TriggerPreIn extends AbstractTrigger implements Trigger { + + @Override + public int getFlag() { + return Flags.SCRIPT_TRIGGERPREIN; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerUn.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerUn.java new file mode 100644 index 0000000..2da6e4d --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/TriggerUn.java @@ -0,0 +1,14 @@ +package org.xbib.rpm.trigger; + +import org.xbib.rpm.format.Flags; + +/** + * A TriggerUn. + */ +public class TriggerUn extends AbstractTrigger implements Trigger { + + @Override + public int getFlag() { + return Flags.SCRIPT_TRIGGERUN; + } +} diff --git a/rpm-core/src/main/java/org/xbib/rpm/trigger/package-info.java b/rpm-core/src/main/java/org/xbib/rpm/trigger/package-info.java new file mode 100644 index 0000000..08d0aa1 --- /dev/null +++ b/rpm-core/src/main/java/org/xbib/rpm/trigger/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RPM triggers. + */ +package org.xbib.rpm.trigger; diff --git a/rpm-core/src/test/java/org/xbib/rpm/RpmBuilderTest.java b/rpm-core/src/test/java/org/xbib/rpm/RpmBuilderTest.java new file mode 100644 index 0000000..2e69d13 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/RpmBuilderTest.java @@ -0,0 +1,255 @@ +package org.xbib.rpm; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Ignore; +import org.junit.Test; +import org.xbib.rpm.format.Flags; +import org.xbib.rpm.format.Format; +import org.xbib.rpm.header.HeaderTag; +import org.xbib.rpm.lead.Architecture; +import org.xbib.rpm.lead.Os; +import org.xbib.rpm.lead.PackageType; +import org.xbib.rpm.payload.CompressionType; +import org.xbib.rpm.payload.Directive; +import org.xbib.rpm.security.HashAlgo; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.EnumSet; + +/** + * + */ +public class RpmBuilderTest { + + @Test + public void testLongNameTruncation() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + + "xxxxa", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.build(getTargetDir()); + Path path = getTargetDir().resolve("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + + "xxxxxxxxxxxa-1.0-1.noarch.rpm"); + Format format = new RpmReader().readHeader(path); + assertEquals("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + format.getLead().getName()); + } + + @Test + public void testFiles() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("filestest", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + EnumSet directives = EnumSet.of(Directive.CONFIG, Directive.DOC, Directive.NOREPLACE); + rpmBuilder.addFile("/etc", Paths.get("src/test/resources/prein.sh"), 493, 493, + directives, "jabberwocky", "vorpal"); + rpmBuilder.build(getTargetDir()); + Path path = getTargetDir().resolve("filestest-1.0-1.noarch.rpm"); + Format format = new RpmReader().readHeader(path); + assertArrayEquals(new String[]{"jabberwocky"}, + (String[]) format.getHeader().getEntry(HeaderTag.FILEUSERNAME).getValues()); + assertArrayEquals(new String[]{"vorpal"}, + (String[]) format.getHeader().getEntry(HeaderTag.FILEGROUPNAME).getValues()); + Integer expectedFlags = 0; + for (Directive d : directives) { + expectedFlags |= d.flag(); + } + assertArrayEquals(new Integer[]{expectedFlags}, + (Integer[]) format.getHeader().getEntry(HeaderTag.FILEFLAGS).getValues()); + } + + @Test + public void testBuild() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("test", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.build(getTargetDir()); + } + + @Test + public void testBuildWithEpoch() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("testEpoch", "1.0", "1", 1); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.build(getTargetDir()); + } + + @Test + public void testBuildMetapackage() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("testMetapkg", "1.0", "1", 1); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.addDependencyMore("glibc", "2.17"); + rpmBuilder.build(getTargetDir()); + } + + @Test + public void testCapabilities() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("testCapabilities", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.addDependency("httpd", 0, ""); + rpmBuilder.addProvides("frobnicator", ""); + rpmBuilder.addProvides("barnacle", "3.89"); + rpmBuilder.addConflicts("fooberry", Flags.GREATER | Flags.EQUAL, "1a"); + rpmBuilder.addObsoletes("testCappypkg", 0, ""); + rpmBuilder.build(getTargetDir()); + Path path = getTargetDir().resolve("testCapabilities-1.0-1.noarch.rpm"); + Format format = new RpmReader().readHeader(path); + String[] require = (String[]) format.getHeader().getEntry(HeaderTag.REQUIRENAME).getValues(); + Integer[] requireflags = (Integer[]) format.getHeader().getEntry(HeaderTag.REQUIREFLAGS).getValues(); + String[] requireversion = (String[]) format.getHeader().getEntry(HeaderTag.REQUIREVERSION).getValues(); + assertArrayEquals(new String[]{"httpd"}, Arrays.copyOfRange(require, require.length - 1, require.length)); + assertArrayEquals(new Integer[]{0}, Arrays.copyOfRange(requireflags, requireflags.length - 1, require.length)); + assertArrayEquals(new String[]{""}, Arrays.copyOfRange(requireversion, requireversion.length - 1, require.length)); + String[] provide = (String[]) format.getHeader().getEntry(HeaderTag.PROVIDENAME).getValues(); + Integer[] provideflags = (Integer[]) format.getHeader().getEntry(HeaderTag.PROVIDEFLAGS).getValues(); + String[] provideversion = (String[]) format.getHeader().getEntry(HeaderTag.PROVIDEVERSION).getValues(); + assertArrayEquals(new String[]{"testCapabilities", "frobnicator", "barnacle"}, provide); + assertArrayEquals(new Integer[]{Flags.EQUAL, 0, Flags.EQUAL}, provideflags); + assertArrayEquals(new String[]{"0:1.0-1", "", "3.89"}, provideversion); + String[] conflict = (String[]) format.getHeader().getEntry(HeaderTag.CONFLICTNAME).getValues(); + Integer[] conflictflags = (Integer[]) format.getHeader().getEntry(HeaderTag.CONFLICTFLAGS).getValues(); + String[] conflictversion = (String[]) format.getHeader().getEntry(HeaderTag.CONFLICTVERSION).getValues(); + assertArrayEquals(new String[]{"fooberry"}, conflict); + assertArrayEquals(new Integer[]{Flags.GREATER | Flags.EQUAL}, conflictflags); + assertArrayEquals(new String[]{"1a"}, conflictversion); + String[] obsolete = (String[]) format.getHeader().getEntry(HeaderTag.OBSOLETENAME).getValues(); + Integer[] obsoleteflags = (Integer[]) format.getHeader().getEntry(HeaderTag.OBSOLETEFLAGS).getValues(); + String[] obsoleteversion = (String[]) format.getHeader().getEntry(HeaderTag.OBSOLETEVERSION).getValues(); + assertArrayEquals(new String[]{"testCappypkg"}, obsolete); + assertArrayEquals(new Integer[]{0}, obsoleteflags); + assertArrayEquals(new String[]{""}, obsoleteversion); + } + + @Test + public void testMultipleCapabilities() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("testMultipleCapabilities", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.addDependency("httpd", Flags.GREATER | Flags.EQUAL, "1.0"); + rpmBuilder.addDependency("httpd", Flags.LESS, "2.0"); + rpmBuilder.build(getTargetDir()); + Path path = getTargetDir().resolve("testMultipleCapabilities-1.0-1.noarch.rpm"); + Format format = new RpmReader().readHeader(path); + String[] require = (String[]) format.getHeader().getEntry(HeaderTag.REQUIRENAME).getValues(); + Integer[] requireflags = (Integer[]) format.getHeader().getEntry(HeaderTag.REQUIREFLAGS).getValues(); + String[] requireversion = (String[]) format.getHeader().getEntry(HeaderTag.REQUIREVERSION).getValues(); + assertArrayEquals(new String[]{"httpd"}, + Arrays.copyOfRange(require, require.length - 2, require.length - 1)); + assertArrayEquals(new Integer[]{Flags.GREATER | Flags.EQUAL}, + Arrays.copyOfRange(requireflags, requireflags.length - 2, require.length - 1)); + assertArrayEquals(new String[]{"1.0"}, + Arrays.copyOfRange(requireversion, requireversion.length - 2, require.length - 1)); + assertArrayEquals(new String[]{"httpd"}, + Arrays.copyOfRange(require, require.length - 1, require.length)); + assertArrayEquals(new Integer[]{Flags.LESS}, + Arrays.copyOfRange(requireflags, requireflags.length - 1, require.length)); + assertArrayEquals(new String[]{"2.0"}, + Arrays.copyOfRange(requireversion, requireversion.length - 1, require.length)); + } + + @Test + public void testProvideOverride() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("testProvideOverride", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.addProvides("testProvideOverride", "1.0"); + rpmBuilder.build(getTargetDir()); + Path path = getTargetDir().resolve("testProvideOverride-1.0-1.noarch.rpm"); + Format format = new RpmReader().readHeader(path); + String[] provide = (String[]) format.getHeader().getEntry(HeaderTag.PROVIDENAME).getValues(); + Integer[] provideflags = (Integer[]) format.getHeader().getEntry(HeaderTag.PROVIDEFLAGS).getValues(); + String[] provideversion = (String[]) format.getHeader().getEntry(HeaderTag.PROVIDEVERSION).getValues(); + assertEquals(1, provide.length); + assertArrayEquals(new String[]{"testProvideOverride"}, Arrays.copyOfRange(provide, 0, provide.length)); + assertArrayEquals(new Integer[]{Flags.EQUAL}, Arrays.copyOfRange(provideflags, 0, provide.length)); + assertArrayEquals(new String[]{"1.0"}, Arrays.copyOfRange(provideversion, 0, provide.length)); + } + + @Test + @Ignore + public void testAddHeaderEntry() { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.addHeaderEntry(HeaderTag.CHANGELOGTIME, new Integer(1)); + try { + rpmBuilder.addHeaderEntry(HeaderTag.CHANGELOGNAME, 1L); + } catch (ClassCastException e) { + // + } + try { + rpmBuilder.addHeaderEntry(HeaderTag.CHANGELOGTIME, "Mon Jan 01 2016"); + fail("ClassCastException expected on setting header String value where int expected."); + } catch (ClassCastException e) { + // + } + try { + rpmBuilder.addHeaderEntry(HeaderTag.CHANGELOGTIME, 1L); + fail("ClassCastException expected on setting header long value where int expected."); + } catch (ClassCastException e) { + // + } + try { + short s = (short) 1; + rpmBuilder.addHeaderEntry(HeaderTag.CHANGELOGTIME, s); + fail("ClassCastException expected on setting header short value where int expected."); + } catch (ClassCastException e) { + // + } + try { + Character c = 'c'; + rpmBuilder.addHeaderEntry(HeaderTag.CHANGELOGTIME, c); + fail("ClassCastException expected on setting header char value where int expected."); + } catch (ClassCastException e) { + // + } + } + + @Test + public void testBzip2Build() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(HashAlgo.SHA256, CompressionType.GZIP); + rpmBuilder.setPackage("test-compressed", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + EnumSet directives = EnumSet.of(Directive.CONFIG, Directive.DOC, Directive.NOREPLACE); + rpmBuilder.addFile("/etc", Paths.get("src/test/resources/prein.sh"), 493, 493, + directives, "jabberwocky", "vorpal"); + rpmBuilder.build(getTargetDir()); + Path path = getTargetDir().resolve("test-compressed-1.0-1.noarch.rpm"); + Format format = new RpmReader().readHeader(path); + } + + private Path getTargetDir() { + return Paths.get("build"); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/RpmReaderTest.java b/rpm-core/src/test/java/org/xbib/rpm/RpmReaderTest.java new file mode 100644 index 0000000..0b65004 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/RpmReaderTest.java @@ -0,0 +1,53 @@ +package org.xbib.rpm; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.xbib.rpm.format.Format; + +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + */ +public class RpmReaderTest { + + @Test + public void readNoArchRPMTest() throws Exception { + RpmReader rpmReader = new RpmReader(); + rpmReader.read(Paths.get("src/test/resources/rpm-1-1.0-1.noarch.rpm")); + } + + @Test + public void readSomeArchTest() throws Exception { + RpmReader rpmReader = new RpmReader(); + rpmReader.read(Paths.get("src/test/resources/rpm-3-1.0-1.somearch.rpm")); + } + + @Test + public void setHeaderStartAndEndPosition() throws Exception { + Format format = new RpmReader().readHeader(getClass().getResourceAsStream("/rpm-1-1.0-1.noarch.rpm")); + assertEquals(280, format.getHeader().getStartPos()); + assertEquals(4760, format.getHeader().getEndPos()); + } + + @Test + public void fileModesHeaderIsCorrect() throws Exception { + Format format = new RpmReader().readHeader(getClass().getResourceAsStream("/rpm-1-1.0-1.noarch.rpm")); + String rpmDescription = format.toString(); + Matcher matcher = Pattern.compile(".*filemodes\\[[^\\]]*\\]\\n[^0-9-]*([^\\n]*).*", Pattern.DOTALL) + .matcher(rpmDescription); + if (matcher.matches()) { + String[] fileModesFromString = matcher.group(1).split(", "); + String[] expectedFileModes = {"33188", "41471", "16877", "33261", "33261", "33261", "33261", "41453", + "33261", "16877", "33188", "33188", "16877", "33188", "41471"}; + assertArrayEquals(expectedFileModes, fileModesFromString); + } else { + fail("no match: " + rpmDescription); + } + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/SimpleRpmTest.java b/rpm-core/src/test/java/org/xbib/rpm/SimpleRpmTest.java new file mode 100644 index 0000000..b4c301a --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/SimpleRpmTest.java @@ -0,0 +1,33 @@ +package org.xbib.rpm; + +import org.junit.Test; +import org.xbib.rpm.lead.Architecture; +import org.xbib.rpm.lead.Os; +import org.xbib.rpm.lead.PackageType; + +import java.nio.file.Paths; + +/** + * + */ +public class SimpleRpmTest { + + @Test + public void testRpm() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("test", "0.0.1", "1"); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setSummary("Test RPM"); + rpmBuilder.setDescription("A test RPM with a few packaged files."); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("MIT"); + rpmBuilder.setGroup("Miscellaneous"); + rpmBuilder.setDistribution("MyDistribution"); + rpmBuilder.setVendor("My vendor repository http://example.org/repo"); + rpmBuilder.setPackager("Jane Doe"); + rpmBuilder.setUrl("http://example.org"); + rpmBuilder.setProvides("test"); + rpmBuilder.build(Paths.get("build")); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogHandlerTest.java b/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogHandlerTest.java new file mode 100644 index 0000000..603f689 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogHandlerTest.java @@ -0,0 +1,65 @@ +package org.xbib.rpm.changelog; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.xbib.rpm.RpmBuilder; +import org.xbib.rpm.exception.ChangelogParseException; +import org.xbib.rpm.exception.NoInitialAsteriskException; + +import java.io.IOException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; + +/** + * + */ +public class ChangelogHandlerTest { + + private RpmBuilder rpmBuilder; + + @Before + public void setUp() throws Exception { + rpmBuilder = new RpmBuilder(); + } + + @Test + public void testAddChangeLog() { + try { + rpmBuilder.addChangelog(Paths.get("non.existent.file")); + fail("non-existent file throws FileNotFoundException: not thrown"); + } catch (IOException e) { + assertTrue("non-existent file exception", e instanceof NoSuchFileException); + } catch (ChangelogParseException e) { + fail("non-existent file throws FileNotFoundException: ChangelogParseException thrown instead"); + } + } + + @Test + public void testBadChangeLog() { + try { + rpmBuilder.addChangelog(getClass().getResource("bad.changelog")); + fail("bad Changelog file throws ChangelogParseException: not thrown"); + } catch (IOException e) { + fail("bad Changelog file throws ChangelogParseException: IOException thrown instead"); + } catch (ChangelogParseException e) { + assertTrue("bad Changelog file throws ChangelogParseException", e instanceof NoInitialAsteriskException); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void commentsIgnored() { + try { + rpmBuilder.addChangelog(getClass().getResource("changelog.with.comments")); + } catch (IOException e) { + fail("comments_ignored: IOException thrown instead"); + } catch (ChangelogParseException e) { + fail("comments_ignored: failed"); + } + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogParserTest.java b/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogParserTest.java new file mode 100644 index 0000000..05cc1fa --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogParserTest.java @@ -0,0 +1,214 @@ +package org.xbib.rpm.changelog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.xbib.rpm.exception.ChangelogParseException; +import org.xbib.rpm.exception.DatesOutOfSequenceException; +import org.xbib.rpm.exception.IncompleteChangelogEntryException; +import org.xbib.rpm.exception.InvalidChangelogDateException; +import org.xbib.rpm.exception.NoInitialAsteriskException; + +import java.io.IOException; +import java.util.List; + +/** + * + */ +public class ChangelogParserTest { + + private ChangelogParser parser; + + private List changelogs; + + @Before + public void setUp() throws Exception { + parser = new ChangelogParser(); + } + + @Test + public void testParsesCorrectlyFormattedChangelog() { + String[] lines = { + "* Tue Feb 24 2015 George Washington", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", + "* Tue Feb 10 2015 George Washington", + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + }; + try { + changelogs = parser.parse(lines); + assertEquals("parses correctly formatted Changelog", 2, changelogs.size()); + } catch (ChangelogParseException e) { + fail("parses correctly formatted Changelog"); + } + } + + @Test + public void commentsIgnored() { + String[] lines = { + "# ORDER MUST BE DESCENDING (most recent change at top)", + "* Tue Feb 24 2015 George Washington", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", + "* Tue Feb 10 2015 George Washington", + "# a random comment", + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + }; + + try { + changelogs = parser.parse(lines); + assertEquals("comments_ignored", 2, changelogs.size()); + } catch (ChangelogParseException e) { + fail("comments_ignored: failed"); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void error_thrown_if_dates_out_of_order() { + String[] lines = { + "* Tue Feb 10 2015 George Washington", + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + "* Tue Feb 24 2015 George Washington", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" + }; + + try { + changelogs = parser.parse(lines); + fail("error thrown if dates out of order"); + } catch (ChangelogParseException e) { + assertTrue(e instanceof DatesOutOfSequenceException); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void errorThrownOnWrongDateFormat() { + // 2/24/2015 was a Tuesday + String[] lines = { + "* 02/24/2015 George Washington", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" + }; + + try { + changelogs = parser.parse(lines); + fail("error thrown on wrong date format"); + } catch (ChangelogParseException e) { + assertTrue(e instanceof InvalidChangelogDateException); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void errorThrownOnIncorrectDayOfWeek() { + // 2/24/2015 was a Tuesday + String[] lines = { + "* Wed Feb 24 2015 George Washington", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" + }; + + try { + changelogs = parser.parse(lines); + fail("error thrown on incorrect day of week"); + } catch (ChangelogParseException e) { + assertTrue(e instanceof InvalidChangelogDateException); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void errorThrownOnNoDescription() { + String[] lines = { + "* Tue Feb 24 2015 George Washington", + "* Tue Feb 10 2015 George Washington", + }; + + try { + changelogs = parser.parse(lines); + fail("error thrown on no description"); + } catch (ChangelogParseException e) { + assertTrue(e instanceof IncompleteChangelogEntryException); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void errorThrownOnNoInitialAsterisk() { + String[] lines = { + "Tue Feb 24 2015 George Washington", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" + }; + + try { + changelogs = parser.parse(lines); + fail("error thrown on no initial asterisk"); + } catch (ChangelogParseException e) { + assertTrue(e instanceof NoInitialAsteriskException); + } + } + + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void errorThrownOnNoUserName() { + String[] lines = { + "* Tue Feb 24 2015", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" + }; + + try { + changelogs = parser.parse(lines); + fail("error thrown on no user name"); + } catch (ChangelogParseException e) { + assertTrue(e instanceof IncompleteChangelogEntryException); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.lang.String[])}. + */ + @Test + public void errorThrownOnNoUserNameOnFirstLine() { + String[] lines = { + "* Tue Feb 24 2015", + "George Washington", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" + }; + + try { + changelogs = parser.parse(lines); + fail("error thrown on no user name on first line"); + } catch (ChangelogParseException e) { + assertTrue(e instanceof IncompleteChangelogEntryException); + } + } + + /** + * Test method for {@link org.xbib.rpm.changelog.ChangelogParser#parse(java.io.InputStream)}. + */ + @Test + public void parsesFileCorrectly() { + try { + changelogs = parser.parse(getClass().getResourceAsStream("changelog")); + assertEquals("parses file correctly", 10, changelogs.size()); + } catch (ChangelogParseException e) { + fail("parses file correctly"); + } catch (IOException e) { + fail("parses file correctly: " + e.getMessage()); + } + + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogTest.java b/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogTest.java new file mode 100644 index 0000000..013d4cf --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/changelog/ChangelogTest.java @@ -0,0 +1,66 @@ +package org.xbib.rpm.changelog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.xbib.rpm.RpmBuilder; +import org.xbib.rpm.RpmReader; +import org.xbib.rpm.format.Format; +import org.xbib.rpm.header.EntryType; +import org.xbib.rpm.header.HeaderTag; +import org.xbib.rpm.header.entry.SpecEntry; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * + */ +public class ChangelogTest { + + @Test + public void testChangelog() throws Exception { + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.addChangelog(getClass().getResource("changelog")); + Path path = Paths.get("build"); + rpmBuilder.build(path); + RpmReader rpmReader = new RpmReader(); + Format format = rpmReader.read(path.resolve(rpmBuilder.getPackageName())); + assertDateEntryHeaderEqualsAt("Tue Feb 24 2015", format, + HeaderTag.CHANGELOGTIME, 10, 0); + assertHeaderEqualsAt("Thomas Jefferson", format, + HeaderTag.CHANGELOGNAME, 10, 4); + assertHeaderEqualsAt("- Initial rpm for this package", format, + HeaderTag.CHANGELOGTEXT, 10, 9); + String expectedMultiLineDescription = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod \n" + + "tempor incididunt ut labore et dolore magna aliqua"; + assertHeaderEqualsAt(expectedMultiLineDescription, format, + HeaderTag.CHANGELOGTEXT, 10, 0); + } + + private void assertDateEntryHeaderEqualsAt(String expected, Format format, EntryType entryType, int size, int pos) { + assertNotNull("null format", format); + SpecEntry entry = format.getHeader().getEntry(entryType); + assertNotNull("Entry not found : " + entryType.getName(), entry); + assertEquals("Entry type : " + entryType.getName(), 4, entry.getType()); + Integer[] values = (Integer[]) entry.getValues(); + assertNotNull("null values", values); + assertEquals("Entry size : " + entryType.getName(), size, values.length); + LocalDateTime localDate = LocalDateTime.ofEpochSecond(values[pos], 0, ZoneOffset.UTC); + assertEquals("Entry value : " + entryType.getName(), expected, ChangelogParser.CHANGELOG_FORMAT.format(localDate)); + } + + private void assertHeaderEqualsAt(String expected, Format format, EntryType entryType, int size, int pos) { + assertNotNull("null format", format); + SpecEntry entry = format.getHeader().getEntry(entryType); + assertNotNull("Entry not found : " + entryType.getName(), entry); + assertEquals("Entry type : " + entryType.getName(), 8, entry.getType()); + String[] values = (String[]) entry.getValues(); + assertNotNull("null values", values); + assertEquals("Entry size : " + entryType.getName(), size, values.length); + assertEquals("Entry value : " + entryType.getName(), expected, values[pos]); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/changelog/package-info.java b/rpm-core/src/test/java/org/xbib/rpm/changelog/package-info.java new file mode 100644 index 0000000..0354a2e --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/changelog/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm.changelog; diff --git a/rpm-core/src/test/java/org/xbib/rpm/exception/InvalidDirectiveExceptionTest.java b/rpm-core/src/test/java/org/xbib/rpm/exception/InvalidDirectiveExceptionTest.java new file mode 100644 index 0000000..3813eda --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/exception/InvalidDirectiveExceptionTest.java @@ -0,0 +1,17 @@ +package org.xbib.rpm.exception; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * + */ +public class InvalidDirectiveExceptionTest { + @Test + public void exception() { + InvalidDirectiveException ex + = new InvalidDirectiveException("directive"); + assertEquals("RPM directive 'directive' invalid", ex.getMessage()); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/exception/InvalidPathExceptionTest.java b/rpm-core/src/test/java/org/xbib/rpm/exception/InvalidPathExceptionTest.java new file mode 100644 index 0000000..603d870 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/exception/InvalidPathExceptionTest.java @@ -0,0 +1,19 @@ +package org.xbib.rpm.exception; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * + */ +public class InvalidPathExceptionTest { + + @Test + public void exception() { + Exception cause = new Exception("cause"); + InvalidPathException ex = new InvalidPathException("invalid/path", cause); + assertEquals("Path invalid/path is invalid, causing exception", ex.getMessage()); + assertEquals(cause, ex.getCause()); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/exception/PathOutsideBuildPathExceptionTest.java b/rpm-core/src/test/java/org/xbib/rpm/exception/PathOutsideBuildPathExceptionTest.java new file mode 100644 index 0000000..431c41f --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/exception/PathOutsideBuildPathExceptionTest.java @@ -0,0 +1,18 @@ +package org.xbib.rpm.exception; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * + */ +public class PathOutsideBuildPathExceptionTest { + + @Test + public void exception() { + PathOutsideBuildPathException ex + = new PathOutsideBuildPathException("scan", "build"); + assertEquals("Scan path scan outside of build directory build", ex.getMessage()); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/exception/SigningKeyNotFoundExceptionTest.java b/rpm-core/src/test/java/org/xbib/rpm/exception/SigningKeyNotFoundExceptionTest.java new file mode 100644 index 0000000..47617ad --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/exception/SigningKeyNotFoundExceptionTest.java @@ -0,0 +1,17 @@ +package org.xbib.rpm.exception; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * + */ +public class SigningKeyNotFoundExceptionTest { + + @Test + public void testException() { + SigningKeyNotFoundException exception = new SigningKeyNotFoundException("keyfile"); + assertEquals("Signing key keyfile could not be found", exception.getMessage()); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/exception/UnknownArchitectureExceptionTest.java b/rpm-core/src/test/java/org/xbib/rpm/exception/UnknownArchitectureExceptionTest.java new file mode 100644 index 0000000..a602716 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/exception/UnknownArchitectureExceptionTest.java @@ -0,0 +1,25 @@ +package org.xbib.rpm.exception; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * + */ +public class UnknownArchitectureExceptionTest { + @Test + public void exception() { + UnknownArchitectureException ex + = new UnknownArchitectureException("unknown"); + assertEquals("Unknown architecture 'unknown'", ex.getMessage()); + } + + @Test + public void exceptionWithCause() { + Exception cause = new Exception("cause"); + UnknownArchitectureException ex = new UnknownArchitectureException("unknown", cause); + assertEquals("Unknown architecture 'unknown'", ex.getMessage()); + assertEquals(cause, ex.getCause()); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/exception/UnknownOperatingSystemExceptionTest.java b/rpm-core/src/test/java/org/xbib/rpm/exception/UnknownOperatingSystemExceptionTest.java new file mode 100644 index 0000000..5c0df56 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/exception/UnknownOperatingSystemExceptionTest.java @@ -0,0 +1,26 @@ +package org.xbib.rpm.exception; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * + */ +public class UnknownOperatingSystemExceptionTest { + @Test + public void exception() { + UnknownOperatingSystemException ex + = new UnknownOperatingSystemException("unknown"); + assertEquals("Unknown operating system 'unknown'", ex.getMessage()); + } + + @Test + public void exceptionWithCause() { + Exception cause = new Exception("cause"); + UnknownOperatingSystemException ex + = new UnknownOperatingSystemException("unknown", cause); + assertEquals("Unknown operating system 'unknown'", ex.getMessage()); + assertEquals(cause, ex.getCause()); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/exception/package-info.java b/rpm-core/src/test/java/org/xbib/rpm/exception/package-info.java new file mode 100644 index 0000000..32704e4 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/exception/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm.exception; diff --git a/rpm-core/src/test/java/org/xbib/rpm/header/HeaderTest.java b/rpm-core/src/test/java/org/xbib/rpm/header/HeaderTest.java new file mode 100644 index 0000000..882403f --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/header/HeaderTest.java @@ -0,0 +1,292 @@ +package org.xbib.rpm.header; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.xbib.rpm.header.entry.BinSpecEntry; +import org.xbib.rpm.header.entry.I18NStringSpecEntry; +import org.xbib.rpm.header.entry.Int16SpecEntry; +import org.xbib.rpm.header.entry.Int32SpecEntry; +import org.xbib.rpm.header.entry.Int64SpecEntry; +import org.xbib.rpm.header.entry.Int8SpecEntry; +import org.xbib.rpm.header.entry.SpecEntry; +import org.xbib.rpm.header.entry.StringArraySpecEntry; +import org.xbib.rpm.header.entry.StringSpecEntry; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * + */ +public class HeaderTest { + + @Test + @SuppressWarnings("unchecked") + public void testInt8Single() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(1); + buffer.put((byte) 1); + buffer.flip(); + SpecEntry entry = new Int8SpecEntry(); + entry.setCount(1); + entry.read(buffer); + assertEquals(1, entry.getValues()[0]); + ByteBuffer data = ByteBuffer.allocate(1); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInt8Multiple() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(2); + buffer.put((byte) 1); + buffer.put((byte) 2); + buffer.flip(); + SpecEntry entry = new Int8SpecEntry(); + entry.setCount(2); + entry.read(buffer); + assertEquals(1, entry.getValues()[0]); + assertEquals(2, entry.getValues()[1]); + ByteBuffer data = ByteBuffer.allocate(2); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInt16Single() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(2); + buffer.putShort((short) 1); + buffer.flip(); + SpecEntry entry = new Int16SpecEntry(); + entry.setCount(1); + entry.read(buffer); + assertEquals(1, entry.getValues()[0]); + ByteBuffer data = ByteBuffer.allocate(2); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInt16Multiple() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.putShort((short) 1); + buffer.putShort((short) 2); + buffer.flip(); + SpecEntry entry = new Int16SpecEntry(); + entry.setCount(2); + entry.read(buffer); + assertEquals(1, entry.getValues()[0]); + assertEquals(2, entry.getValues()[1]); + ByteBuffer data = ByteBuffer.allocate(4); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInt32Single() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.putInt(1); + buffer.flip(); + SpecEntry entry = new Int32SpecEntry(); + entry.setCount(1); + entry.read(buffer); + assertEquals(1, entry.getValues()[0].intValue()); + ByteBuffer data = ByteBuffer.allocate(4); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInt32Multiple() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.putInt(1); + buffer.putInt(2); + buffer.flip(); + SpecEntry entry = new Int32SpecEntry(); + entry.setCount(2); + entry.read(buffer); + assertEquals(1, entry.getValues()[0].intValue()); + assertEquals(2, entry.getValues()[1].intValue()); + ByteBuffer data = ByteBuffer.allocate(8); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInt64Single() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.putLong(1); + buffer.flip(); + SpecEntry entry = new Int64SpecEntry(); + entry.setCount(1); + entry.read(buffer); + assertEquals(1, entry.getValues()[0]); + ByteBuffer data = ByteBuffer.allocate(8); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInt64Multiple() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(16); + buffer.putLong(1); + buffer.putLong(2); + buffer.flip(); + SpecEntry entry = new Int64SpecEntry(); + entry.setCount(2); + entry.read(buffer); + assertEquals(1, entry.getValues()[0]); + assertEquals(2, entry.getValues()[1]); + ByteBuffer data = ByteBuffer.allocate(16); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testStringSingle() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.put(Charset.forName("US-ASCII").encode("1234567\000")); + buffer.flip(); + SpecEntry entry = new StringSpecEntry(); + entry.setCount(1); + entry.read(buffer); + assertEquals("1234567", entry.getValues()[0]); + ByteBuffer data = ByteBuffer.allocate(8); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testStringMultiple() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(16); + buffer.put(Charset.forName("US-ASCII").encode("1234567\0007654321\000")); + buffer.flip(); + SpecEntry entry = new StringSpecEntry(); + entry.setCount(2); + entry.read(buffer); + assertEquals("1234567", entry.getValues()[0]); + assertEquals("7654321", entry.getValues()[1]); + ByteBuffer data = ByteBuffer.allocate(16); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testBinary() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.put("12345678".getBytes()); + buffer.flip(); + SpecEntry entry = new BinSpecEntry(); + entry.setCount(8); + entry.read(buffer); + assertTrue(ByteBuffer.wrap("12345678".getBytes()).equals(ByteBuffer.wrap(entry.getValues()))); + ByteBuffer data = ByteBuffer.allocate(8); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testStringArraySingle() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.put(Charset.forName("US-ASCII").encode("1234567\000")); + buffer.flip(); + SpecEntry entry = new StringArraySpecEntry(); + entry.setCount(1); + entry.read(buffer); + assertEquals("1234567", entry.getValues()[0]); + ByteBuffer data = ByteBuffer.allocate(8); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testStringArrayMultiple() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(16); + buffer.put(Charset.forName("US-ASCII").encode("1234567\000")); + buffer.put(Charset.forName("US-ASCII").encode("7654321\000")); + buffer.flip(); + SpecEntry entry = new StringArraySpecEntry(); + entry.setCount(2); + entry.read(buffer); + assertEquals("1234567", entry.getValues()[0]); + assertEquals("7654321", entry.getValues()[1]); + ByteBuffer data = ByteBuffer.allocate(16); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testI18NStringSingle() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.put(Charset.forName("US-ASCII").encode("1234567\000")); + buffer.flip(); + SpecEntry entry = new I18NStringSpecEntry(); + entry.setCount(1); + entry.read(buffer); + assertEquals("1234567", entry.getValues()[0]); + ByteBuffer data = ByteBuffer.allocate(8); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } + + @Test + @SuppressWarnings("unchecked") + public void testI18NStringMultiple() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(16); + buffer.put(Charset.forName("US-ASCII").encode("1234567\000")); + buffer.put(Charset.forName("US-ASCII").encode("7654321\000")); + buffer.flip(); + SpecEntry entry = new I18NStringSpecEntry(); + entry.setCount(2); + entry.read(buffer); + assertEquals("1234567", entry.getValues()[0]); + assertEquals("7654321", entry.getValues()[1]); + ByteBuffer data = ByteBuffer.allocate(16); + entry.write(data); + data.flip(); + buffer.flip(); + assertTrue(buffer.equals(data)); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/header/package-info.java b/rpm-core/src/test/java/org/xbib/rpm/header/package-info.java new file mode 100644 index 0000000..c9888fc --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/header/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm.header; diff --git a/rpm-core/src/test/java/org/xbib/rpm/package-info.java b/rpm-core/src/test/java/org/xbib/rpm/package-info.java new file mode 100644 index 0000000..a01ccb6 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm; diff --git a/rpm-core/src/test/java/org/xbib/rpm/payload/ContentsTest.java b/rpm-core/src/test/java/org/xbib/rpm/payload/ContentsTest.java new file mode 100644 index 0000000..fd84129 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/payload/ContentsTest.java @@ -0,0 +1,64 @@ +package org.xbib.rpm.payload; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class ContentsTest { + + @Test + public void testListParents() throws Exception { + ArrayList list = new ArrayList<>(); + new Contents().listParents(list, Paths.get("/one/two/three/four")); + assertEquals(3, list.size()); + assertEquals("/one/two/three", list.get(0)); + assertEquals("/one/two", list.get(1)); + assertEquals("/one", list.get(2)); + } + + @Test + public void testListParentsBuiltin() throws Exception { + ArrayList list = new ArrayList<>(); + new Contents().listParents(list, Paths.get("/bin/one/two/three/four")); + assertEquals(3, list.size()); + assertEquals("/bin/one/two/three", list.get(0)); + assertEquals("/bin/one/two", list.get(1)); + assertEquals("/bin/one", list.get(2)); + } + + @Test + public void testListParentsNewLocalBuiltin() throws Exception { + ArrayList list = new ArrayList<>(); + Contents contents = new Contents(); + contents.addLocalBuiltinDirectory("/home"); + contents.listParents(list, Paths.get("/home/one/two/three/four")); + assertEquals(3, list.size()); + assertEquals("/home/one/two/three", list.get(0)); + assertEquals("/home/one/two", list.get(1)); + assertEquals("/home/one", list.get(2)); + } + + @Test + public void testAddFileSetsDirModeOnHeader() throws Exception { + Contents contents = new Contents(); + contents.addFile("/test/file.txt", Paths.get("src/test/resources/test.txt"), + 511, null, "testuser", "testgroup", 73); + Iterable headers = contents.headers(); + Map filemodes = new HashMap<>(); + for (CpioHeader header : headers) { + filemodes.put(header.getName(), header.getPermissions()); + } + assertThat(filemodes.get("/test"), is(73)); + assertThat(filemodes.get("/test/file.txt"), is(511)); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/payload/package-info.java b/rpm-core/src/test/java/org/xbib/rpm/payload/package-info.java new file mode 100644 index 0000000..5d593e2 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/payload/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm.payload; diff --git a/rpm-core/src/test/java/org/xbib/rpm/security/PrintPublicKeyTest.java b/rpm-core/src/test/java/org/xbib/rpm/security/PrintPublicKeyTest.java new file mode 100644 index 0000000..97cebe7 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/security/PrintPublicKeyTest.java @@ -0,0 +1,24 @@ +package org.xbib.rpm.security; + +import org.bouncycastle.openpgp.PGPException; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * + */ +public class PrintPublicKeyTest { + + @Test + public void testAsciiArmor() throws IOException, PGPException { + InputStream inputStream = getClass().getResourceAsStream("/pgp/test-pubring.gpg"); + OutputStream outputStream = Files.newOutputStream(Paths.get("build/test-key.pub")); + KeyDumper keyDumper = new KeyDumper(); + keyDumper.asciiArmor(0xF02C6D2CL, inputStream, outputStream); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/security/SignatureGeneratorTest.java b/rpm-core/src/test/java/org/xbib/rpm/security/SignatureGeneratorTest.java new file mode 100644 index 0000000..7174f0e --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/security/SignatureGeneratorTest.java @@ -0,0 +1,87 @@ +package org.xbib.rpm.security; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.xbib.rpm.RpmBuilder; +import org.xbib.rpm.lead.Architecture; +import org.xbib.rpm.lead.Os; +import org.xbib.rpm.lead.PackageType; +import org.xbib.rpm.payload.Directive; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.EnumSet; + +/** + * + */ +public class SignatureGeneratorTest { + + @Test + public void testReadingFirstKey() throws Exception { + SignatureGenerator generator = + new SignatureGenerator(getClass().getResourceAsStream("/pgp/test-secring.gpg"), + null, "test"); + assertTrue(generator.isEnabled()); + } + + @Test + public void testFindByKey() throws Exception { + SignatureGenerator generator = + new SignatureGenerator(getClass().getResourceAsStream("/pgp/test-secring.gpg"), + 0xF02C6D2CL, "test"); + assertTrue(generator.isEnabled()); + } + + @Test + public void testBuildWithTestSignature() throws Exception { + String pubRing = "build/test-pubring.gpg"; + String secRing = "build/test-secring.gpg"; + String id = "test@example.com"; + String pass = "test"; + + // create new key + KeyGenerator keyGenerator = new KeyGenerator(); + keyGenerator.generate(id, pass, Files.newOutputStream(Paths.get(pubRing)), Files.newOutputStream(Paths.get(secRing))); + Long privateKeyId = keyGenerator.getPgpSecretKeyRing().getPublicKey().getKeyID(); + //logger.info("key ID = " + Long.toHexString(privateKeyId)); + // dump in ascii-armored format + KeyDumper keyDumper = new KeyDumper(); + keyDumper.asciiArmor(privateKeyId, Files.newInputStream(Paths.get(pubRing)), + Files.newOutputStream(Paths.get("build/random-test-key.pub"))); + + SignatureGenerator generator = new SignatureGenerator(Files.newInputStream(Paths.get(secRing)), + privateKeyId, "test"); + assertTrue(generator.isEnabled()); + + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("signature-my-ring-test", "1.0", "1"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.setPrivateKeyRing(Files.newInputStream(Paths.get(secRing))); + rpmBuilder.setPrivateKeyPassphrase(pass); + EnumSet directives = EnumSet.of(Directive.CONFIG, Directive.DOC, Directive.NOREPLACE); + rpmBuilder.addFile("/etc", Paths.get("src/test/resources/prein.sh"), 493, 493, + directives, "jabberwocky", "vorpal"); + rpmBuilder.build(Paths.get("build")); + } + + @Test + public void testBuildWithSignature() throws Exception { + String secRing = "src/test/resources/pgp/test-secring.gpg"; + String pass = "test"; + RpmBuilder rpmBuilder = new RpmBuilder(); + rpmBuilder.setPackage("signing-test", "1.0", "3"); + rpmBuilder.setBuildHost("localhost"); + rpmBuilder.setLicense("GPL"); + rpmBuilder.setPlatform(Architecture.NOARCH, Os.LINUX); + rpmBuilder.setType(PackageType.BINARY); + rpmBuilder.setPrivateKeyRing(Files.newInputStream(Paths.get(secRing))); + rpmBuilder.setPrivateKeyPassphrase(pass); + rpmBuilder.build(Paths.get("build")); + } + +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/security/SignatureReaderTest.java b/rpm-core/src/test/java/org/xbib/rpm/security/SignatureReaderTest.java new file mode 100644 index 0000000..28e940f --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/security/SignatureReaderTest.java @@ -0,0 +1,35 @@ +package org.xbib.rpm.security; + +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.xbib.rpm.RpmReader; +import org.xbib.rpm.format.Format; +import org.xbib.rpm.signature.SignatureHeader; +import org.xbib.rpm.signature.SignatureTag; + +import java.nio.file.Paths; + +/** + * + */ +public class SignatureReaderTest { + + @Test + public void readBadSignedRpm() throws Exception { + RpmReader rpmReader = new RpmReader(); + Format format = rpmReader.read(Paths.get("src/test/resources/signature-my-ring-test-1.0-1.noarch.rpm")); + SignatureHeader signatureHeader = format.getSignatureHeader(); + assertNotNull(signatureHeader.getEntry(SignatureTag.RSAHEADER)); + assertNotNull(signatureHeader.getEntry(SignatureTag.LEGACY_PGP)); + } + + @Test + public void readGoodSignedRpm() throws Exception { + RpmReader rpmReader = new RpmReader(); + Format format = rpmReader.read(Paths.get("src/test/resources/signing-test-1.0-1.noarch.rpm")); + SignatureHeader signatureHeader = format.getSignatureHeader(); + assertNotNull(signatureHeader.getEntry(SignatureTag.RSAHEADER)); + assertNotNull(signatureHeader.getEntry(SignatureTag.LEGACY_PGP)); + } +} diff --git a/rpm-core/src/test/java/org/xbib/rpm/security/package-info.java b/rpm-core/src/test/java/org/xbib/rpm/security/package-info.java new file mode 100644 index 0000000..0aed641 --- /dev/null +++ b/rpm-core/src/test/java/org/xbib/rpm/security/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.rpm.security; diff --git a/rpm-core/src/test/resources/org/xbib/rpm/changelog/bad.changelog b/rpm-core/src/test/resources/org/xbib/rpm/changelog/bad.changelog new file mode 100644 index 0000000..0f04b45 --- /dev/null +++ b/rpm-core/src/test/resources/org/xbib/rpm/changelog/bad.changelog @@ -0,0 +1,2 @@ +Tue Feb 24 2015 Abraham Lincoln +Fourscore and seven years ago diff --git a/rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog b/rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog new file mode 100644 index 0000000..6c3ab5b --- /dev/null +++ b/rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog @@ -0,0 +1,24 @@ +* Tue Feb 24 2015 George Washington +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua +* Tue Feb 10 2015 George Washington +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +* Mon Nov 17 2014 George Washington + consectetur, adipisci velit, sed quia non numquam eius modi + sunt explicabo. Nemo enim ipsam voluptatem quia vol + eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat + Excepteur sint occaecat cupidatat non proident, sunt +* Fri Mar 06 2009 John Adams +- nostrum exercitationem ullam corporis suscipit +* Thu Oct 16 2008 Thomas Jefferson +- Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, +* Thu Aug 23 2007 James Madison +eaque ipsa quae ab illo inventore veritatis et quasi architecto +* Mon Jun 04 2007 James Monroe +- adipisci velit, sed q +* Tue May 08 2007 James Madison +- dolore eu fugiat nulla pariatur +* Tue Apr 10 2007 James Monroe +-+// quis nostrum exercitationem ullam corporis +* Wed Nov 08 2006 James Madison +- Initial rpm for this package diff --git a/rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog.with.comments b/rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog.with.comments new file mode 100644 index 0000000..b79e19d --- /dev/null +++ b/rpm-core/src/test/resources/org/xbib/rpm/changelog/changelog.with.comments @@ -0,0 +1,6 @@ +# ORDER MUST BE DESCENDING (most recent change at top) +* Tue Feb 24 2015 George Washington +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua +* Tue Feb 10 2015 George Washington +# a random comment +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. diff --git a/rpm-core/src/test/resources/pgp/test-pubring.gpg b/rpm-core/src/test/resources/pgp/test-pubring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..ef103cb7dc37ed82d26f4a7ad5329b122e79c9d3 GIT binary patch literal 1157 zcmV;01bX|K0SyFM)72&c2ms{btXZPe{|Xdv=>Ao#(g@*%9Adr8N`H!W04^zbqGp|s z*@XictD`iQ3z27Fv1sf}W1jTn2iB$^Sq8T?r;__F`>eE&Z(`xhNL4EBE(TXiRo~o# zY>3TSf&epg($nk597bO^}7sCezZxF0BtB{1^^6ayn_!OitO3i(l-qE zh>cGg?L{fay*SfP-RL_f`K;TD;JhdUNiI&%cZMK#3bIqy#p{~}sBdSw$$VEyY6QRd z@5H()+@y%OO>I?oTCz%f$5ys5XN(`D-wi#`NPzxzdZJI!h8Vpsl%m!vKd3VkSPg!T z6G#5oXyj~BfRXh+c%CFP-TEC_b#J^02Om;FV0QZkXFl^bRV&>Yp+)G97&YA|3da!+ zOXerYub5gB&IolrcE{|;^=e_R)?LNeRxf*_j{Ug-4Fp-!)g}Q50KDKn-~}Z>%n~18 zdrBEeT68(EIh$3wVHhnAG$;(Rlt!iU$I8LT3eQ^3eKkwi=fvsH*IYPA^{)@yK+|p9 zVCPzWjT<})6Pqnj_6?+?=XXC_ag!E}S^cHEQxXk^~r593&|A4W?tHg**&&EIY7t8z9t*|>TjR|jV_YH1i zahr!vq_|lXa!etgrq^`2&Ouj6xm@LnK1k|oTzEaLF+YF&kpK|^00D^s9|RZy0ssjG z0$J15CITA_0162Zy3$A%@GNaCApQsdk9`zF!ScjO@=QS=uVCVH^?yj%3`CHjkIKJk zGLgCjYOqE$97?;DShk?Qz>Duc!XeiO&2k5L{8=-56`j^ub$oFxYCv+Vub(}Ceh$|e z)9>vx#Vo+tRUYnqM&ZwEg7dcQh>_AvL1)kz9oHM#$QxOlBb z5M8?7@eH-MF0=2RMSGY_53T|ja{-GpWAf;erf~RtILxajQddms7n;1$iS#Wib;Xm| zsI!ux!KZhFmzi6I*0Q09*NQ4|ma7;(b_zdr$@@Q2ge+ooW{Saja`3`zc^ot11vP;N X06tfO>76g)=txqDjyCF9Gv>y)zOoph literal 0 HcmV?d00001 diff --git a/rpm-core/src/test/resources/pgp/test-secring.gpg b/rpm-core/src/test/resources/pgp/test-secring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..2044048a0212d321a6926cc727415cf4da5fcf81 GIT binary patch literal 2551 zcmVAo#(g@*%9Adr8N`H!W04^zbqGp|s z*@XictD`iQ3z27Fv1sf}W1jTn2iB$^Sq8T?r;__F`>eE&Z(`xhNL4EBE(TXiRo~o# zY>3TSf&iUhPpHISL0{UszGqN^I?hO;+h85KNB|b!IQoPxV!I4~E!x{b#Z!|Q&Y$hU- zUo~!&XQI^3A@~np78SNnh>AAE69;WPX6tj9e5s?75%r=ny0;BQ2t#ma)pb5V$73CuCVEb`^|Ayr&&!^%>qw;3) zv_!~WG|m$(pXe5^#W1BDn^fJ(PkQ*2m6}~7$U%D2og>#<0UmD8fZSQQu3v~^Mk9;M z#;!wg#1}DKfxg$sFOs|k8?1U2KXN!29_*z?fo{UCJ`K+Vr!+GX)YIdwwgM`k&E-cNGfO$p7(NvZy}8U5XQS%P5ZtAA{uezQ)yhGQqw$EVR> zSvs!qu9lk&G?)cn5az%g%w)iNqwCEv%J&bSdEq@J{Kxt{#~l0vZ*cov^*1~3b<=p) ztDeziiI~dJ%F0Vohxl*n_xJXCBglCTkaQqJ5z>AKMX}WmctN%Zv=DSj(W2Ki|~xA6%%&Z7&$eSvJ%(vYi7ZVt0o=`;yizxY50*`-LdG#1G@Yy7jvZPJXmW zk^pTeW(EKZXS{dmy2*T3NooYY`0vEJD%_-qxJ_+UcUrPae8*O{FlUS(q~8ra&`5y( zb$X&t(S{hkFO;IzD?g|+6Icy?juS`z*l6TzQGk*4K6suaGu`?fS#@u`2?rliL11?K z24_C=HdQO#8KFh!juAK6c0K$MtGquGU?}*j6ulqmKQZ z1I7ed)72&c2mrj`KHvo^DCz zj^b-JB0$3FO)bq6qjF&64ZZN~sub^37!&Xife%Oz93m*y60>s7fBJ+KwkSS{qb{5{ z%{6NBIg$h)AJJdmoi9jSlNapOdIYvM6Q5q5y8=mxm`6RNvNRVe)uk6wi2s1G#H+-J zOwYzY_7}_h)UB{PE{zFqZubptV{w~@Po%h66>>}=pQhJzxz0gXNx59*iato{Yg~9e ztua4;{E+|=0RRF12?Gchy$jF`KlRJNK~8%_2;TSwdUfY&qd_D0%L@b`ucNqc?-;MCk4z1wWzvGr+w(>2eE)|; zO%f|hhgoeI==|bx*x41%k`|&$^tU_~p-bFFbbyR|R;G0FyFFxS^x2d8fDdmH7$(I; zcaE@d@Z)1PD1?299f_$5wzSL~6qJ*iu)lFLI2onQhLoonlV>3s_sd+@r3G=^~4FR$rfvQQ@4SGFlH;dc;D4~GtL&mo9JG!WiS9N(Uq6e}bD z-#sHdigow5Tr5|tT}$kyS$kg8GNh!tREkF7Bl7NX@b?gD8QtY2sjRvT9FyOWW!4e+ zi;lESrv!(pAJ}hBVTeyr9p(!DbPhaE)&zI*%|VMH(?MHq)QL0LGyuy70%uM4GET~}G)yh1qsqPTk|0wFSeGP0C=o7;; zZZ8pbiYrGnNzCo?|t4?M!8opwr zQ;B?t&ocQWTGqEWTuX-B3xWv$2PLw%csMRtklDGu|4mbJDUUyn z2@tx{NEYxcZ7d-E2mp_L6hp!C#7Xi@K_9PR;&b(XNZ1TSkfD#tziBd&x&&&lMl>8s zyOmhBpufP2??1vJ*9XmV2YCEhGkX=C)>(CYaV=^L=#!>!_>RB`9#<;0+*a-jt literal 0 HcmV?d00001 diff --git a/rpm-core/src/test/resources/postin.sh b/rpm-core/src/test/resources/postin.sh new file mode 100644 index 0000000..fb9746a --- /dev/null +++ b/rpm-core/src/test/resources/postin.sh @@ -0,0 +1,3 @@ + + +echo Hello Post Install! diff --git a/rpm-core/src/test/resources/postun.sh b/rpm-core/src/test/resources/postun.sh new file mode 100644 index 0000000..3bbb142 --- /dev/null +++ b/rpm-core/src/test/resources/postun.sh @@ -0,0 +1,3 @@ +#!/usr/bin/perl + +print "Hello Post Uninstall!\n"; diff --git a/rpm-core/src/test/resources/prein.sh b/rpm-core/src/test/resources/prein.sh new file mode 100644 index 0000000..5ea1cbd --- /dev/null +++ b/rpm-core/src/test/resources/prein.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo Hello Pre Install! diff --git a/rpm-core/src/test/resources/preun.sh b/rpm-core/src/test/resources/preun.sh new file mode 100644 index 0000000..5c739e8 --- /dev/null +++ b/rpm-core/src/test/resources/preun.sh @@ -0,0 +1,3 @@ +# comment + +echo Hello Pre Uninstall! diff --git a/rpm-core/src/test/resources/rpm-1-1.0-1.noarch.rpm b/rpm-core/src/test/resources/rpm-1-1.0-1.noarch.rpm new file mode 100644 index 0000000000000000000000000000000000000000..e3f535465eccbd15d3f2a7aebe118cb8d2216ccf GIT binary patch literal 34397 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 literal 0 HcmV?d00001 diff --git a/rpm-core/src/test/resources/rpm-3-1.0-1.somearch.rpm b/rpm-core/src/test/resources/rpm-3-1.0-1.somearch.rpm new file mode 100644 index 0000000000000000000000000000000000000000..4130276bcfeedce3a48fa250abb32b23bb055134 GIT binary patch literal 128435 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

*-`^)mO0@$2NC5mHGx62~40>UHvF?3UQj zyT&aNR2P0?YkvnIUQ|hG^?VGB`W#LqywH{dkQT;|p+ItScuo(VO7(loU~KbR*g)&B}1S z$IJvrxVz^ubu9hRB^v!8OHVcF7at?mSnk9Hg+Kx9V9#T1r;8e@B^0y|MenbI+hr(f z$NT^xptb&ppB-4?K{vdqA8%;3$W+&tFs(A!z?osuX8;Gn8VIC7P?aO9iZZKci3eC9 zE*rND5j`S@n0yPG2w-pBYBU*FyF-76_@fBTUnzk64~k>I>4E$%@$y%SAiqnzQ2!gM zV}GUL@q6d3|E=ipyTxB=di-v&{;R^r?}mS+@$tJMIPhO7e*DkYe-`~>2L4wHAio3l zR~jI{17-^RA1Z?U>iSnXaf&7Ou55KzpwUGZ0s!QPiVOjsLu79lq|J}6{nt^q*|Y{?YGWWuCwL4deVb^3Y#D=C3K( z@9t)}{?`fFubuo#%zy8MkoVuG>HiSVLjL*s__udL|D>Iz{~zd9F)$l2vi+O7Re*n~ zTV?l~4%{a_>p#m^{r{<71^#!9P3J#IO?~_2nUDYd%-^x0`=6me{-BynuXc0&|+Yf=Dn5B@)sXlcF%SttbbG2k)WE{2_=dIk^Sa96sy*awA7$ zN5}WaTbL~{3x5rNE&<)LYrn#aL&g;mu5_Cgsijk=SZ8US^z>wx_vg?l=hgnUn}KD? zY~dm)Ee=79%n(sS+#A>4NVs!MRWN_sUpWv8sbJVK!O+< zvjjh#6Crm76LFobI(c;ZX}7^}b=n$7>WMSycJ>rT;b(ABRuTlNAsv<|c1Tbis1Y-Y zFoE+fKSmY0Ch!y=sKpx<>s$Cg!*foX2iN?wMfejQ{Qm(S!LNEIe_sV0B3hw)WD$gn zJ@&qfRMk{3420H)6F~bAP%I5LeI+4D(G=_$?}5L{?F;wL%37r{Hdr4fGT9a;X7uO(O%Md6Epod6gljQbmm?{GOg{~uC)=C{Yt?s zyaKv7rAMhJz;&8}XR(N1a0t16S&9!FT$u4-{+^}Mip&)-?g%_|M<^15h>Z+Rv6D@Nep;fBCKdYcc%$PG2Oy^ap+f zVXy8X0%&m`7_DZ;rs~}l((b;#tl_k*5)#PniM7Als7RJ6(+_j zjj4f{#BH-C&a^e{6jUS7I(mCi$2|n@m~?Dua)JuLNRdKihYR8Rf!MP$2|F>tPb+|J ze{q8u%=|Ml@q$5^aX;fm_)q>r^iRl$8d@6KngISJk{PbJDnBcWqE%Q9=l3lLMG-|V zG+{yhnpRj20kl8I#~D*cz>UU6+{dJ($g1NzvZv1kg|x!}{FTxh+-LsvL*|0UscXW+ zmPPHC8rzPyAFrrg75q~nuxuMTo#4>C#Kj&ed}cy6;w?H0_Ccc!Sn9e z6`{`}5NOs4u$bN{3ROj02|laP7Zj@ckj9^GtD}wCtFDB+3ako3);yKBq)m$%Wkye# zF{LpIwl(!32Sk)H1ZI^Zn9elmB{L|?D{bA>bUsaqP5Q*o!%(+a1huN}*JIbst;_HST2XrbZ|A!Gq0EHsyM1sE1kSI@=!ZfBWwc(gmtd*gH*)5F&DLBaSq zDCylE07A3b4l;9(0c3`9javO977Y9a|xj3ez8_kwucA zSP$$G6s-NP0Thn(xW8r4^SVN4j)5=Kx_6DjuYf~@- zqa#+VM=&?vWelGKQ+3KX;7+lz*Txt~-cRA}M)`Rx?8-*u6b|lNKH}y$6{GVD9l@PI z^qR>zy6$tDNJ(Yed_I?FR=}d5n*Fp2gPm585=t+l0aDEASzW3RRa!ivzEp?y zykD;Cb`(Ww$Q3XbR=1UVDBsC_Aw1h`=Y9U?h`dw6V^_!ritwpOmZ>ZV-25OAB+gSb z`EYVx`6rF9r42cu2whIl-yv46YYQ?;RAiJwe6~T$1(_qoEGym;Z-xk&{bN>Z>TtoShrrsRlx!_e-Sn_K4ksG~UMDaGZfar8 z+3cf8ZM;|QV6T#n$d3YEEwWz*fk6Clr56V(V>V>yA0_5Sn{$zFPYTj3uYOeb49vkF zOcT4Nu1J8YHD^o&4x`6d41Kbi7Dgiu(W3CxoDw@y4@nux3Y_*KY}JW0-ECdlAPhw( zEUDM2XPXwkjVqw#AZ>Zy!SNPTT2U($nc_UFp514l5d~XRF<50?QN9R8bY#OV3jYNr zh?kT9IY;FKCw`%~wc=3yYy1RDWT^0>L&g!N+X_l3|JF{jIo$63c`^(7xuX9D$zZ*v-55N^)o7D9v~e)BvllY4v(~f!nI8O|dvV=cE%JhcgA0OtI)Ym|g5!yRKh(YE-1Oz3 zh=4=DWtR2T^g+Mv%;nUi%^|(%LgL1$tEQ-H>W51-G!cBRWS>`O3XAq0ttaBB&muvl z=$hyno#=vK{>((jQOHb+N`aZCqaK%bl8_9t7sM~+Fib=Hg+^@R%cmI-%*i2wNK=>q zMiFq}DCoDezBYT0_q?*XJtPrujxuRqu7GHMj52a2oy}Q%+L3{wuA#0zpbwHXVX$Mc zG<>|&QG?(ph5%iN5Sy$}V3#z4dst1NKLbK!{JH1DPcW8&f0Nk$Ga!G8r5>{Xb)x8@ zFk7~;QeweVGI*zdwH8VM4VJCj^as~2AaZD0`mhAw~H0H)+wQ=@1 zd5Hc^Lz5zn`VKOKbp^4)w@|GuOhvEznGt|4()qlRQYUSQIk_g?#Z~?tbtHhHhCG`RPFaTYq%Vx2fe6jMQS%J2MzPv{dFW%?hdd zM=55z7k+lg5&%)_5VGf7vZ~1Tg_mCX2|RFTr>Tb}DCns+ zD?N=Ug%ave$&An&P+p`pg1C?x9D(!px)*EKdoId6F%8TjCVp=9(K9xc13)PUI}-K~ z#K2`!Lt!=)+7a)&&V^kN#1b%?OC3JVy*Gw3<W-RNrCl$-|y64lK377s2D>q ze}kGdlmP$iypBx9zQOdk!=%;V0HtH<=3EoAn15l|L)K}Ic1eiRJaXw7BvLg4UGU2}YGpL~&+m<`J&s~(f} z!gY^1^%@$3HIQy6&5g}FPa(IEVJ#6ZDGCRihMs4V1u74y$4!+!X~4r5o`#kP zU=1}2t`bH1gQ{A=&umXkbi#c&SNAHrU({LB1w2}qdzy0i9R=F}zRZ|-fhpH}I`aP{6ZHR= zvBz)!lUC?E*c%Ev8e0BiB+9Q!|ID&^M$5k`AzXXHTq70f?&fwUVfe^^;?sLr>G_i8 zlNg#j(MXYy@jd~2#@VzG1oKTXCnu+K7*Cwpf9~A)45Ycu9!%kg9HdJ}{{sCH1&2{c zvvw4du|DEBwFS^((N4&{YLSSU({T)I6audt$OeWAU~4c7)d(=vP6%|(bnEE#-@teg zpG0-17`aV9Tt$UV$(u2YRxL4VZR0&sUvp#rBs%h#QUFNGz)d3`4sxz}f$h&dsx!m9+7^A$pG5N}M* zci<$cBM@f9iT!j)0P*G`3o=Ekl-CncJsb-Zx#2GM(jbiLLS(p@-1YB}+ z!w^M94xiezN5q|55KAY|p3tkw13a56}B=t!(2<|f$w{9Cq>9ZC%TZ1DpBwk7>< z{g#p@cJ_Zu1Ah(N+K%9~|A^fkeK9dPV|g<%ii2@+b3a44$3KVg^R!Hr77-MP?u zureLUAbbWQbY3LvcKs#<9v0UwYbzUH3GvjpPus@F!F|5hXOWB(me|NhAZ|(LZ63Fr zw+AnWyU72Cxo-@zY~8j@SK2lzZJU+0ZQEw0ZQHi(+?iEr+qTWF``+!2KBw=yuYdH9 zj@Ypx){58>`&)Cym~)JY-3K=ru{<0PZ32S^qXIJmdzcLwe9kzZc{!Bn1!Fe6=h0Wp zEPmySD%lgau^88XEJmMYu!TnCCtbo9o6E~jShO!nLXYWLFYf_?eXBB#iEZU_b;OgT zb83W6dJ|Sbx7gZm&vEF8X541o!j_Eo?e!aiP_yb=X}A@s{-E&0FqACTTrCitsI`oy+Fg9suDS9UK_BcgyRA<5uI!SM

_r)Tc@qnNNmgKoFEcp0o+W`%mLzID+eKSW~RWJ;eYiQDR zHop&k3i{MX3-xeU#42q!P{?eR-8$znqfXpOnSL!Y%aX{*@@eUjh_M)gu}Pc$fU9vP zv*yHmxd__qO`t!M0OkLw$uKcT<;Iae*dpd|2Ki&Cw@Y9Kpru83f#I`^`>^0o|szL$Vl5VD#YAS zNE@=h8r&`qLTe8Fpx&7z9EPATlxb2Mn+3AvclycxIJ=YGe*h~kp`E}8_88X~#8?S1 zoCcm2mm|n({aO1=xIjal5{h{8J2zzek%=V3UUowIOo~MvJ5g z_aL4QZndHN4ChH1lc+RHt#JCDuzS`>Qv0S=td@XzhGtRf%_xm{jjmlLCdM)R^(7oK zT?h&pG&`})g$T!L-@i2(!cjqC*8j1r&mU+Z`>!l3VQcT={D+d87})$1S>$Ep=H*d& zTYa*zPAT-z!LYD2F#BSK#PxTAz#$UJP;_94fTLz9W7V0Pisp#kfj)oo3)qi@MB}i~ zy#s$jeTBR0hSo6-lldIwcwKcKtv}s8JrMw*E4bw|>?lAX)-KFp?p-hO)sv9E?+V*&q^28HDa08?<`3q_r`Vxq#uS}Q}8@?a&ey?uiHH^ zio<((P)|9#Uu(+~U8dIu+@4QrB94QDSin?evsRS8y%kBX;AKN*QCyI6GRODn@WwQV5%UQtwMC)Lhe`{YHbmu6o4T-bGjVW!E^S{W+XZYFPLP72{(- zU-=@Xt~D0p5^^%4M4sY4ITDQ`g6TLV|Db$vw@~Y}oWmsoP&$%d1VkSe3fV+Hws1bc zt!S+SxaaF4xrBH`aA2lImMrz2RujW7_3N%z`Vd1nXM{H54<{kgZr;GOTY1g7aW#ls14o zi3LmWVCa5xhM%lq1u9%YM6mEsvSi{`mAqxV?*X1`ICiU%i_OH9HH(`r)tk*x-AXQ2uf03y~{Z29G>%3|H)TW4&8nklPGBaWmmCz+I$!#7x%1ynQfWH^_A03`BcO zHR5SW_9`J!R!p12E>P^oEJl)KrEVbdplOE|DFnF&h4twsiAKyAKPTbutZf?@muc#L zH&WJVz;2_5GLxc`s)I;!G5a#Idb(1jOXea$79TB;vf7rtX!97Qd-Ay{=5Z?g!k}ff zlPMFGK!Lf+GSN?RSaaoL#THpQt#LoLjE=xp3;I&py?H-XZOL}s)bo2&9DVU0*G8vZU|VR7*% zenF=ss3>W!Wb}i-Sdw-sN<)b=XVP~(q+MEUkCBiX$z z%2`RP$<-_*6n=ZwF={*oVgz8(QS)FnP+Lu|fF0z446HHGCA&DF(UH)FUm3&d2%%u^ z1PdevmorOP>k*Y~)PaN4S++NxyKe0tiU8ZL}X zlDuEVsdb)|#4nYNRY#>sD0tz>O%BqZWm>JM>w;W2m%eUsrLo$8Mlcg0!dglSa~L2A z{L#1|5eqsdcpu}wWuw$e!T#G<M62{|UGS~>Ubvu@0H*2M3ZLvCl|YI&NMTUL+2 zCj9HJ%Pe^t6jE1|n!rc1+S@U;6nWs`+V;WV54R2rv{HrX<4C^X=S5du@kKkk$8AK7 z;@zK^oPmem4Q_Wszw&j$hs!T>cMw>MJIAi?Jzk&AKcNdZyhg9_pISVB^mNAB-*Y?0 zjY8hPcJUJ~pCP>QP7%trc;*@^k)6Z5spZ|j=Hz=YoqI)0OchpdGPSHfg^keXR292q z`638J1i&k49?b}ak_I?@mQQ~`;pShxXSM}6r4@Y*vSwtDz-hORafr#`71`qE*F68a z98~C%4Hlh_H4=U_WTxAO%la8ARgp5*h~F6t&2(JcDZ?Srsk_Q)!d_Ur8pX|Wn|NUp zTNgaGA$btP6EbjT!xM{7L(-%w9TWfxE<~%S(OQQ_Hc(fGLCU>Dv0*`G%pYuLXir=0 zN_EQuj)0Qy*vePzZz;)ho7iu@2CaD-F!mVz4RSjl&sq#nk4|jM` zH3* zk#CB+#pWx_$)~J)J?6>mQty~5AgKV1MRMk3Io*v+eQ%t3qBXBJ#Nq8+ZMKS zDi90X6usY|yBDP_wNk7pymBodAoh$pcEIo~%$Z~5C9gbLrgI!! zAiE^oo#JJN<&}M$8pbDhKlbO_CZ$%Rx??_mSQoO+*~fRny(dCj6jwl^mi#&JTFuUo zLT{?mH(wJ+P>o)uDfxfAQ+n)m$^PdGPx zUZtS`{2812P%ornJAv*FC{3~6NQNbCp6EZ5ZVrI^2}T)aqqDoq>qpHVQp6Q z3RV_m@#FrZ31BS%ZK9M*+aPPP#w$$?2pQvOVLcnBwl%9ajTzh=ZeGNKxTawXgcVu$`2yicj@Kg; zV0n^M)r>+GCQ%s0OsC&DzIvPYLc7*Acui%(n-y3J=*4vyNIcT362DdZJiVRO|7 z`cgPdIS4<#6GfHFbM`dZc%a14XL{~L=2otfFuD30VJb}vD|)ttDRWrNPc8QHg^h_T?Dr=^C*+* zA%ur ziWPO_1{hFzgMlNI-un>V1w0bN>BzbaSVE3b`h{VyjMzAsLSkZ`8(C)3p0^RM#jxx; zB?5>U8OK)Fa-2LbyD#2G&TxMIK%EuWH^RndJE%(*;tRHcm}u?TugY#B3hCfEx2psqZq*!OHXV58lNKQmWCVVBL#XPJ3Zjx(S}e7El7 z)ljUmW8NoM8;raC@lrcqI~%roSWxqtxj`Fqc8!XZVUQ?>HON+q)7{tl(oQl)B~7(0 zP~lQkUq}+36hkS^jOlj;l+7QG>md8-vH>82zJEu)Co{TN$e}7wwulKl1;9pgRohz7 zdPENEo`8=p`-zVKvX~0vr<$l5I;P1JKf)|p8w8O|E6jE*6Nb3^8G(LmMx`l~GS8H7 z>3}BT7kzC~C45OXk&w%fgXo2cO}1dFk$y}>#AKijSjBXB?|gQy;wV)=`&(W9t@se` zzm;nKZsz}2Qq4cBE&o4CHNcLeAfbQWi7e>9Nv;3+UjBEfM)9A25x_Sph9;<>EFAU? zI64}?OaETrUYI3fNB0y$$}+qd(Km$*8yR7^k$;q9O%*KAekI+5`RZc)+}F3e8Rm=(S6C_*TSl%9!{B#?UT%9nak9s`wR4s=b<{-qokcG zj_#(_dR#txs>WbRxDfqjrx7e<*1_G>?ohH;yXF!J@ruP6*2esmkT4&H<8>UJknHr1 zBOlvvy5gG8D;j<{rtezo>HEz4k;NX=n1wHz%;vqVfYa=p>N}Vp(uwJ_PyUGVh$6E7 zQDFf#`&bQ)##qo!OZ{=^td>)Z(hDLr88$UA>s~?&^HnLQ{+ENofS~#G=Pc0yOG=LFPe{LMl7e9sihdfWEbiEw;evZ-Naw&7!lE7 zn;o8sjR8u`ST#@=a<1``Bu1q4bULx{?XvE4Xeb@MGxDY(N{b||xzlfMhhwV|@F-NW zZiekEPKJ=#smVCyLzR;lk5FJ!vcLIGlc7wMfhv2H&c02GJwW~S^a0sLuGW8Y4P`_i zAj<#d^lFX<_V)j4m7c1O+aEgXTOuPNfdO6Y4UO3|haL|7tO6!UsYq#KBN)N^v0FC?;C6Y!x0S zlNm425BD-0DH`d_8%(W(lR;`!OW=<*V_uuAR;(up3Ci^glyQ@=O-eZxV!Ln|Ji8vY zoLkrCC*A(()_|tS6dH8lJaLpQ`&cfOclLeWV$-GQBDa5V^gOx>b2|9Z+ z#p{Ha3yHPNIM*3!1$|R}ZT*MczVqk|Sgzp<%|Q}b3srE}5fct%)~umYLEBOZbCrbI z$95ae^XN%#t3`^!CzIHY0h-o=0h%X~x5IJCK<=lL#qJ`lw$Refm?L#r5q;8TM^%Yi zzS>>42COPn8#(S8jG;;K#y-AQ7C=h&dHC=`cxE>hY0DP!izvNLDmre9C$nC#O755bB(6$!XTIZI(VEQm{XS z{Mfi%mzj4I*@F|evp=%AKzgrHxVD(^Eva<0X-iXDI?kZ5oMQQ>DzmszB^FqzqwHYw zemRrYdOtJqmN!|Qlft8WF>&P3Df&s!s_xk`q&4JGDeYVf9({JIMW;eJ1E9{o zd)aJT0hi?(6QBJWEQjS96#;k90hDB&$6o=|h{I$XD~pX`(qU>h${z}!cZ}gT1Aw{Y zCd(ho_7Kz5VY2UT+zl85PJ6dK)dA24<398AcacYqse`@0;BH%A&6h*qtVvyyC0Gj` zd;H~3Q=i<%vuMp|M>?2^N{WOAmx;QYa&h{FX7sMQ*vkRxIpjhx{)z34elJ;lxMB}HT_pJ4>FOi!CM|frHlbT<3y(oWzrk-$3IO-ALBWu z56SJJD;==U%-=8$9$NSRd=zMdsc{Q2V@?e`emvC`fJ%u>3jgxdH0I?HPSpXnFs|cb;kL0FAZTo#vH`em1C1oebPHIrU3|7w^Gs9PQ_D8Dl_-3u*)8`nQ zmt1P}=BlO2vYMvVp2NaiC6jllNuCwm@HFOOEvL zHWBe$gYFI5ij6?c&eUDm1yHI%Mq1Dh;Av)S6-n`|8K&vp&@`uZ8P3#QZ)r(gXeGnp zP0ckGrb;*szDswWGS5oOZVdDMZJO=nfR#O?kwYLi;hAxvLI>hB;@>!F+jBT){4#6Zx$p1OR08xUD+-DHWmrM zM|Xzo61BA+z-F68*C|7s=qVW-&|ErEn-09R*Lu%_HM@!8uDBGIac(m93U7gBOxqK7 zn%ObMi^~epra-W&x7lwu?`h`p%MZjQMlj=79kT}2nO}l6ki?cA3;GJQ%2_44o={)2 zNV7*kK^FYJ^l5gDOFWu&UybWK9~}0h7x(SZ!NC;6)F>yW1hMtUzqf_aJf*f}nrfSp zg}&o91_UgkE6l3Po-P#W4t5MfvmKJLr-yTEEibku&UY@)wZ&3Zg2TJZ?OcERs}Ut; zKXOn0WRJwq|2FIRPb2;{@G4I@tqG%kRU3`e7k>vh%+1PYq_l8ADWeK$iBs6f2*H(5 zHcOa85kxd7%*T_)(zvbS?~EhaYVQm%FwDt9j$b<%9!L9y?ObT4;7Lhx9CvTD+WEbC z5qQddejKm^9pxV}hD)MfMQv~JD3%@CV_s2m1mFl&K{_afJq5Rs3d@9bAQC{g>egce;@KhCosE_K+k20a_?Gj&PF%mQ0T7Qn!B>1z1(PQuU+7JRo$-ZW9TyH zOz}(KQ9c3MN>>SSUfKA*d{c37jdL*!@pSqcB9kJt!0PO!@-Zdg*UEkdbLBfP}^MO-MDUV-N(EvIY@&I<&L*#xlGfXc=|;V_#1m) zN)7BS3^YHz-W>3A@#hnVM{U!G=^8o85)Q0c!t+z-%*6YwPawNbq~Wh!<`7(>Y7cbY zQTw8|v;#}_lj7~4ws^?(9WhgQy`y+@TP4n^RK!(8ncT2d0}P9I<^1NO0phHt@XKPq zZ#2EEBE4A8@~^EVSS>GE5t2mdP8UgSS3Cls0adO!Fbc?Gf`j zU%1sKwb!}^t34)GczZ(RAWM*uvwq7GvQM7s;oWpE8EcQQJ=RdU< zd#5IOMGwijq;@!dOBjn=iYuLyu)ILK2iqUrRPiKYa`;;a)C_6OR}F0|Unh_~379@2 zG4Y4g4bHX^8qCN102;j37H`C#rn!`=7uXjFmo>sNYKwtVemzF};R%StPiAw^m*yLg zLb^Y6Wy}&n;GmDSEIRY~DG6D&O_P)QbDj)%+Z)kN7wD3Bi~;lBQ*Zi(!Tgl*HbM|H zoY)#b*d?HsK|?<)R*fB-9BIgk&{q%>E446BcRQoZOZdS+q(ZN>2g!67-mX^gGdJ;* z82EaD@ChTqu~xM3gNf}41H}k8TJbgFehDgx4xDjTJgk&u`1%8=*v^o0h6hU0-GY9A zBa-12ozvS*0Ly{}z|4hea{q&dB*O}L0I0CEAj&mtW~!b-EkPbyA&DRIU`ib|qY8?b z1&MZft0?z7YE@M0cesq%n-fY=uj5>GNjs_9v#-C=6wC>BEB8NlNkIRq!BNS=*38;O z+1$~@z*xr4*u?tpkhKiw3&HGLO(9PSN{{$;)@c$gz`s+_uvHf#vVQpY&ZSpU+aUd(8^4dY9q1OQ{`6ORWyGWGrZ`~>YYLEQ~AaKKf_QW{C%n$&93-zoUHko@T?1p(5Sd^Fk8s00mD zY)M}O6=f&cd*ME7l)604{1CHre5;BcrsKlP-bPlZ$N?dkDn&bDl~Un<MqNXGW!G#q>vue_StYH)CfN%-XAodcm`)IgCVffWIU6|pmh$zX%0#^q7D5`t#}2tUBC61!{|GNLHGOb7;aHvGdOu=h)BtwIaX#6I0I%pN^mqbZi#90MGnd4Y&+EZ6+`0-yRaMa@78Z#d&qJ_%`bzOz4h-3o~E2*=Z}QQ+Wl z^g=?O#9#?9GX_`Uz9b>z72gT;u{!J0L_><-u?3_aifN8(-U zYEJ1trptcV*;!eb{7+7N@{jc&Uor`%B?dzo$QG1@nN8n|4H_e8G(i@`DJ8AqJLWB3 zAwd(*lmrF)g8|?eJYw_c$%Wnz&pP~P6 zp4wY%50}K-=#0{1D%1aPoTM}`z;z5RR3pzvLM6_L&3L_}Ym5y=ItfIf7m^ZnoaaWF zo6Mv}FbJ~_)C^K(-U%5LxJ?zcoS9lI+L^gca z=csdo*HRFlm@Kxaj}cO>vlm}(GuBS-^uEusQM9dFn0rh~z64dldbWB3GO3*_vuMfI zw9xO8b@2BEUTio8lY?0#L!3*uRtLwio#Uw~#kOF5psbg-Gh8?anKW39l2a3o&2N$d zVV5qO*LxJ}%~$AfR#;MuL{bUk8XDNlo+(A+6@Jz8#jkE~O~qJjj=9bb=)jU+mDKd$VaAT}0 z0JVUUNlL}z1U3p&TZy}bZO>xFx+>=gY(&Pckce}T1$cDP8}#ohEHbF;GE0vSlCfh`&L^6uh8O4pm>8tCuv@z5v1IsqTa=2S2jtzY8abb z_S^Wb#H2Bb3_2kr_kwH9<&#CG89PT0u_qlM=z*jBn;DQZHoSh1e#oJj#Ffao|}^Gzc?^Yha}1=82SAnY(5%e&bUws56YdRpYF~S^+)=t)Ohx129w-) zCE$~0oeyxyvttLhINJsvXG$2w#l@N41xtyAEk=QbFUFDH5%r7mzI4|GB`jz7P+1#& zAVgUkY2Z}3OBZrX@g@yXQ|ZP+C@nLhQb@dLcL_>R22d)u{i+#N6!Q=ICsw_t3E%ba+zXWsFhp|;ckTp2r82y9IuR#19mPFd`m z!Vpo^F2myvWX_;Mf^OjyV^*KTx*)r%g0o1|w(KsUbOw~O0jNfPSbV>zfcO~L5A9vt zQ1Q(m+gPGo`~n2TgSrfIB*4%mc*9rywR2&qN%$%lW` zc#Wa(DlX){FDw`%Qz>?ss6dDn(2DHAC?5Y$JYa!CW#(N75G>dzLsJ*jZy@yin9^oS zE~sV{5sj<`?!QX!b_57z76jS&!7W69QnXDy&(0iBUA z7!XfNTd3DM9`Wgge(byU8OI=>SKh7O%DbRu(MG;v8j9-G3623oK0b_u4U1M2aDEx{%dEeGb2 z-*Iq$0MtEGhOs81%yMvXASE;hW=mLPWCHw4G*-)k1=NvO1_-mFX%GonsN)bNrGBI~ zX_$A!O*?IQ-La(XAME|uVt?9j+G;;~f3kk!@p*fSodi*r^YRHZT#W5@Cq@9yQn)c8 z=n1H;c+NneeeNK5yDmDSeUs~F_rBiC7WQrW6FWJVBFx(oJ4^3Ch3!qb;OhkJi21IDO) zOLo!FzVdce(7wucbCtVFc5_wUwE<_yo4sYvYG_|%Q`c8izC^iSJ0pI(sgBRd0eY3s z(c0cWDn97ZdXqChwcx)>FaxokqK<+tuX??Lr~diy3`=$#C`A0otwN~v5WFOZKMEU7|uXTYC428TvPYY?bZEF^`d zDf;W`1b201iE({qWrK5G0@9fv0zaH>xG=naSdauyD$2&NOfTNyeFuKhr8hCc4L! z+t{7iJO&7cU|?UuouwhnvB+%eP^5yFzbk2{YbXP$Yu1g616(c04|5$r

EbnONd{cE&on`$;_ivluGe(@h+|B%$jMC6AbN5TRGR~*d zb4imJ%UayKeh`F<&Apn(+D4I!t?0oubJnlp$C@%_o<1JY6h8zy_zamhvDG2#vEaM8 z(sR>ON85MNAx@*#!|X}XQ7 zG3-LOG0PWGw=i|m)G(cBgt}}M=RUfn9sR9}kV*K$r^=OqD=8+n+|lFkn3i)HEz6(~ z_n_69gULK>LJa3@H>J_q5P#rUb;-i+N=EilgvR-sQtjSZdDTDmL{idiG8Tl}{go_w zzNla5;TY_W>efZNfGP@ae0W=*hDa@JPbxAOEmM~_`!e33>bi)QHd0ukM(qtA*D!MI zxItQ)B0CJ9I}c$J;r3eOXqqm)OzWsIm;VG~%w-p)I*mZ@0(Mm5myAY;Qw1Y6M6SmH z_>|2s6d< za+`xz>uaTzpSK{l8GZsfZJ5xHz-G(BHvx>;@REGd83(eh$wyu0GTNpUUTH~kc5EvP z_6*z)%UHERfh*~W4<0+)+TiAiXaa@N&RBx`Z|WJp3FeO-%hTOwg)&iBt7gzG7mlG< zM{gJjc&=;=CfO<$&2gHYY-NwrrJbeOWy%%4xiqj_bc%ADmkQ}%AJNx#X+fy8!dCDP z5jccjaAE%b!y%-dqdm*i1_#}R3;8xh&lQ3h zcT;@Wx7BzC=%emw+hRttqYxCRluI;Y{n^c-W&t0rjC;dgia(ifChg+bjc zf;m~Kcy=~7c)RtOZ=3Y}6X2uM^+~=lgvol8%_Y?)bJ=T<4p0p)x@CG5J%lJrZ>VR9 z(GTK0>GjMtBetPM8r%gTIlR9L1l%-aaaan5jmf;Cv8;(>cp7)@HQ|O7;&qw$B8%&CK)`2^gp9(uO_-6|Vakr=Li!u8t zrd&SdF(4Uz7(#1IJ!wMWnRs)4uruL}@dt2c5XXcW0l3<9f2s{kdTB zt;F7R{ak7j+hZJ5CgBgy&3R4C3zb$46sv9L@Rz`6jRY3E=JV|^83brM-jVMFye|nW znL`9jzcFY7Ke6!8UW)f|!ySM{3<8|RioGBzV3a8IPYc;tORd0B@y*=5Q2`tieFTHp zJTaQ%hM<#am>D#@Y!IBC@JpF(8TO!?QwgYSzoWhNF~-z`5w6Fh5&k5a%drTIfb2k@ z<1u%cUyVt`SDzkcpXueb>z-G~PHV604YhT0{F5E)?=-w{Up?0n=G_nZzgTWx)EL}4 ze(vXc?<){NmqU^&TU(a3K5}78W}_spl8^4?y)xLE(wgP&rQEIdUSF)_J__agP?S{G zB|RF!=|pRGK;W1^<28O&9|3gk*&GPW*4-E`W0^1I7%sW7o}h+(p14EaTJCn~d^6i} z4cFzEFRvIb%`l%Dquw}&Ki;{6*2ww!&fWcf7XLP1Q`HOTW092Y`+=}1S9p&)+VP^X ziQ=>?{xMupCk_M`i*fHKLKIb}clJZmo^f>f-CN^&-lA<_O zUV>i)t~&~`JAlj+t@<9jW%f+AD^xxtc3TdM*{J{GnVESN=UPE`uQM%jJ@-zU;b^3R z4P%PYjnAsX-$9jWQ&+8VZQK+V_!P@raN@j%8e)9u3F{5-Z!o~QEK3rvb=Jn95u z_5A=wx%IhJ-UFNUP1aDQ*bloB)XF_=Skv^5%lZtxVS&p!VoFwS^!!H{a#;bauR;ung1=!sSaAU|njvLM zgh?5)iaNO}A#s2?(!w&aWt~cuXsmjmZ$0`_m`j`LyrIxM5ui&<7;Hos)D|_mO+-5& zHteBINju`(5Cji##&wW8Mh|_)O~7*4(vXR}lD6!3O$t5h0u@8%g$UaD@0u2=Z2_-C zs;l&?$93kVXCk3k9<6uD7=qlNAR{kyFop=1eIxBHj?yM~k!G&GH{+x9$*|r7Q7Zg+ z!`U4D3K{}X>i}kiqYEzw_Ngx1r@gjNGiWOD5xraKgDAef<^U1|6>a{n2s6IVNyipTbJhw2MMa4`(l+0l+^M^nw-N48 zmWj<$QMNK^X(boy@l9);#{}aI*(4Z~M`77u#HQ!90p^u!&dtT?8*0;$0URG81D zgUqIfEt4fhdN#=bEmI;A)X*N-p<2>}G>YcP7EKYbu27(fyO1#vg>5?8p?kX!>m-}8 zQxP~0xz9lO=mbL&H$-z*YK;%B4oyBGHe_#sO0u*(2lE3;EwwR5tbAj9$xk;JH%q3p zuujDNPQqFWtU3GNw=q$B&`d&BbZFh2#9)JWje^7NVyCd)&aa7|ZQ^@c8bXwyqfAFF> zVM60=4rS0mHmm-o#^nej#l~3#tZD}}Zz;q4u4$AVOyi=aIt&Y+aE z+?`iIJMa4Dw70iTw3^i#lBQB(fz;k5--7c{)GEsMSXXprDZ6p1on(5i&Ui$p#C=IO z&`Q0g7_HJw5g`Cx&e2MFoEET<*s!iwzF7u8Je1k)SVwd(D zczDpd4t01y2Ek;8+`IkKjXUH77n+*On$NtvhQk-=LwiYy+Tt#2^wrb1K<+Ss@_>|H z;ddQJIXG2(S+UMG*bIWdB>AkvN?{MKu)lkSxJ0GgshVl7so{x)QA+QC6$d949atGs z?L3>LF=#PN8oH(C5a`=Y6K4H{z1D~w*bgm}S+>Y`K1~zw~Ikt~9`Uj`ZBQQPz zGu-3z-?ySGt)dZlg8%_Vf&b^mzkg3q|I!Z~t*oUsCxDm}Jewkq8W#ax1szaMfpoPL zIvW{Xs%@=FaP5z>W`iT0m|@LvU;FZp=2DNWbK82Mz51gfJ=4d`jTckXySt?)0-!q3 zE97e)V0z^1gr5zW?Dqo>Jqopm>SZ};r6s1BvSQ3>W}MmVV5TrNd(3kNxzgNu67C}U z9p3V5dyEq9GWs?03sP?ndqU-^sk%{_l&4ox!_znDczC0?z-No!d?fW1CZf4z1Qq*T zdNt?C<5Zk-IZveT%H1rLN?fqctO5`ZL(U6+n+Ipmo+61?raq1P^aaJU?_0<02^SLw zFeFzWm;KaeQFRQ!e~>n9hY!)^MA%^W%1?;GmXRgWa9ieQJr)|oH_>S&mGm%gsJU#? z0lAy4@G99`Q;$jc1%xwljHQPh6ASzRVJM~0iG9%?#rA%^o^%jO-=XAWgB?GN#D+5{ zv`OTdp}N%sRRs%pX~J?x$1y*Oq@bUedO?>bKw&PsDzxlo<;i`+Vf_fuA+6&v6>tr7 z&Gc;O3&DgUlG}mr;q-TTpkTMPrFtfpXdB6RB`AtI(38ct3kZfLTc{&kafoU-dK^kJ z^eBMTHodP1EmnzWx|%6DE;nrRUn(-8KA%eXl}Jvz5c`j41)43Edvii|Rj3hKxgy}WtiNhx?SMe$+_6rqAvvm{JYQxc&FBtf-u z^^aTe5HjWyRSl@>5i|)V(ZxfG zrVi?h8aZ{!>J>C}kt2#m9;3UB^V~?z1w_*hgAUpH%KC)7jkb2nnJXRl>{Lv@QUdc* zvWYdq&~I-oek=yYk2eUs1*s*0kelJE{b56uvwBTx)J=}2!_>>kCTnibe7e>Mqp~{s z&-V20+iT4k_hMjNALb(4qeMn1$G+ML{;$G>iIce43B<$IqVl@y6$;O0MpM=RSE-fY zKDTvbEYjcR19v$b365t{X80%9@xU4c)^3Tc6+wW8a~4Q?BvDzC2F?sV-r<{guY^81^KY&crRSx$jIcVi-)>#2e( z)S@Oije(AnI(J~QBW{sB7mL)&6^5w&K}Qgd%-*Q95Q-w~Ots8~*h)6o6$%ubj> za~p(QO1*7QdBgH|%E0Q_P}hPv%1VqdV;E(^q6@P3k332CA!BDD78UDHRb~bjT@62C%}|?b5>7?R5mZUDpI7v9d@~`1Ny!0MIRKirR zHJQ4}raYc|eT>pHxo?Bui5>~clm=F$`o38Nn}%JA#^>?p8R^^O?0R)bZWCKDY2ytc z!S0}SoW@kyL#6s(pF+?$41%L62u8`L8&(JbH6B7e_|H6THH?E+)?@zY*ZI=gk= z-rI(QT{>uwYKNJ2rzG57h}64=G%5pD}g)smW$ppbf_wRR?yuhsd5 zMMux`H5V&WI$$j^k@!}|l&m1?FG80J2@O$V2V{l4a>xj#ifs*u(M$60g%{2tJmN{$W}@E+quj?) zv_4@91LHzvlORW0S$mndz+{R~$%*ehPE(5Q+efprjhxlXjibFRemS@Sk^AYMOs;hV zsR?0m#xrzSh<4^tk6LasVyjH-@FYv5i%II}H|de*^lhMsj}cG0{b_e`ZzIW za8vWdRvtDWL`ff%jmbA4$S|SBwB@LB_Rt?m$HihsKj4ltWNYM718kUObrSF?YA|YY zc1vjZ*icSY637Y$OjOkFe{b;8ybdINQWhG7Nit2ld7FCs;3p;AmXzgY9>C5pW4Q}$ zX*vhlHR&;M*>03GNgd7!kIuUlB*1@=78a&Rup*1n2PP=`0nCNmxNAZ%kyJ51Zh{iY z?(!eMGGYQ>c{;8o3O3N*ULFjY5!9B$$fX60iEW{*M>Ef zf2>IRMSqv{SArpAvtuL!7ta00x0YPJon9TyH7T3h^q#7cK4xO-fe}*r+_Do zE&}hqUB+v{NCbZbEpIR$+Z%;z#z^vOw_LzT|C5$Ch_@pQzy1y7bs%~!1i$gk;I#1~}zlUcG~L7mlpD?_9E?6x}I2jZhXWkeZ5s%zj8U&?c~-yrTbxv$VWA*z`zL zy?;@?vR2T_t?a(D8Dcpy{Ibo$)A{kF_g8#t46kv!0|f$_hxu27OaB$V{WbSWp87{K z$4D=|Od=bWKp9vQBskJ61a?@Nc-^Ntj7+phrXs3BmT8UwOZxBhbY<0!x$H~lQtlP| z_xgdh>+MDU(Y_}e7F02ecz49A2}m3A|uCIGuOiI_OKF#9q2W-tupU31 z(!EfWEU4V^hrPvlv`NP$T;O z(^?|ExJ8LmumdHn@jU4}GgH~0S^isI1{t@M(r(suD=Fk`w7nF8M{AL{M!I93NdUj?O~^KEtr_I(!It?O+K;^&lr%=O7c^xb&&q2#qI;^+!B>L>e< zS#Jbzk$dZ+HLDD*xYWcotG(0Y{(=MX2LuhXW{&~}7Abe$Iaaj`OJ+YQd&v z#ZG2`NHYEiXhJwijn2{yRh30+$P#N?&mGy;Tuonnh!u84eRKr%V(Z< z+DgQDzA-Wx`)eJMgZ1;eAVa>H@pV`>Wc#%io|Ie^HpL=Y;~FMf^bY2%;n5Nj@*P+K zI#?8;XMx6FeLhM0TU!!2mgQ0jBJw0O}& zJf3BTi~zE`q1X3U7^&p$)w34*TKH@-+gL+!m}9qfZrPr&mIAqjTlgwur_=*Ub!UN08&1}}(AK0&e(;4! z>M*@Q|1&(MkfLXpfFj#-@SN`at1?V`6%$ifNeMEEN8oZo*Cxtw`)V#sT@TqXJ7e1& zb&{#%`8>!=J;$>@!-uAUFFRYNhEXn$T`6e$LTSJ2J5QZ2#+Z(ZD^=^ma2<9S>m;kb`BJiAE`@l!&-XE=O?#kskATXI$zvO=^vkhb|U4Mg*~Ltz~SqhrKw&8 zrP@yl_|QOasT|)}ucTyxv5WDKtDnDZTXfBeMXdq^05AjoUw)E1I{vpDj>3e(1|PyD zXpaHRupi-}K7{O9K!l@=k{Fc)qJv$8AP4PzQbQ6=Mqi2jxLol%c;z>SSnIk^I+b$9a5j{ecf61=EcyNHfmMxlYMEwDIE8 zI+=?6OII(MuCBN>U4UO>$+XzIUbD14R=w$OCKeW;kG8(qg+grG=76bY*7O>OQ&A=c zD$?9Hj^up4zPl~#?wjv}9MPu{?WSYcoQ`%!ZC@;f@FE)Ka~|>$f~&JBEAslD9PO= z;GXY&pjSRf;E&?LTD82%59-Hsv+qAEdQ&@vsURRJaXisLE&PCbGzCU=S-I41`gIYg zk*rl3T7ub67wK8DuvTiDMp?C*W*VEr(+`@GwXgm%3* zq&U2P+h4n0&d=BPS40B%e|;?e-=ocM=w$BlPs4)$iS3H#0w(i+39lkx@%xMW`;(uY zx06{}enJEdG<77&@%(}V368i|1kbGs2*9vjaOP$c}DoRMC8 z=QkxaYg^ZA1K@VwsK%B!U z{Ag?hQwN(6s7A&3{Pgss=YxrCCm$8U#3cIkJ>`E0PNu2UMMAABhe#C*&d|UNy z5<`d5P{K-YT58>bK2cW3b)EJMB9HRKmG@aJK5-(cEFKag5N^!blTzf!=cAck3P}Yl z7jknX)bjR*0joz+WcdN!${O$h`sN{~tLg;0#7wfhx)MGy=#ryz?)O6q_maWmzyCHx zE@4yCTKLm&ur<8zgcd_{-kAj8pAuQO>tdSTbeRs(3!5e zF0#JdS#X3il}l;VObSC4rD-veb7f7dGy%&e%hyD-A3`3eo0aP6TkZaw``@ij?<1@R z|B46z|CgrzKXLdsC0Xy7K8t=i1mPPYX{+-Bt~=nM*0aLd?1|}!90S!F9tJ*(B0$;L}mXFrw~U$~u&I}`*f2HZfgn@gt&Yb&uPpq~%0=vp;BhPPT- z^;dT4^*@$eRkQSyyW0c?y?rforQbgOJlXi}3V8wb336@6G4#&fl?jXT22lT+c`bm) z&HdYYQ-U{#DBKpL&ldS;AI;7wAAfdMK=9e&PSnd&GzaF93&m z3BXoU&|I(y|1Bv?f){1#n{;>8%s3Y~J78(bY5Ux@HdE8$3Se4>RvQMZEx|U%}(#fsQ%F-Ieh5DI)=IfWIR&wxTD3={e^jDxFv;6wXl zbksAFd+q~{){J9i7^;8cD+py3Q+!HLy0q-u5;rHNC!xEy< zzD$n@7gx-+C<~T^X;tw{z`7sPLvt#Ta<%d;w^W0{D%r#347-ekil&c|4lSil0MDsX zO|okm7R+Iak_lhU?ajUcrpGobODK_(u?k!H4*TE7!Kn$O_`3iA z57a+bHX-Rx!OKUGg6PPm;@@fzXaU}f{KSOuTa2emG~9lnNUnu|)RF1_BQM8O+`2E| zeux=>7J%n~SV1ZJh6OC?W^lnTK|a3jPz|>gFc&l?@}lt#8mrCUaSc((nDA^t&nsNU z#GGBfT)ne(9%D}ACxfnIEip0of}Un+$sfN>Q5vW0@*)Nf^HesFZ-QpWiX(;U#%(F} za`|qpk}p$r(&a=zYZqJ>&ixnUz5kY!&%VFXNg)9Mm=OPy;L!i~{K`(|R*wHQ0aZQi zlvHuQue%%C_#kL%3B~;d#Oet(;}p~gB;5XN1;rr{1*L3WZd2(W;cg zQ)!^mqLs5*l=IEOvK+~8tglN{Hg)6T7c$4hEU$2U??t_4cVr^yDfZW%WO+{CynW%F z94`Ivcmnnse2zzQ*~MZ5>#PoT&OnEz-8}V^9)EDR?m)wD(}#Q+y!xRp-1%Yfo(?g2 ziB;jDXn_yj>a+LTaFe9q-|ZK2s0n3+^1P;@>jfa{`X^z02Q+GNEMh^{v`hh(x}uOq}%rOIKQDLzy66(vM~Z%FaPN7N)Sm{+GT zNs$`cC?;Sk2au<8F_9!4oidvcVOlA}(iVy?4NFm_D2<(HB#V(wfK-5 z2$YbRkFY7zWUA<E{O(8E)L;^MDYMrwC4|8-C(U0- zY!pHIuyMv8>5@p8{Cu`+5?MjDS@)2J?6yx@i?V@iutC^KJ3EtizQ~$v7-2Y>Zn5ZC zQrg#vG)g$Ki8M*OKv9fGsDRTAmD99=E1kzo{H&P&{paD?C(zCvDS)M*1cR}H@sLVqMCN(!o94zkC?03FE68i_}6EGu=sJ!M%|eb;*Fmizbza5@i($ z=Xh(=0CLEk_CqQeVuKT#6h*z8o%)OAPd;DN2yk>EskoG*-hF#srD*o92YrkaC0aR$_kG^Kw`a>(W87!}briJ)ol~^5CCJ zPZFA}Jc4;sr;@UXu;&UOmQV1wvrfOh%s=dx6tKSkuS2*;&Jmx#zC9Kb@hY?2VKt&wGg0j1$Xy!}qPBa`X4BgV9aob{oRdk?SO!Gi)b{#2sD zB?>nBggGJM7+OcD`aP4$B9}YU=wYv9>wNJEOK^bxS{!umYB9eOWZ>wUi)9Q)mnHV~ z2W4frNN31w-f3wZbf8_zr53$t5pZEWfuXBiFSr94jf2OjhEr<*PuIw(J^B-uv5*so zZuH0?HzIO95APmDv?-~aL}56);OJvmBiqrqxONy1UAm!S_AFX+A8jno%N_6@5-QEH z0nFlwj#LDpf(v^f<)vY`qZ|@u3croJa=3@P26S9CEbi`JK`qe&Ln15TE9JdENVwCS z$h}rLsP!n~nd%*j`DF_4Tw&gvRhZpGwZ=QNnZsF%ULxH>>k&HES^ZXV@LdF$M$Bs| z*fGyW03)j6C2bwT1Db3x{UPk6qrR15u&ts+?dmNfl9>CM+x4l<&7@1o1f9uukt*MS zr1y#49Z~zmX+VpY|7O>7HH3*~1nM?h-lXltT&!mt`nHcC*+{ zA~{m*IV;W)4j@)tievm~>MxTsqMhNP%?=u23tf>r2CI!wy&eu2a8uI-2Cb~EsexOo z8d_Sc0QSRMu6hdIfRRC(&@3O*v2z+STE{o7Wd3LkX#vrYgCwB@FspC;g#I*MxPhwe7c$WSv!XEmSD)u31YC@mu7+SP`AFJj!2zVFWc~6$ z$OB}9a{(Zf?|Fvo*MdmkbFl+rdlW3(V{ahy9DzKY*G@14y`)CBP|x+cJBEPSWNEPm zWNdoGh0bxDY=lz1s=El=6Y@d3gqvfEsm0{|JnY8Sg2<|Ph!8A}q?f+1nPU@B{Rmg< z{n-*(Le%SerJLlns8=vLi0)kXeO4t(=)%0qKprilQh}M!M_htoP16Y#_3dEakOEJI6aQ4`IA^81BKO-A`A7Uc63bXsjD#r zjF|=9C(7dw+V<02QWi4NzLlNP5f@%VmFUo>(uVIqtbH;d-H}MW#3qRnj=b2FkfyMB z5zp;&e);-SXcLLdC{9ZzJpy(4#?R9s*R4?A!R~*S1i#3<_hgCn%=9;hI5a2D^!*CM z)mj^)WPavqJ0~8}E^0{Byxf{XtutV=0;y6)XAXFwJh3?rigtKw9y9C+-UQkZ1oT9E zxU8J8OrIF3>uKhW)WoLBK|=DVZUuyX&79Ho>#64(%c41Ug(;wv)4V>Va&@i)l8~4- z=+m$B?KQz3Oo%fi5h4tYQ^R!t6KYy?Qe!>f23rBlWC<(a6OIWtz6E&bdPYGFNkh3R zt3TFOOAGBt_H_s9DOv03L-|DG{e)}Td$FSRB{;%Q*YYKr>KT1AC7ab9dt=-B!Oni= zBK!siyOmAp?Wx26=>r^h!}w_;*clAE(yMSO2<3ZWDzk_73NBtAvjt~0rzzy^zZecw zp@fiwJ3`j15<+SauOCf#BYS2u&nob{$0b+rgad?>&N_bu2QL-A42q1MZ_$|S_3GVhq}2{(^f22=13NZ zSyV1%RLT)-&x1Nk+pHZI?SOYTldJX+!YHh%mzUF>?l)7fAjeGn#*Cn)eMb;Jq)6|P zkISt61d~qH3VTH=cIToE%y|f~X2E%|N$a~8s^oEIM^T%LG1n1rh(aa^l1kGa0EhZ#ekh>)V)Pd+8t#^GKDPlF6JqJXY z01mYvl&M~L+dW4FnGioC%%{GQ@min)$~76OjkV{>Ma5m=y;YU8(P}BNjB$&2LZf%U z=zX5@*`$BEB1vJE#1%VT@vim!!xbrgS7k9cP=LH3ZbzIzlC-nX`mpnDFMF&J(K=A0 zhG>}Iuv6&5Gu)C?LQAs4MwX-B!r(xyFHS+9L z(i2uFI{a?9v^6;*O+NGJ5{Yhlr`u5U!&Flh)!+4EI7Eb`h!6bR89=tf_;fucwBSw| zNB1Utpw#>mUa4K1_Qcmfd7T&Br7KJ)NaZ(lvj?P##Dl{tN(_=m>zEU7bmTP<=t z+4|*p-DYaC{pIrn){F56n;|}`0Mm5QjxE3uOrf@)1HUiLpG+%kw?HB~)SfbG0>*@{ zhaoq+%iWVKGt(sOO~QO z3KCO;F31uoX1=GfDAll#%9tK#h@F8uRb|hsN3oD$e)~9rS`N|N_qm0z*mzsNAwx+^ zgn=OEAyj%iUHel?ROhwtu)Kz0ix{+g^jp`dcr)~a_smZ$vlkU&k@eR zu8&q%>{r_LKuivyME435GYt86mtrBfc0){{UnFb2_Wt6Y5%?BA%q%m*nYGIIjkLSSoY=J=>6;@+Gi!6?u!;fjPZ+_EAH|T(cseUf zj6Cks#n2sRqOg2*aKa3N+a`gzg827Y%~bxoNeFLNneDV(Il$2IA<`Ax_FCzHoa84n z%(w7@({dgx?9^%LbQ&2>9;@)HAB1x0fHyqiLXmgGBVv1w-$)=mLIy;)$btfQXW-~Y zmj(|B#wYdDCv5d6*ZM2`<9#m1evi|Yy}=c!aNwbg+#S0T%mIna8Ca-&#v{n1RhUT@ z5rp&&G-4D>C;p-t0ak2x&>f1GW=H+Wx+fO0#`@{sln1JLNn2OJ00732|C7Sxza+}P z@6Cx;d2?M(2d;23HJw#mKL-1O1yW^%~XKQ<6;OF`Wc-MS0))11ntluCjdT)Tk z=;VMH?d0|0z=CFooMyV=HEGC{hOTz}gwYGJ^iBYWwH0F^)jn!OwbLtnXoaR*-#&e) zg{IrkK6vPb#z)USc_@~~$G|>%Xoki|-#&Y&hQ`OxK5$4Ht*7BNaEO-duk$Gkn~_^n z7+>))4GmuMd|r3CE7e$%`;4XBrAwh2>k}ke63*;!xOt}N&SnjPVtpHy1yNEOsPzVE znr8G%t-k9 zu14o*o6~0vNa@0o)!M{O3$}%fAbatx*=E%Dc2>*K_3YwwNO9(36W5BA>78q%#p|U9 zq~l<&!#iD~-xd$N^$uONnmglxSxYeUR>=(Ute(n}(($F4(Mjpc(J-L>cVzL((NY2X zLWu#cG{WtT!Qt;HB#ST`TlOsEX_5J4ifq5@x(UsGIOZRYMf!nM?N=??vy3UDY(DjbteuZi_WJY4km!}tCK)g4|A3&_TrIG>NKu5iOL zZWbm_7E+^lLOFvbGl~Auh??f~K6^81sF7{4rln;mFRSpZ5B9Q^FN|muRyG@Or(E@5 z+mrg^f@D!5@#~I`lMcNlwO^;Cpk!gH#9fR$Io;p+vIB{8MGzFchprwE;zI^;oiwXhmspE#`ouH=o*Ih41j7M%uMz7Zdo0Y}!v^L1bW z3R=wd_pRH;x(??P$t3->3+9^uovFqm)==O~A)Do@oh4hP@gj|D_VHZV)GHnQiv#rx zznI|?tYycE@qo-^Nu)OM{3TI8vi==S#_k3J`szI z1y0c7suOzrFnHVx43P&qYRgcwG6^{X!y#(eIJG=!O}~&ktIEQskty@?E+}7THR%CT zSM`9$iV*hM?d0MxS6OEeCl=A8(x#vlgdq_|vx1-zL5-l4PAug;m_EP#b2}Qz9}{$T z@s3pwT3{J;x#+?6dK_EoeFx~_v1+{GCeq=i)i4u*q*h_ALuWb%R$_M8{BgM|V0$oZfuR5IvLQ)%$$ip!gP@>D zxjssYoz0^)tv#D<5!ob@^|LiR5urQeHC(YnW2aUj58-N{ryz!B-ANC>44%pIJG^1o z0yc4H4jmU>X;L&{_@MV}5iVCxfd$K$6agWZ#q+zp5D3|LjzO3*d?IJH4 z5ZPxwoFjs#j{@k&XFf21nTU76?@ADA%REe{InIIGBB6Pco5I=jc>Rg>vFn)rY#Vdb z`IHR8`45^gcEoPllg#fS1Qm~*HPaB?GOd41c)a3=4R5QC=mb9!LH`tKAy|T}I`LZpHni7n;y1K=rnJcb}G9=jJl7A_9r;@nXi!)vguBfJ6&QzXU0?9qruCvrh0af+iRWBpA$h zAMD4#t0E;XSG#9{I3SJcXcHeeqB0$0Lo;uNViy%NiAa#rG1FlZPZa_E^_wIZX&^z3 zvS7jon3H4tY>qL3bC3=E{EFXn)F$sGW2cs5Og$blvT=24hzwlCzvhVy%AFfM#pnnl zFBU#RjA`Om;|U3*B$^BJ@WagSfj@55_g>7WFD)Bah~y!$2~KM(_fNY1D)i*&JO)@F zosq=x#(5RLBN#wIXKXNb0Aw9`fn|uhht3Kq3(CRqnki2?bzIa-j`7A;9gUPqFhTmR z!17q|8Up8nqRGK8Pv(rc&xqn*4$kOpx24xU=9ZRD9XPwwYS~pUFQ<;)+&Fz4A$xRc z=Z?UibMKDaQz7LrqKI8GQAh#U1eHo4%uJdH&CzD1Ze5!ZAM6mP`5WETZ;SON4mFAx zHO54WE*J*Hr~LW=#)S4bC&x1C+qD?vYF-vja=#$x`lm}zSVnJZbn75by{8%gJtr2j zvSC(9WFy^m017Q~w$Yep8L`3jUV3B1z%TPwZhCw@mT!nj^O$~p9PK}aW#i%J;^aPm zpr?XY++>2@;s;dsNE{EY_vSoZDQ4JTE{jqPj?2e^Pu{*CabVP*Px$(JzHhzA+Be>K zjO@I`^yrt$f|u^V$aH_vW9Ypzn(AtR&?RbNvM@pW_UiV{j@|v~W9Nh4*BGQhh-84f zTSI&{ITm2@Cu|i*P9tHmVPMQCQ!u_f--j9LEe#>)Q28!Bn z^NR$P+98r&PH+q&I|`rY*iD}*6hzQM<%fa|+$%a_H}l{ob39|JwR-L$O4ONRp0hu! zM)rb#mShqqlS8?+yv#(zsC7m>+5_ukIJS)UXNEQg(7TPa1D|S?$IfK7?&v*(?Sf2j z=dY4X4q698O#9=G0%rDV!j?)njicwUz?#7efqMK(4z3C~F%h9o<`66xq2;+icGTFw zwsDK{Iy%QAuS|H$i7a0)Y$cv3jdh=efEM094{CtN?^+#4_BKo8)+obw``MKwiyZVG{B;L!m1C0dZBZUli=4wFWAiwTin>AlA!Dgg)q9E?$tl$#5; z`U|`kAS3-b8QQPz41C$3qY6EVey95b(=V-Qg53H7Z>w$!rN#IM*7ByF|AU+?8u8W< zjGFqD4+|*7tn^L_VTC&IBw?Hp42YSi9w+Yiq)HzD8Plx+JR~#zJe6gDX%)|^amJlp zp9Z!Dd=e4-(A<5PedCeQEI*+^n?nHo6=sU4dVyK3O2`)^a|CF8-c~|9N4iFXyc9Nd6rG_8TDtYS8Ujm*ha+e zH;uCh=3&Y?YF^oZcBBX*RLhU|rHtk*+1*&OC1I05{SWiP2=B2d5|lXko8f4DF!|^Z zk>y}vS-v0z+^xsGeF{pd1{*{eptFk>5&G%;{+gs6V)F2rD0)_HLQ zJ0(DL-(;i}ES)JC4?QHYPdz}P)_R#)P5#~lPx0hK_UXv4@|QhGR1nDih@asEYK*b^Wm6yn)qL;b>k)RQKE` z3UFbl-E9t=_=w4h%h1gdY)#4Mz8V-%IinE7s61yKm{!#`(`(zWedtuoG=_N^a3>L| zYqlYPHb8!n6hC!WMJmk?#if4EG_q8p3DmbI!0m&1R|PNZ>32HNWg?TT^1)GGDcv64{DQYRyW2CcG@?A zwhXQtQAMxucTeNdfDeIDQ<0IelFTj9yy|}(KoCCqxIz$1Cy~-fR#yv=Zib#hxt;JI zr`%fTV+1d};qjLERxkDUbx}iYctE{~n}0;ANfz01%%KzZQ9s6VYTDM01`G~`+ao_% zvDJ;HpDr(?b5x=O3g#3|j-eG()EbW_?#B4>v7cUdU5Z~9N>{*7z4ulF`q)JjV($!d z7)XHE2?ZEf4EC#C79KEEhFb!y|9)rXec^UmRSbCoLKur$WdbmcU2X=DfYa1S-}>3^ z;x7fJ5$9DarKMKF=6w27Z zv>^q4eV`>NIpL?@no1s#f%(U>f>pc%!mOm?iArY8UkL&YIRI(ttc}iG@=Li-`=UTc zb7B%DZAoXKD|!%Ok!_a8wqLN5rKGag$8Dp640vFJCtH0&J1{gm#-JKxsYDnEwXjSp zhj4nicIJZjoGEP%0L9bOF)UkwcLp&%fdq-$25WOAQ%>IDI<&v53la){%MzEUE#G%|G2_H|q_GC)VXeErVoSC37oCR z_MR4pd$EDtkTKPXKJ-`ZH=OfU(lVSP$+$&<5Ftxf?L=u)m$abYxmSDHG?=Cv-mJTLsHBjo6#wIxPHg$S~Wv)#pqW1yg;wQ+1K`f+slsZNkr9iHwO4;zV zLbFh$X(jDW0SLn_4E)2<;W`5cu?<0{{BdX9SgMgRa&xYpu2R-06&}VtR})wXB&>NO zu)olWy~U(h4mCV=2>iN|<93fR324$FpgbcB(mBTc*my-B7W|_7^y}sM^DRtZ1j5yg z*Q*vx6uzLFZNIzbzPt@S?gbDx?|h$zp})V4U~*@DaffY>ABdWeD8$$fBD3QB z7sP#kddGy6muPDw30$bf`^VKuarePcln;zTj^JtM5cmE0EKZUNruB%USv+NZxFE9O z&szb#RhBf3f38LD{DmZ16=p8@`>0U>OXN{VAx$E2AAJ~jx86f3)K-1wg4ej<=Wi9Oiptdgm7?90^_z^g-&G^|WAq5pn^roc`dv15{1SOH*g)>sRa~O3;H(-YVzW@yF|sM zlFET0oYYR0y{cc5A0Tx$WwB6&WrZA0V0#YCxlxxmUN8fBf4eaVjiK@284Tf+Os_H` zl7SH4Dap`4Vl7Ey-|&XEFrv@Ko)JX6w;UadVGDZZH&aeojvFsTEZ-eOYQ})Yt_(bv z6I}|0Jc90Uxf?}AJG}wDth365yYF*V)`aPdOxiE^*^vXR1oy~Ap<^AAEa5}7mLmHc z@v)fGo_C4B^mcsN&I(A@N+W6M+KS5%0crS|NI>bZ34#X27q74Q8KSsn08 zgHRxYOykQ{YS?H#&F?nVZ4TuT8(%>WhUbt8H-MT8SmNJsD zC)dVvayKb*?!h6}qk}E)QnpMTaEN9ram#sjIx}n^bFS^LW%{`fc-`M>zh6FHdAw|T zdeR5zjPL7qt`vIpcXcUubo4GghI8e5Pshu+wP-q>VVND58{G|}C5DEhCAtdY9e)F^ibCu=^CgqsV7b)ALG&gBjk=rh2t!@G- zbE-l1US!LATC>LZoyU3OkxFHSHCPZAPK72JLzVbH4nnZTWc!Gig!!%q@C=VxR8}mAq{<(U%z%MrTWYu$%3QBNum6M=h&3tH6tzS5+MwdCEDWGO?dp$ zlW2TA*ztFw^vVq&y4hfXGt9$-jo_28rF_Y+|W+=oOX98LS@>{wd2Z|3(Su? zsvmf{3139G{n^l((;tNX_K>K7#i#EkYy1#Jy ze%LDg^~u@VgD-V*Fb8vey?1QGi8C|es_5kjA9!Z^YRrx6!bha%7ULzQP!F)A1X2>U z+8|7TN+p*z>zOhdtWg1)QKG6@mr%>Zm2MG{AXI{#9Iwc%#6ZMYw@xuJm$Y=%AK1Qy zztjD;T`cyEJss+`Zrg&KmJiHE)DZ$t)MV!jE9gI;gOr~9s2g2q85Z%RV?_LVym9mA z&!64%{r!yqP(ZK0>m%WpbL`gFlcyusmDJQ8LzW_48ZYI0+32ajNSR^ZG%gIMED)|J zMJ3_uq7X5ZZnXVLTsdoNN~(r$35K$j*Xfn1K727 zs{^rI4W~DUa6ekb2K#q;#!MhNcRfO^?Ojr=uS+}=_DA1mfmR-(j{ z!M@Yb*BNLEGy%nH9x;jEF`UNCQ~v8fB6TkZ5 zs$J&kbv2baqueG-K&eH8wmtbU4YwH~c|bfhwQnC3Yda8Q3rB=Ir$#q~4(Wp$CBPfE z1+=f#z#ZVBo!bDbTM%Y7428Lss2!N%J#GMxm{4JoMYXh{7?V7Lrwviw>i!%Rk^Jgh zx_T~;Xz`OczY;mt7j^pUF5zb;M=+M1f+b$Cr%3v=XdGnYxtg%%j)7ywwV!Q;zkpH6 zeC64}AKr|(GZio5S5+cT>E6m!9wTsen`fjWDt;0?^r0|M;MMLxk0Q*-8;9O6NOLbN z1b)L>ZBzSj@cd#_I+l&BPl6_F+B8%V9|(~m(my>sz+9B#dzw8*?A~xiDo!G@HDt*R~{7zG}JhP#D;l+0++)W;QZSj-mf+V2n!*N+y+JE63m? zW-6!MnYZ-VyyKy(S>yu6y1+y>wAp9sfoEHKQ%nFA#Q2^sPkKz+6`7oU%rsXhUBb-J^$9z5dvOs{e1A%ye3Ra% zkg8Bre>1!l{&!Gwto3gg>Bh>#$61#wyj|95aD&2c9UPt7)>MZ4QM68s#`TmgbV2TghPC|;|k=q_`8y?wqvDHnZ09S}`f(O)aL5aHbp zCcJOw+VY-1Dk(yN^?H_1UgfB=x2vt{R93|^pi52ZoRMY}yY4?=l&)>W601RdUE^Ko z?^3R_l_{HSzJ}#&8>&!!xe{WB=GBi9-qVCnx`vtQ&t?rOV%xrNgERh=S(d-4To<>j zI6Ou4{JtyargS!oP_T(JkX51bMGT|cu|UKCkwE~OOzM~)hdr=g{PA6S3Z3h|ei+dA zjoiuHwNpMJNwbRP?pTTI0&je4OcF&=eo1m%%%}Z*@;G_bM6c*Wa8hIuZVv&3! z$9O4jY0Nux9%c1c@GqVp&DVbR_dyMV(sq~lnC+m76#ltGw{&gvS>jLDKl~-_)xKhm zv<>{YM&*M$YZVgYrxn9@Mw zs+0?MhBbhSlVKyQW0X3@{x3XvcsPmZv2@`&*l39dP)Q574dVB{lyYw+@_Mn3Yn`vn zUAS8~3Eahn ze=ZQhU;jl7+v~65gIry;-hII1GmrAwVC4j~bX=E>3r6O`YS{Qh_X?ge1RNI)u2 zwe}So1e|qqW^c#ditg&sjY-%QIAofhH?7{fm8V89r@t6EyH&r~TAO&@(eT~b`82qZ zy?Q=I@4ILvSD<|7j|UxUgMb^BhyQlXfQ>Luirh736aSD0tPQ9RTu9vj{6uk&D<&Gf z*GoDA@&oCjp}lYAqTJQ&M+h`agJwiBCmbN)Kn>{4?fk{X%MAr)PZZ!o(}wuS8y)Eg zqLekllxFf4st1r);PZRiqd9$1rE50t3@7V3a-6f`sY zHRJ=J5U2yJ=tK?)&;E?`GvMU|a!`^BG{nVNGcta*+DcIQx;Id^A>R{WMxXUFsnP|8 z0>Dsp-pkWBXPtqHoGiR1^V!v7?Fe}g{bGq`5gVeKLYnY~ zX~(t8XLjUq7B`4dU7|+`XMp{KS5UQP@>q>Otq_mbJfvt)%h;v6k_mipTqB||x z5Ql4xe=9;-@Y8N0?UckjVpG5WTl6 zD3yIVR7lTj2uB8gHZ{fKoAVT)Ampmpg~}RA==r+Q+kcAJFt1%`B(pfs-VmRp?Q*=bQ(SBLJ&A3m5hD)6|Q@Wzn3A8kXH! z8#mI&Khbi@X*nGxd*EEpyj;(8enNg+3M11eP-KBL#~%sDEtf43k>ur1U$gIgRvTIX zzS}Z9Gr@IEyG3J-AwZzocP!bzA6HlR{~yZUDM%Lvh7$bRcK2=Dwr$(CZQHhO+qP}v zwr%6~{4-Tsvs1NIyZf5F<>BOCVC9NNg(P6+!8zTjX?N?#`j;=_dnhO z;ofNZJ`m!4pJ4q08#$PD4D)2iSeE9svbYU<^bgO|X3tr#>J1TM z>)@kKkSR%e%T?;F69tzY@pMN7axPkqw03z&5oyH}7lEqsn<#3bAS;38<%wOF%B><* zXzHcR;aKKs0W2)8xsUpi1YZ~k4Y9yC^PdG6vc;;-Xc$1)A*w=5Cs`#w2fKfiAhaDR zhH&A+q(%F1!5au2LP#fV#A7=qno4w(IWKaDhtvi$9c7FUxS~ibYIkU}@}g1C0I<5B z>)C%`w5aALc$R^YvTDWERHvM14j}97Nerw{ZVU(dDnmzPVL--6kU@i^DKZhUT1B>? z+v1iCk*A+-Ry%Q~?hg|@%|egq|6*I}>`GW6hX*NVXmhQQGz~r*lI27XHvhrtm3M4o zL%-l)&<(1G3+mTq=~CM|`-iiXAEl`cwN{PPPsZ2#AaM)8`L`V{UO+EDZ#&}f*#f3g zZ#%BLlYP3rN_@-Z-PZV4o{yl9lY{#$^EGa>8aC5ydCn(PR~4dOX1pP#!f=m63}`6S z1qs?jVXS80pu2V+xDCOwGQ;BvKFCvIfhaHp5s+WPm>d6(VV^S%g?d)`g&9_y9P^rE zWs&3)f^70*Az7mpw2_0nBS3S9Pe>eGe*GM7f zdsXmM#oZnHw&#vqgufhi!L5qP|Lh9N)}|X1(hzM0RNzJZeI8U1A-Ir zUv<{;O}X@5!F3x3gp5Z*mz~ygw!O>=oUV3JdG9)t}l^F`GLegS4tx!;35ln|B zguPs%8J^DKumL%CBE(LpRF+f2;5e*e| zHR?d{>B)wqr9>ZC(e1y0w1_oviA~L|F3sp*%8`Z+ytIVu6T_d?zKUv7a8BK!FDty= zMVXwsQBp0{#Ox#BD!DAoq<&gjnSg1b>u~=hFn0h`rHa99KsvxaZN@=Q8fNeBDrJ0i zX<M0w^LphK)Ck7D`Bk^1nzk?QEAg6Knur|FeoGzEZ2JTP(zZ-<7yNb zLy})B2WxodP}(;3qF zDD330k5-*pW)Fh_f4C0-bUtJk`zD;%<&Oy>!zL3MF;Bfp-e3oEg8>3b|xjW~^U zmy+g>7FFM11uv{5 zTWmrP^g|b+PoAmd*W0_6|0?2SLZwqP3(U@7t6Z~rdL_IdToQ2mT9_WY=^^5sc1SI%*(2Z%XIr+k0NMS+$W-YNr7CIp3(_2jS=?R)JX6Q(JKa(~*`$x1-ddF^ob+d3 z_rzBqD;ViO)DST-uUn-c$93naXDt0((JAU zGXiAUQB?JP`BLdN5m9Ox2RuB$dXH;#M~G_lytc;1MzpWj_f^2aQ530{Vn)dY|wsnOa0k&*^u2 zxXJXMo)Fg$qk5>0Lb=+*qSiquZcaKAWezANBX!1atO-w1Tkf6swA!))^2^%`h%_!4 z^&rbxT%Y+)v$3dbHaC8_I^f;<_S#?bT0zT7FuIDBK*{R0rM7dcKSih@OBynBq`M$^eOQ++M^3g>$~;z?)~ul@!{UM z)t7J;u(1|uOCPxfnz^$&xU-O9lv`ep7z;O(#{6BL3o|V?V(s88%(QnfU8&U<;_c+| z^!YqyF3nYYd105Uia&~!)6`Awj^M?#g|DA}KnU>P(3b`|}y?d&2#2((f^xkFpo~-R=MowEjS~(Mq)a^1JK}lN3 z;7VnL(8q+p=)7&P?J~%9d*ogj9~`RDpKGaVn!gbfCT48~Jd5Ta|6_|ODm-O{a4wfL zK{f(ZOAoQ(Bo9cJ<1&7mLqBk~iiDvM_0yYvSu@o?Q9N@~D6K+&4iqopC{0P#p zh_SJGA(`IBuA!+qmhP^>%rwL<)X+aD&>aHNx{pq6m(4xs!XTlYImMQ21Y6s>;nY;T zZ8mdNyOqF82{-aMz+qD@SUqsX5+U59wIZLjL)UO5p6WpEnJQt0s`bG&2CHvV2X3b9 z5XQsw!?fzU{HOPh8`ia!*L?R{P2FbQ_3BeHMQo3GI@(Z3z=lP-KFK`kFTvGtkWK~H z5SUQlCwO}N?K;#*9YWQmSi~(OFA`a&JVPiq@l>ip|HBOJ+YB6 z1*WM{q1X?xQ5QyH)0CcKZ7{QD@|@Bp!@E#t>yi#aeRdf{B{QFW9bS`VlWF42olm=;vM@E`hh4WV>hoE#QFgBZ$2pA;HbUuWeT^)08&N3MG+@{YFh<@&^w*3gm zbQSc5&F-5I$c|oveEX4bmy51_-PcEERAk<`p0Z_vtcjfJgaJ5oAeBciScZs~H~$gl zJ3HZ#)Nw+flJaoPJUWKOWR~%u7f2C?vY?%SIB%Yf`FE!wqiZL$khQN@-qFYM!XW`j1}^V|)(xb|-piqo~QFfg!rc z6pjt1#aUfv4+Nc4o|6^~X9GIRjZcx8C{jfF&Afwvx&lHm84S}!cMMD7o_%yfWcu9aJdRp3E7*OWg~_nPeLZb$7H(Z{!xmEIyBtU zpD5X=sff!e*BU$4Nhd{CWr}2%BO-N_79;w9I3gkAO1NxAid!OzzBV#nL#1278v~Hkn+0^*-HPb5IO=9_TrhW}it! zT%Y&t5~G4OZfM<~Xj&z1MHPGglSoQ8104(h0}EclE)){JepHyK#k5|V*X1jg0Gh5wF{MFI<+-J6 z>V91L2y4L+!iNk_5{MSsOKsW$VuoGf3G+0*P^IqkKi7RH>^-Q%z*Kbv*){2->qk>;BgrIF7%>9@9Q{z_F)$q{u7lK&UDMZ(EyD`b<%F;_YRU)vf z*`Xt{`BEiW?J0MtlyJWD^R05et3HU&4V~+r6`aaGO=p8D4+hpv?HA+bHSMdm;lQi5 z5@(~!XrjE%&NUrCp!#xhZpWr);b5ND?!2$Ig>y94P85skd-XgJ`&rZqjC?oh2edjG zCVm{{WcLjFS@qG)obl;u7}=@{FFYec~`n-}Jm+bTQb*1rkVf|@kvDR90 z$eeHf8F;Og-cm=d0EPWJ+7mSxB@L?6Z=P$i3_S!&g(AB!_7?N~gOj1W-3rhM0GUm`wNBUW&)1R2s_%4m6 zr&l4ZViGCsec|qvYwH$-+2H2&<#8i}nq^%(w#ftu~{~bZEwD1LnL@T?HT9o7r5D~Zay#PI}*|e6c|Agmmuqxb|>)bV)a?LR8`!G zVG0QmzudkMx;8Yjn>#RVJBWpE-V4atZ1P>SQB98c-XWVL=#$)MfCZqP@a``%Ye-zD z_Tg~IRrFDp_BK@Sr$D`*n=s4~9p~qu1tkP$imA`)c%EzUc|bMagW^RJM!^@$diiD8 z8O@~EjSK6-YB9JQ1zjkCZtkoeIC+hgbjDws<;-BD(g_uQ@l#F&&g6y}4z>I*8L8v+ z8Sx0eD-h+%tCBenko-h-EGk`1yx(jJAX(G<&UF-}P^jw9z^QHZ$vz{y3MvqVe7M+b zlO^xm$L!S}u6p^=qGzax*fFrbUQR`F@-oPXS?(VM!rh`5#`x(tZQRP6?h}s5r~+(t z1x>>%Ftgs8Ng~E6>lk^k!T1jkhMU~z#8z3u$m8#Jws@ls;IQ105uY0=JYs>`MX8 z7R{RZvfRZ?yFM+o&SO{U_h*A_v0jKHao8+YDW{~uoku0EYF^~sQFcw( zmijsopw3lR2*l0~dI0p9C#KRIKi>ji+nPOi0>;v7%}9DWnBGN^2R=Ga`PlIL7lPbg zZ||Nw$hL}oab|r1oqirDz$gaEtIIa^e6L>!gi0>@{_%g9cG>1G@A~Cvm+y7`!`4QB zT4(3|rP7Xn_L}?sbUn)-Cue8;rJ~;6>sz%Q@AB33ECM^J`u7hMW9^IAcD5G&;CN?0 zzX?;{$t-TS?%wO1TGuZ}`-?s~H8%}Sd*;fbtv#XZr=wj?cJ2Y0dt7QQx6;F{&0PoE zCjQ0n|NSk~|GZ<;+M>v101E)ra{vI0|8GvIyQz`>|4do_7tv?bXZ&BbsrIh@HV3l5 zTi`zgoqgeLVG{C9Z;s=w>&}l2*LvA5SFc^8Mo5duw#}&|Dk;bB{r#DQ6belzC2~rw zgU=-`lJKXG?7s=V|E6DvjMRygnIiR{-@bSGP#hCNnDTnh`};Xw{%!uA+&>hb@q6wa ziC?$EiqwcqJcgV&4~9`99Mn59&8G99T(eLfCY%XG^Ww(I=QF-jGNp|Kkz#6-WCDn2 zBo29!lZnFZ6U!#vX;6#^4|P6bVnYcY&Ck$_i&5B86AcPgypXQ@Qa*5?PMj+@m?rs} zDH9G2DIBfw7%_*#L(ND~nDYvG8tl8Kj&1UqO)!HeoHIWqPJu}=^R@I+%A~u4a`2Of zBK}I)pf(Qwt5zL<{m~_PK|On(wfNoa-+)%5e~%iKoP#FCSvurGw1lq#){kaM;(-q# z*6f5hKPo0nl5v86;*=aps^pG+_G<12`{`i7oiiZ~HEKa#c5q1zmLVi2uU@hB8rfte$!G_o__Z#(99pUqs09e2qvA(y^hvd>j2m6 z<#8h^8srIy;6_u@7k!KVN)Hk%6hPjPkav`+9jG~23CRAVvqoP0U<>1G#71N#2Be$=4-6_Y>#j$p|yXn>4nA&o=CY%M4+Np1}c{c)whDk^JK3eN^L zO{r2xf#rJwO%-WKP*x`)s4=CydBxq9PC9JGGc4nUIF47V_qZ@Oz&23=G$Mk3Qs##8 z1&lvL8M(s004@il6l~N{lfHiizcB{rBx=Z3*ayue#tG?6f=r>U$KYoDr&43kh#pCX?U{gzYE1=7XKr&%_u8OdCv6FK8 zSsGx&AsJv(7Q{fWj!V*$Ku3WrB~KEJ)l$EVv7!hD;r8RcyGxufArz6cXlf54j7;tx z@k4MyvJ~n+&Y!ZnK7^Y(W_aqyy89x5WE?y_1d*!_3Ne*xC+=5%mOF^3rko) z0o9OBxe*b7m4VLi@_iDr=OZc7z~k2lgoHOkpKq-rJgVE(dxX2)ZDI*mOK6TmVhE9l zwq{W+q-pw4n3Nli+SdKrZM7RNEhXSmAcS7A6g?PZbzIcuIVA;2)IB5I%0``x>`5l8P)nW)$3P@^FIBr_WY0as{{tuNZmLsn0oH-N{9Y{e6lnuWI{7T&L z@11#zS&gSTxnl;|Kp!7b5#$NR0_G}-Aw5|9Pxny(G9rQk&04LpxbM1lW6x-vNa)WT zh35W@#>6`hWE5=F%PybDN$XGR`t$pknO5#rueO_W*qt#qi}Ezv`v>2|`cJ1cg^kZ> zvtML|Yr;)L#@=UPFO{11D{XM)RIpCBA?IBrRpbG5Rdh*R6$6AY>Xc@Ub^r!_Gzkq6 zP0+y|09zkQme%r|rP{X}FB~w+)3%yJ_B6sfmqxOK<7YVB3Uum|n2{`{o0UZ~CE{!0 zUizqEf5SaR&1|O|JQtH}9oK||!CZu-)#B^XRNy420%T*3uvNy5z0g^HdK;?YCXpKEnYtBsGvc4oESt`Ts4j&>z zlca+)WaFPnL&oz4Ci8&Zc<*Cy>MLE|P9;Y0i0HTF(7_pg&EtRtL>qE;u$efaHL=R- zZb`OBRg3xnT0bqaQE`uTq1COw8N*0P+hWd)gNuueoE4xv;JZB3K=zXccGwtjFb&&+ z;q~cO|F?wiOE#YBk3imE!GPXBxExGr|Fzj+^pUfJC~>f%#E?nV`RN+`QRSY+s?hPU z0SDd3v{DU9dv8Lv=_I01;zO6-jZd_2Hg$Y4@i#3um%<)F+-6hvwz`*!1SMVwevNN! zT|_^Af;sl*VOFtb=;Fu*wb&a&4ShQtH9Rd+Rf?8CbiX$Wt~P-hS%IAUm24QiHY2mz ze;q%0?yrVhsJR)<40UJR&RkR+x+HWM1@{`XB_Xz8SjVG4c*p^BA8 zof`ErF_#>!d=)!o?C4sNseQ0*R;^2mnDaZZmV4#!{v9EkdcHe_nN4C1EScLtj$Pze zfE8@F+yjP2xrQ2Z@_N0?ryMc1hTfb1kfc5J(0G;tt@XoEF}LWpBKnGD5Ry2(g5Ax< zO^lLHwnt(tAD;yu;z`8LE?R?4YiJQZ_2p=+i3QmcozT5QY=)zJ`uOd3o0YT8@k;OM z-6VHM$&DTs`XuM28#u7yc$GHU?jxPNG%XFgSA{7TbeYPew4YsGqi?vpj`=3k1ADnm z!HGO$DXQXr&ew`J7U7g@#!hc+JnPR)$S3D*=k)ep4*8(@esl*7>Uq0XN8zHLsNJql zQW}mRnWi+*!UKr(HFpq{IxNh3M;c*NN!@P?_M^9sgo*zMW9Z|pH_Ft6#xupSSGCvO7 zwrl5Pwin`8&h}V?8+nwY)7USU`!OHNkhNWEzfo81n4|7cA^b=<%$$K-aU`~{^OnCR z^j}y@TKd=v3LkSb8VL8;-o3bh4)J^hsq@xVTbqq1jgzO+PC7?=WxN0UaMaoAC+t008Lo(;n2HU}3vvOUw*7&^V|KD;Y;6J(Yv^Ak1 z2?YQck^=x7|G(slv5lpvoy-5FT28dIowqyC{M5`I@fBW2Z!=PEd@nk#Y|!-GQq68w znzCn;A&3Z};Gq~mEhK;Y&JKp6MM(Q5xpTdkP@+NM;rpI)tG}P{_Y3s@AdjVJ?RhK-(Wu`j>}M_A<{)rXUt zn+z$7A=h1#arn-?z=B<1s+-V7*P8$rStL2|WR-=$cqC|wLbH{E?Lo}Vri10ru_QVJ zspmcL2r;gEBrkTZi)wbBs&t&8Yj#<)*j)!~5dStVBo15Pi{2b$4fMjf{R*Zva2HDx zm5`Mh!7bV#HR}Xd6{x3`=R{?&+NtFNdH6UA9cw6S$j{0~?8XRq8k&E0L)LSZz;22i$u|TEKISy1-_Yxx}3^lQES+ z;GTjR(3W|NaBdf8l*L+2^aMFPoB{)4p^;(5OgNw?z6LEgwuA}u!`J(D_jHoygR83t zM_=x3MCj@#CQn8$?!7!bf%4}j4-R2I*6$Bp;u!$2bB5791K0ssID;mUjWy&+>Ohu3 z`Koh9e192rWM!neOiic;p+K8R%tgo0uv7zsprRx_$65`OwffO7{%X@6SW0a!>z}ajxx>;6w7Udrd9AE_NB6b185$iE8N0tX{krIhQw+( zA@>Noz&WS}gkeYSXd-8qMaD?;c6|gV_}%|?MZ0(9Hb2xFjq+j$?T#6^7TP1&ArNg; zc@g;MU*o?bz<+-6b7ub76~1~(hl*33s%~>`l9H=tU_!%{6wy)i(xahEEbR{xKd@_P z9yc`@LO1X~JK>d@ppUW}NqHtQJF+9v8?YIK>W7bk1QC4@b1~DVpYe9nf|{|( z5||T@lVBizL2;nngjb4=pU$6)7YT@jC4f6wWuOP;C^4O@WUtMwlMMVxr$a0fQkR9E zIzVI9Z&3yL0u>;BO8|?A&xB#m1Vwe$`WHBXU;e<9+~kxMP0~zd=1hc)6zHqRTO!f1 zpl)Rfh-*M1YitSOyINbFiW57j!ngD>anX6zgOB4tpSz1CpbJA&t-~0tuQTVh%SG*( zC>8LJMo%AbeljuZ3Py9}s~>VR5Rkqj6~GG0a61Dz0i>ekiq>8RWTkQvvPk86?B~7k zBiUk3Tl=zZVA=8}8?dfpPeVkFNbM zoiMo7Q{^|Nu7W_-W3Vf-1#+F`JfmGhPy<@S+4yWS8i6{oT3CC_EiGo3Ds0rLVjl)( z?Utr!hr6w~)R@W$2VXK|rln|Jru;Bo)p`fc;U2F@FJSUHyTbNRY0%u z$*VzObT#3*Sglf$iGFe?s%&Mnh-X=&4i`a*_+xw{>G^U2^-nrW7+3XP?{7S<=w@4-!|jT z-ii#(cBfZPe@kA;$ey4sDOq`%LDzG^sYj|Ms?9VOBSLwfr~`8xU=wgyE^LIfqK+@4pOVQ;MMY1dfZlJ=B6V4O>Np-o05}ZPWQ;y zjRsvy&+5fw<4i# zAc&T1I~lz=hhEw+n+Caxd}KiR?Kl6e(w?B?P1j-_LTltLjxG?ra1tp?Vf@ix8^FzS z5!Q7}bzS2j=DX6g=ySbeKnekD{gT6ojUc;hg)>9Kkn=}C{Mfr%vwL;G#8jwsa=>-$ zez3Jj%*1Yqp=&jDyK6`jyHST8f>vCQUd3-aq2D09628s$CJk@bvH8*t?$%D&o5KnP z<$&AF!Z16v;RZ|^b{6IvxFAY3ME)QUMUk6DZsKOJ9%*(>`;#vGDi6R{$nJHja)^{H zlycu=S?JhjE(dEX<+HbfXv$;n)S(OgA$He2+j8fxAIwoM+wXeDHK6wJZ#MNkAoX6~ zXoOMOjUHVm+<-PKv-;JRGFe&AAVCazRkbde1r88Rdbs|-5bjNo9<5;`IcX?XV=9Q z`g%&_DaH83X|~7nn$+=HVC7F;b8x^}q2s7HS;;>HXf4SnhcSz#Oay$@IX-TvHJLUP zw&ju-I*9xT)rGX`KE`>NNJZ<`!}}5cP(45)wTb!o+dzH4Uco_4i^kn6j>n2j*K9i5 zDSd1ftv+#zw!!RuYF3p^Irno&qMkb7e5NRZ|T+8`?-_QY&Ndy(rJ9{{Y(4&9; z0l#1MlO;bfyP?>NWJO1yC82o_nqlq_kHX0rGH2PhZAIT!l2Go7s+@_HQ=V@I9EPNd=>J{#)=z9y<|D zfNH^YA}OOB`>%uA4bbDeNy3EdAW$9(xs07I!&vkavrg3St7u}c*TbO)j~piwPpZ}1 z(Bs=7c!^hcj(miL&J&}oNM3NoKPqD|U_E~3JRn)@K5!`%KarQjD2W$=Va33j<=u=E zvA2k^RYh7RX#$2@;D7~>DLOOhHVwT7_U02KPB9XHI<@|NU!6vNa|DFu?Ny^b6L*B` zo86mLqt=e@Q2liqx39<_b4PDhGhk5guZVIPLI?q~m?2ao%QdVdQlME-|Fg`Ek!=r< zn$qs1JJ9V(@j|7RB$*1~oMI(s0tO)#NuLOqrm~)CbQ_k;?=YwD)Ev;-3yL)qL;@73 zO_ruO>J(6F2CHb1v`0&DL)cv)NFM&9jy)=XyQ^Eq{tW+&z4|JAF-FhD3kGR6 z>9=RnWAH#exT%f4Rbw%iesIbn$fz>oWXECnoj{}zFIiE=FFPl|DFK|9&``!;mTm9v zrLYeo{D2~f!Jtu)8n2`Q^@bt^DO$lk6oBHLE^>W;P--EOW>rikWdr$A7Rv28rXx_O z!Ip$DvAbih4qS-|DMF+q9tAPm3tZ#DQ?`Rn8(N@JWl7u)h`l2D1R{?F#S5k@6aq?} zo6&J_em?HYqnVxUQD#c2L=E(VT*JT6_4-mGoS-+b4OtDq8eixT-nSg`Ns z_6V53l*zO?$T7tY3x8@j1Ml=&;q=}^xbbNBFl)oZpWlDnWn}s5L^*yEF}YX^m|#2W zYuy%#E9r8AkpQ_tFPyN!wXQf>c>8^OU(Cm5kXS;gS zYK^R;dh!>4P%E`KUytwS`H>|E*Xk3}O?S<@+Pe~3%s!g8oB$Y}h~QsEX$3r%-EmV}HsuF~IQ#}NBDiA6Lo3aotwI5!f&0o|jUTX}+aZgZ`0Oz%Jq|8Xb=cA)DXJLGsbQR4=zH zE&OI{c7mk_`wosel=I5N&um?06G-AS$1Ih0u2Y6AXdNt2cZ3>GmSF!BR9)m^4ap)W zg=Y-9f^_}!hzBB?DPoa(j~6XDQ2RhgM-|!ywCZ4Z1(E&M8YjFu^07Gq3{vm;FW&Ub zNrp`g4UJZ8t8yp1U6<(_dGB7O%-}@Ql>fdZ&4It#5XGwDc;8c-3=#Yv)nK@UAr@+5 zUVdiLEhyD<6(1AT@_8x1re%XUinqcWcnwol{6J0SDN)IGNnEs6OkYnZPTvB5(gRi$ zVQuovyzhM|%jp4)^hHe{-PCJ$6y>gVQ5_yc6w4EmjaZ~y&}hPLI*Cg(Gm|XYh=ipE zAX_wVU8|S~{txo6*eRnZh|DhcIS#C@hWf!DCDG>u#l=e14Z`J5#_mM%ATkvKH&cBe z#1;Rbo5J4F7Gpt*!;&RX0rtI^O7$(Szm4#sfbbtG&^opc=-u#6KYJ#=k4x7aAO9zJJbZoQ#L=s#kl3e=!jIjQD@=o#~N6 z3e}xt=Wr`u+R4g*eCP2!t$XFeW&OUyZObt$8~f9Q7=Ym{O%@_{1e^GgTEIzc>3{yn zbGIEpa7YZqCB8WDgMT%kM0eNfaxmL)B?H2RQH^uW5sZY|d23Vu_jXm?))hJzgV$(>t^wC8=)2)fn)|VvkU8Z4CPE zAmPTbn@AaZJ*LV~;P{-irk*^|jc%>XJQ!?-6}tx@a7pA6;?@Sx(X}=%8)-fD*ugPV z*s?LIAhU@Wnrh}Votlb`%W;&{#Jh(unevkKx4XAjZyvh+3rL|kL-603QRfvR&U{`! z%N0h^Ujnn8go~rA1{-P8OkAEB;LRxI&}{4rWD|MA>a%Q$4?)V4P~B6k|Mi1SrMN#D z2&x98S_Qox@!h6z>c#Nc*;b7awdkB}Ahz~|!9TeOgr%9%7wcH7ozlf{VxPmSIHLH{ zjZnX44)X;@RqeaI0r5z6#JaQgzz`6j2{i{r6o{{6e76?sOUU|WH0n9Wow)?>$6m~M z6tQ2%oN#q~C+}26R|%`iE+XCxBAy_w%Ql>TobUR8aen=?gYqw1z9=;pN0;B%B$|N1v!j;>tP_qn}({Kdpx`kR89m(`LAwIjM_VOr+0W}>>T=B{J{n8y&b@8 zf10x?1q0eVE^UrKHAq79crv3QIV6HEHb3eE9is-uzxBhYm*?z)+d55_`5-=y0=5W|BjkMRY?Q z!J^@+W2%#zCUiY7Rxi?)ZK_06)D1Q8Ez=Oto)obTy*mhF8+s18vddb?kRo2|EV1`9%d~pFu*^l|u!OOmX2ie5t*uzq(!?*MiNt^MvaE`+Cd@HD z+f0E+r=emxijx#{*y4WCfQC6NSzRw-Ps}&=g^h}MfF;v!4V2~v{>3- zx}7d*yDl=?cP=yNp=JByfj#$TcChM{qCt_Kz#jZVytW$Rw(AFVlBxb<& ze0{tb&Cu1O-Jeb_fu)|Jue!q#&|SpbA70IX9`k#0`D^IBo+bojw(oxiccSwSro0FULA}B|qkcbsj&F|Eiw}O8F-YS`TLL*d9D6XqTZlP7tVH3daai z^@0#cCw$l=m+SY49Kqx}FFdL3)s^#X)l>_|nYo$!9mBC%-WrYy_5QXMiYilV!fkPl zwf_x{@%r7vEbHy@%t9)=(UxuCQ)4Pk4JofwZy#7;Z=0KcO=^N&c6|Do{^;p))Mc@g zR8&7bsV?g(eYgx&^!~}_`Pb^LyXv2}Ca_>UKxqvb=)hKrSj4GA*+PNW#m`!kKtTDo zT2)pt*9|5-EE!Ty)`trf^U;$s1L{|mN7 z=0WP?ITlp@K%sgqJ`GiUhaASad`Q=Wbec79dR4v-WwQ52SX!%BXjQ%B2g3Q>HV9r8 ztJ$HYL8wAzRH3OhFp=^J$$+8VSBSq2&(QBe@m=q7@g3-q{u8`s#9@)LNEZiWk=OL; z&Ay?Z9Va8epl+wXzgLR`PVBEvec^{==9T8Q+=6d<7)}c#>&XIr7!bSj$boqU)Aamm z$FAJAYrEd!AHs1$De#;BNMV*YE8XT`+rdLihMlSnCt2xjf-F2V@XPNiXvPU4GOR=d zq&++vW&I7a0qm(WU>V!8+Ia^YkwA#qd)?mH*C*4c4anArV$k9R`Msjns*YdFh*(A@ zJvLKej9vfUi!-#C3N)F33wDE>GVODVmq;3>6U1gI$9h}VYlbpo23@^|Zo`4@s`Lug#nP0~$KA{VcqX{mkv*~*=t8GGnTDYVx^kheVzUNQ*W z?G=yEQFT&EpRiHMiqI~>rVskE6ZG!VqfKjlURP}Oz7FC~q42Nw z*>(&dR97aR~B2_>XehyKup<3uo=R1pmpxh9d8* zdpW0Ah4iR&Q$#F?HQ^ikSa5qPPbR2)XTlz_x3jTuQw+zA2P>1vvjm2*_3Qb zwxsUO5y}t8^v$$Hro@3=n}SADOtb0}gN4I9m~V`8Vrk-pbK~p-6bAA%E#0GMBg208 zUkx1kuq5!xur_btT7^A*v5<5HyRPJ_vYuj3v6_Johg=|@b`0+3%DQgmyL z|7nvG%kM|ifhI}VWY!OSxW{+*TarcP(WlseH-dHjFb^X6Qy?FBF%@2kpoSyezyt(uJ zJ3}7^LtZQPBSM?u*QQ@#%ruT|3im zq^lbuDdm)8-ITjy;LNQ+KpGvgC=Q27C6yJ2;JEPNn3Cfz+@vl?j=Bp8zuo~Pb*F3Q zyILs%q)O0B585?FdP(?-NBjAs%<&$M1cr^%q~`=05!Zc!`m=y&?M0UcSt-W*(f zO3F_8WKu5?Z+lX2K^wk5tvKWa#iK}^v^V^{z($;W?^PUl@fM$|J@M&Y>UdGin;4)> za_PnQSWItZ+JRTek}0&e_2Y+=c8KNFR-d$gjw;s6NfJ))t8`;m$bUVb+68C zd;K#dbU|d^NWLp>kvR9qJqFYFS{LgDrYjFma86+?2O121d|rU!hGooAZcIKVPt{RY zOJakiMh3S8?%%7rr59Lo0OdT497#PVk*5_EAP^(@3pOYwrk29Y0PUqI^)%MYq6>=7 zn@|(64+QjOPGkoA0B25p4%qV3f!YL_lz@D2Rx*Xg`z_nPT1NWh5Vg zb*o^wEmODXpb+{aJ!)Mms*4vA4%E)em&j>QZTDB-!TIO;$o#?InTG@S$pq+b9JuOG zj~G!z6jY!J-Q=6((z*p$AlQ2B5QQ&wICtjwr5hNfG{*Tcejg(ZVs#J`dSAYus-!>U z{UU#kvq7xFBD~?juP2PJ{hr;~KOhEWfou_JQeZ?38Lq%N4HW4TN<76mcO?2Yp;@eL zK7El<`cK#Z>?_TWrsy;uRxRj_`rf@fbQP)2I0BftY93h!Fg@h*9OB#UJZvk*H_K!i zb0NbVHMQK$bZm31S;dmvjD{nmbI{fqOYh_h^g8YWm1&6dGKzDjXsx&fEj^q(p^+2{ zUF0lja|gox{gY`t0!}3L29z05I<}-m%cb9%Ns1Cp(kDrJ;4kpIiy{G#{!?%pq6UhVe69Oes4Uygsyha^6>?g!>kp|S86{*y~h-OG9#sMNGh zQQabQpkBHKTnh(@W*UjHbF-`R7>FH7GLzC9MjJo0>26Siyb_`lQ7!*RqKJL3G)z=( z*IbRs#UB=$2xNO(we>ow#6XGvF1f4 zpBL5jh+m4#I7ws$r(7L@`a+s%VUD_9mX` zO!AnSvXSwKlaHtsH1KfovC>lVn0s5y4cvg1I?7^idU&9{e~h`qW&$pa_8xi$79B`e2#6f)e#e38meIZe!&hRmeXEvKM;=cx(4NVv$7UFciaE$^iIz zGkYZ=Wk{e!=5<@@%C?b~Vc}D}qJ0P(`tiSvEk_=s6oqQ9hOQjp5biMHvCd$LX1Ern zNQ?ah$rEY#?7cfS!Epk$mo##v!*CQ=wH@12;7qrI;mk`D)@ zOg%N`_Vj1}Y8OK0W^sNi1Hz`7mB*pz;yea&D7#r3u?`W#Tpn&>3(mqLwV?h5AIp__ z5>IXfbK>2l`OYC@9pp4Sp){vTi6PGB|iw_&>6q6rkPvDQ1C@PP2m$2737AY>_;W(6{zWohx3|;|lKjBw@ha+(@ z^1IBr3`Tg-IL%5vQ4NS8e9+8|pG8b{DqhThH`F={hzZQy{deCl)Ise+qwFABE(Psb zutsX&lfi~LF728bRG+La1w)TmOU;f{7?xTc652!dfU1$SRz%9C2~B1`cVM)-Cb zv3^J}XV%8-tgWJM0G*WKzym&Zg~4~d{yEBvz zDU3QSJr0tM@h5AAXSUgRHS%-g_#{-bcK^0$Wc zPQTVkif?G^a=*$+Ov~{iMp65(&B2SWkO{JhZMRv?ARTRMtJrMf+aPGvle={i2O}C)<(pZ8yXsVhkhq38bP83cPcDv zlNwj{#>RxdvobuTd-kw{j6~}2d_NdH8-;zvx__NF2L%)-BC0}=Lav$s>k7|9&I=?zq%^<5UYaRu(EochLyzbG;Lidf z_L-COrfNPqqukqLw5VbLj-IHHOUq*n*Nz8{_;55U7Jy(I=N?VWN}VvUK4A`uKzyry-BziIyC zFvix><`r5vG#pcEP5VY2xWK872x+WiekM{RUiCCp${Q8e9m)yBDfZfq3)Z)WoP0_m zQvBQ8g0;HXDVA%ahpEC;cMy(V01PIkzA8QaO9Zv^I*K&42J_3oPvK`2)sew_D=IVC z{KmYIQ3V26cPP}Bve~Gt-=ZSZU38wW1z*#5m<5b|>Xs;JOPqSf3 z%Ec+OGRAEVV1%po^# z<&<#JrAeGJAmk9K!$5g@l^lN4j7N1k9&ggL(SwgMexHQ7zl~-fO3B{Za$7OhW(%gH z_E;%-jMAf)H#9)#fmpXgF3NYqoh)x4!SSODC0{h#MAk>4@nYHHo^wnb8Be_~J09=v z-Qy3<7e-@IoF~Mfry7b)x)KE308pKea9`ulQG^$em@+W2TK*H0mA|ew6M>zTMg!-2 zZD<&rb|W+x;Zsdh#TE>I)febX>A;nr)3l*wHM=O1fwGX%5HuseL7Eg+ln5QnkZVo! zv=hpFoNJnd+NL2w*4yj{4d?~bBf_!T+1XXCmZW(+$aLB@=96vYMVFc&H}`Dvk< zRxMN3QDZa}&~nO_V33|pTZ=>!Q^}WK3GKV8CB8UnOUBz=RUEE(^a-Cqs^PRIoXkh0 zqEnKQk$29r!cQY$9K@mLt;z!gsCf_$gojZ9+U+#FZT(1mFDxyRazBQ;udYbDHcLqZ?>-o&09NFmp<2y<3u?l z0hn;_b7NdCjAe&!x+js&=7P=FF9~%Be>8DdPmMW_a2ez*B4?q!;&078>`{F>b|YOS ztT#tW-)`^Q`9-o4m>O6kP4t$soO4^5`_h_zE;SCMdi>o^CNiMk4Ws0IaYn`8IJz-1 zSeJ4^$t@% zZ|`sGC$Dz?KRhBfc#cs2CO}vCcNRFV-!j%UP>;=Orq&(f_C8-7|7A?S=~ zyEmQLW|oNbw$^Wh;Q(^V+;L1dXfHc5HT%!|j;SbLWY!Hrp>lF<>&2l{@ROYm+r>E1Bjc7z}{(6Ih^_WS#-MAENLM>{a^qbzn}gn}(HXwnP~u{m1Ha zoXXOFe%KDMLcyC76dIZ)8Q(cHVB{y2Tnuy@PR)ueJ3*SS*c=QK8i;>;LB%LCZF64^ zq%CDNJl@1(edr=?R1WM*t)0}&ff2c0u(!C$Mi>4D`ve?tR$qe;&y7O%lm{DL;yKX? z)^OF>Ll#@h1a(QE-tENsHD`4pVOC9N{}E0M>Z>#>WJHe#Bom}9 zjCX^L@fSZBz1k%4;^Mk`&6O1~qNG@{>4o39YAp{HZ1<5NRya^e4d7Ao}opm)EyZ2+~E0S#obJiwavRJPDT?JZe?bFi>S)y% zm?kzX!^WA{!mLZi zwpL0N60!~CiuMn-{pR^T@Ra3lTJ|JG(x!rfjftIce8EGI7q2_l&DGPyKmfuRf+$Ru ze=xUe4HNGIVdcKk(JxXTO$WXIC`x0uX4L0RbLNxTZ@g|o7}o7W*!kQCvzcS>C8({} z!$_|C)s~Q#p^SGl_jQHSbe`knu)9`L>%{gC>A>Rk0NX!aDVA;;RUE*>{;D1tm0Pt6 zYY`fr*V@OfO`{eA2t_BvI?nDQWVFvW8Fla$U;hXMu5jt{7jvxR61S{Q+TSlR)O=R> zk>Eqio7dM7h8^QppDfxSR&-u~zy{H=j=JnDSCL9;O4Xiuv*@1}e(PDx|R4 zE&xy%IBd0r-kF#Ee|^KF@YHT6!uN`Ie)j70j6Zm7j$UzaFn(g4ZS;v7ca$#lA*_t< z^IJ_x!vp62QZ2bQl>zcrHWmvBc&KJ6+KIGm-b1}nwj0I8-Yu8}b)`KF^TXtSJ>c|q#5(6*% z%zusZlS>yh&aASm@2l$tsg?URPpo|?3Ley{?^N#*KA0Nzy7^a9+>Z-GxWC~nV?RcC zp8$za4@_%$4e-{CWI@^Mk|MI8u;N3t_W|(#q=NUY4m6Va){rmiHN+vNK@Rn zkn6IJf7X>sF;7z!Zx;zpYpV|`Y^|Xx{7Y3j3PX<>9EWIz9!=gt<2MTt^Xe|>T79aj zt$3^>jpF)v$_BN)5kpmXuTW^$xsPxBz-^(%GY<=xr!RfG3ig zEe(KEyv|YcMF0!iNB*>P`NxKUvjuw%P&fsn!MvSf^K}yDlNjp(sEsqIbok}N^}}mH z6QwVY#4Pr`o+<$O_}Hc8$I|w$0Mp)fJothqYnzK~5Puu6Gq_3fEDXjbg;zO*&ER)F ze0hJ{@w~VeLKX9&N8)+14TP<#^r^%Hy&^#v!ocgT)1t~B++HHP8+9_Gl(wKcu z+vFwtzH~FuI_;Ntwk1BlpIU41G2_hrnjL8Of}YMiyJXGLPcoz`usrlouKmpRCXZ?X z-Y7nzvn}!)3ouM?#W~n%X-a)kU&z)vu-jbAzn52ULwu*-xfP;x3)U8?9y10(7KF&1 zCE&uSAb$3JK{0Ms*2x~bl$~o;YwcaVyHIn`7b>C&61VNwThQ?|#w}l$9bj-*R;@c3 zR%cj99f2&Ywv$)VzH<3PR>N<9Bmy@_$N@%mx7eWMgT9GOTRObC(tNShnFxNqzl_u7&!wa|FO9#(5Op=}P(R9b0IB%)WtrD+2EXjFp`*?~% z`cAG76udXGp%~Hw_HC_1f{yhBK@7+9nNFLCM>g`ra7KRRswhW`E8Ov2P+U&eF}&ei ze-ZFiw-FQ%XGm#}x=7Y=|d#)4x&YE*1CP8lB}{E z=AAk6QH$@b#O2{zyM@@E9F(y$M-qO$l2S2y$I!~WOiYD#3z>{ z_AjF92MVmu6lSP`s9i5-R8m(PQ_o?eiFFq_)&*0H{(^`bhex<4Y@hf*Ni&%8#Z~M< zsj3{(q|5nLWMdeO|PHkG%yqObX2&BA4csrwrX%=$hJoz@KR2pR4w& zrz6{d3Wbp|YyWmk`UCo>&oF!m*Nq8r!ZEq0JDn}0YTAj3&3kGRC-T+sD`-wNN`s44 z_En1k7>h~e{4e?q|lFe*U{~dWcmIdv3v-{J@G!6qiE< z4nFCXpDFv^teT$(=q=L z8>cukzv{lv6@#h*x%yB)ZGqLIC~b^hC%dIHCg<09tBp@}tjmo!!RPgk&lO^BQ|?FI zq0ZFzsqC8`lml6w;!})iS+>&hoRs~In+w~ZXO*6)YF7-y-t!7_I3JC;+C$M(Fp z_>MF4zf(i-gc=4+8K|WdPWZZH-x7og3Ds(tciyLSb~E{sT{-g(Wz8S?MiCkl(+*{~ zxAtYpm(iPwD`UINGnexAxM!<4n_->vPVMn7I~DfWmb)icSSI?8*I1ta66C>p#$mR` zGBMxdu$Oyle>pdlNYnxprWHTwRZl6qsCL<>0@^CwO8iAC%@X|aDsBEwg}KKACMRza z^=c~k`4-uhyEB~s?bW%xpQujX&h>t#8Rbw}zi;n+*<+i*Ld}}w$n@J4Kk2QIjJdM( zX?I!TOM9GhL%?-U*c(h+;=1IpdU6DJjk)1-osP+zR>;@x>q1%Kk79CdidX!6#xads z9((V)jPKD$t#oncGreG0f^K__%bo>~w4AuYUw|pS*0RPyv1#G4Z#VG7{y^(QDf1G*wJ^JrATvo#9x8$H%Y*h$`RhV^T^IPF!Oi8uG ztOXo4m`GA~@ViT6ZuN5BPdMTAuR#1F{%03`PVME08V3jnGvWUWzW=9X@e6sST55dy z)_|{ZF^9`jDyb@5ul^wQ@UAR12bo03b?+F44fiZb|NGsA8xL&GNJ^&M)Y3f}E{azG z`~>?AayQ$a-uyQ=T6awAgW7x53x_EYp;;+MUSI5X4q@|`!0+>8(PHX&PFY!rMRoM!^#JOUFans0}X&vR%`A+ebjopySIb{ZVWXqcAn z!LxyKuU%Bg)Hey13Dt^-Cngt0XfHl){=9h8T=Y-q<5TFxPNu4nanzBzU@bsM}8l z-4k;Tx{YYddH~gO<7sX=)CE&p7Q#?pZ7b0>3c5`+O^<83XrjOiWCCdDI)z_Y#}cH9oK! zg4~x)R;#N=dbz(TfmOR0P6$v0x=j4xT=4He=2>^Jq<<14BoSRYCVA)WP0$zsiyIEfRUyn>*&uVsg%*H8fZ{IBY z16Bq2gZmNIZx-XP9KMUcF;D{;u_ScoTy%kDS;S4Fkd;;$lQKb?pnvF2jj`eniD>C^ zY2QQ}q#%S7h|_1p_@l{C)P}$$pvWUFoP_?7US0+VZQ7wF%5Zs80k5&l305e-Q#(&M z-lHe(y#vbUyMDi4hgGaG`95qRC)MtJiK9sPQBf+$Y{w1Wxo#)KHh~e&L1yf!yZiYu zhn|X!^bZ);u0T}u0^Es-26=%ZH!)56B%?GI9bof-Kt?B64?@Xn)@&13akTXwdmf3s zW7o+>?$Jp9Dt>7*F@1ASg~8%&eGjak-U?Xczw)nLckPE=cpzTILVTVu2wY#^=3W2% z{q~qs^fi%sf)r&6rJB|#Cl*i;1>W2P$Bpz#(jyXlGvJyzU-7e4sc!QMG8S!pcC#4Q33+{G6qAV_yo5U%1hP;wrk8LbtfKlA{8tsJaGMs zloF$A$UHk$0E2;Y46@bZwR0(Sy6ku%BRmDOTFg!;1tlGCxo2h0ST%zHjXxzMM=~28 zBST(ZFbJ$7U)|p%;B&F83!dz;hA!zKFm7>*1rVbamo7lf#XXYpyU+`C{Nmqa)S{39Z$B zg^V^O>ss*>%N$v&?`&fBIWU1woU>WUI!g04t$m z=LInqag5GFK#45lw@eJ0Nj8E%IEV`&Z)(0)&0RT~$@DCYI|jBa2c~Jx>x5B1kPzAk zn*F7D)KC^j+|X2*kS#>v5rY6`2lW>?T9RS#j%|N5n6Qn4b`nH(r#k5{_Tiu3R(nb> z^zhg@qgG4|YuYC8=;1I68HUp9^Ti8F=~<;++;5T4Twl+iKuip}MVfdmag@H8wl#^m z?rdn*h0OOB_7_TLkp}97cp_=8`EoP1qC+u)1t$uWA09U8V_%mDFe08=39QhBnKSIDaKimM#466XA)!G8m};>liA9@`5vVR<#dnTr@;dt}E*Z79r7d3L3!HlF z@90hgK!!ahcsq(V$ASgJ54X@%!r&lHq&h^5h5!WZJ3$X>{f{>KR~OElE&D>QE6N7M zSwlf_7()11Q2*T&cVTlkZEK9P752z5z9qW1ez6hP{yz0?dOVFQ* zImYZ#GZ4#)-kIh~mS|f}-6I@m{!~90lC|ImK@LngWKA=-H)70x6+cAj6(w&Cl67#0 ziAnu|vtLD`P(ZUwV6z0SslC9ty`@r+Y}Q0{@zJb5c0|6w4l<=jn{PHct&%}Si40Q& z6S?H)>K)5Se5#61xi233l zY1Mv^Dan?U^HwBs7c>JR_si&A6}#@5&T5PyE;c8S4BODHd4b?iv5aD%QvU$|MYMDp zP3N{9CmvAa=t9?jsA#~DF+vn+l=HOy{P)Db|Iy;>nvRzOn@RUTgu{6XW=o^18gkK? z92W=^pN}T8o=3o;Y5qYIJ=nu_Iw@?4n%C87hhB`l0B#OD^a*`8E2+v(VygObV%cB+ zQ{!Dnu&bmn>oZcbpIdXo@eg8NwXqlD5lqTeg&qV_;~~;A#7M!NVe>@Cq>Oye<}n3g z01xQbxbm2GCGfG8(+0bjT~S46=S(+Zx--J5chYK>^^l4-{(ytZ5J5`dBw~=pgQP`P zFt>gf%3VQcT)oc(-Ua@;snZEGDpJ%T`y>|9C3U`OQ=UjOH(^9&+y<$8h_PMjnym&Y zr0n?$39-^9;j_4-YRV%GO3eJtUF74284)IFXyQ>a{9iLH`U{IObtQ>d6D2QkSTrwv z-Lh*zY7bfbF19(J6EkczA!eG^Fs*Dia_8|7lYYBMz(>$(ts)4Q*8GSmttt5ZUsr2g zk*+Dll#*tauGu@8cKOWbR6X(gp(m4EZlEFP6&ttaTE?zC)Ktd^8Ms87p+_;M(e-j# z=LPH0vcX574j8>b+Nnr+>X(SbGpjZr*&vAGtS}H4bzjaAQjAI==eFfCklJvqq>5Tq zuY|Otsl6gjGAmI%XQmnFGrsu-QR8v*G}%83+p5njb+BwM#!n=U=gPU!N-Mm5_I5^Z z9)wdx@QQ?3I2ESTF{7Usowo+JY9!c?(mMDqIFq|6E}+&X~kskE_qDz@~dx z1L&53SF0nH)P~55ED_S!t z%Qh@&sB6JOXZjafL#ZuVCQ|oIL}Yhp)W_lC2f#FGE!ytN$a}jhzyvWWOW->&=>*QW z_UYG_=+pI;9Ezctyzmgqxixao3s9sgBq0}6n|Ob=J+sBLv}0(X$>F(i1yu;(;}bETp!>%8$Ci}gP>x#}bs)ApnKP*0yU0~=y#YIE6uF3^}ihW`Ob zsS#Tn-efIGH0C6W&u|<+qM$HBVCfB*(>g_bCjrat-sWAkuS?wu>;mWN)C9D6yxI3# zpbYgUCm*63TH})h@dF@_v-jCww$d)7SOH8$a%96RZ_ytvl=~_1h+-Q0?#@YyxdYY`P z70FNp+XUVG?N9A+w&oGb8#X=lCDwH-ewF1eyriCtiK!dM$kr0Oxxs*CZK-3e@){-8@#gcUOAQ0Deol#A8n@a8%YBDvO-D$o7=I@4c^t3)$KE7 z$m_fFFkaE3%I(H9d3$14argh@EjJbGKp{GwFA?DTTuu^ZvswMBfS0`veBOZX&X|Cg zW@s*h6>%U8#SNqM0BPk{-hq67X7kmUUG~V_)jCjO#(=NaMWTHxl zIv-mFRY2_wH^jtgg&8+|Jcz@PbW~+GkKYmwoxarM%ox=c6Js5!-$)gEjglN@>MkiR zve{=w`w1^_ir(aYMQCJw1s2(;5^L58*7{fT0Fb486DR}AZiJgTlm0_JlvW6vXa)fl z{(7yUgB0(U*inRS^F2`+3;C_lQHxihh<1lZZ?#vQ|S>vd%GE+e^Gj_>6;r-6*vN^+jvpvse% zZGb`BGiA?H&yO3MM308$5go0(!zeRpMk8~AMW+%5lGHnr+Oj>u;z9n0aJnbW;s|>g zDkd$QSn6%x)FEwKBW@IgG*rKrYHb*Ifc(lzK5Z zO?Kf~GgwEBOQ3;lmeV}9p_&4k?5<8dbo3cjwd9y}P#~Y_MK)L4xux@$Z+p>|@QNO! z-2U2B@0iEn`ltJ5?#5Bok;lDHXPt;sU&{_EVXJ^!CSccQMEJZiylC8iz(S4ob#yp( zUwyJSC@6Yxn8=dZutJ>7F z451FB&6esBeUsKf%NTILqhpo!V*Yq=k-}>ppL-jPS)$lN6_TQ;`(ZuSKz2S$AEUEP zAk&142GtALcm$JmfWDPS$lTaxUB2UAppKV-@BqacrXu5B(7_=fW}XDSz$)Kk4%x3) z0dZOQWD$~kCbp~92JDlsA3lg_k8nEAtjyG+Kd z=Jd$;TnIoda49m%X-O$|f(T=m1FYv^{{^>Y$8TwVh5@Nl)1I@ScPpbocpLXj<>Uan%WhCVcod!$8Xa<+91p=8ipIAYD@XuIi3c`f@{wK8OHi5}GUp zMK=Q~v5E6v55_2>5w?UHT=?zZgJh1@xppT>V4Mlo2(s0^XBEod&}zVQIo&3ctB$q3}j1OL}tF}C!G3N zr)}@LS!$A~DhTFa0o;bCjP85Yhz#v_IoAEXSj5jNdx~Ny7Z(-|Sq9^KsyL0RTFh-z zfX_bQdnJHS;$xft3i$o5LlkN(oWp^HF`0Nf%(*EM#?mXH-}~ix&0}gj*0y^ynw=9N z@a{r^0E%ZV$`X&)cBV%k82$>Zu``KmK>ON)o3BMIY`vf>tUciOIQ}$%|K~2?nqrs# z=lt~jPk6x3zp!7t*q;$Zg}z>cQjV=Bcbm8yH|nTK4A{3Gy4*D>p<& z&v49{Z$@>~7MIzMri(Myd#P7{pJ9VfU7;(xS_)z|k`y#WFj#KU4xw5A53gE*H0G*Z)#A^)GE}<=DZ8nx(5|l!&+?3J);aG{!lz8MIV& zF&4)tbgEZ$ymQPG4XRio0`2$H0lWS_lIpUc{aa?XQ&$iP-Kd*9qMfbg)G^xtUZ7vx z{m!!+USNbM-!hNTbHEsJ#&>z)j_1$#&Yc1^&y9BgA7MX#0z)6PL&aw_tih{QYk{G!>D33&e=HppcAg7;jKXjib(#4aO5! z)GDhRJbl2R35D}vZiLTK9(-E<0`UsdBYE-S_diRhTGUBsggBa34abx>sN+9FbZZEt zY&wTzlUcM6Fgki$)Z;8M*T@WS4~@dJKlio@Do$-=k1Rm;ncx>`97*q;6R0X14^-bn ziiJB_X5PU5)f$L(75KgjIW4hCJecYoUH%h;x)o{1saMNNt*&96bvm*hHeov=bSZ#{ ze<*s{GRHa5E}!U8Tfv7eZvl$aCu6iS_xu=}d*3szgUuc^@sUbo*ne7^TjbWd7*67C zu{-*Fl$2+63mNFl9prbp6PviUH#s5qNbms?j;&R3K6*`Ug~mGCWOm#>VzwSGwv)Kr zHH?HyN*LV%?yi!g!x6X4acn z(1O&(Y23G5*uQ58-|J{snX`o{j%4#!ja{^T5$E7=%lLEHRT=VqZdtRl^cqk9r5YwYU7?t_`Z!W z#8P98i(0A)qUHdKRtl9QD=v8Jqbq4Q--4cpX$4)lc=NeYD&T+HpAVh>7#_hsHAe|gg zg@-hl^88xTUyoMDIW&6NgN|Vg(0~r$Hg~l<( zRs)*r{TF(_@=98QjG!;u39jZbt+K=P{U_(!(LxwXsKDPEBSJb zGAW+&tqJaqEYF=2hqg}&eym-Ae)0!$zINTGc2(Rsg5KEH-+jnC(fyLsMc}B^OPHXA zOycdtCviqDuD;8|mnC!FyZKiHMH?XauW|7RAD_L&%|ACr;aTh5iBF0@KuHa%JI}I1vUwD>qi4c-=-!;)V@jvCxR93gyB`*(K{PYiEB(|ns{*$Yzq4Xk#z3R=;IG1{U^D{~v(MyBM;u9%*UeSZ5-dYYJT~0!Dz_p+n+&BNZ}M zq;8uW4w)W~zA2u#b{QPfoyFJxO@HM_eFxobwQ{J#CePGey;lolz{i70l_EZ}VuefY zJ8NW<$eaSnUoU2clEB+AMTNk$!`N>3Ab3El{tXqDK!I+`t;LX+l7 zPsvZk3xi$LL&d5Y=W5AiB-Ms69fUf+4<3kGpiU$#%dMN%R)^_Mo2KuztrevkKQO2bcnM6<523G`Ms)U$TS8sC-ekB9E>n_(_;NaR!jdR=T}7)o=M zRf!HADSn+Iy)W09U)w|a^0b}d69e!iS1n1*R7`!tfH6}-N)%vK=baQt`*$wWu5u|W z!^}Akp3y;fm0bf*T!4B26)CvlB*QG)Cl2#A+VI}#qUn$ z1}w$^2$)?`($XDKoTQ<)EBSkQUD$!a(uEB)_fkM1HJys z503Bm%D0cfPw85BKz#0B6J!}CBdwvPPQ!2+{;+_#WGnYkl|g-SFW!CZ0b@kOc>{jq zyK+e_6xg&9+r$!(zy>E23oFhkmN$B3B<%cBVOr6gNq&=4VWc;}8s>1Z2|GN=AbFuG z!{C{Q5xa!o25-t0wbX7SRp3FgRD{T5L@1MQqhdsf@t8^uVobMg>Ar1V;}LZj)%>v8 zpF!O{**OCo-I8FL;vwDuNoPKIi?J*u$SVP~^L40l8 zqnq2x>0d_yDGN}R{y}KP=clg@3CTkqhXYAT=#mdML>0n2%h5L!N1R7wL!5gWU4Ct1 zZSR47YH;oGW>E6yO%g1RSePViFr$MTP(fCL=DoWd_<7Km+5PvsFQA|ve0|^7k6CaZ zA5V98Pj4n(evA6~^~Fm7=5X}>rOMkS84vGw(D&Be(IA)-P5X~hI;WJFD42z3uxm^%j9ui7ol-YDb6^47Q)H?~vc1VvepJ>K4m zQpkr6Ptl-8>l3Tr$32>7e5KfNLITmgHwY)KY(HyrVHTmpRcFW z`Omu^-=*IJDxa_Sy?X=5_Fl~dM<3AD%2n-0nMtyyPiHKzPaEfVE zOWOLpnCE7s%nOA1&5@eolQ2ZXzf(>)^g`(p6BbGe&4yqCJO_fr28T_h3YnomB&-S>Hl_ zM<`~{`Ci~X<8kr(-|LQVEN>=s?L#bt{!lWARzTM5%pdhwk3YS}s*efgdShhCE{dOm zRg|5=V3xJ4zp7wuVt+HV)JVkky{Rrj8)p=Ic^sw{FQbx07jL{4ZeODzr<#oW!z|z~ zKx02OviN2QXBf*#vz4^#&G@5)xgf=zB0PWdM8<-H-Oyl#C{CVfX*U?WeX>LaE&igP zxAV|_6c8`m%_#`@xv>QMDrG@%(xu-OFvAW!(6Ii+8yQO|M9OByl2B)3DMBXlu~~(P zA<}NFMymCThd*Qn`_yJX&1};%EZ9i0a3SS0D$0Q==}+^7?oZk8;pQzaM;KU;U_6u& z3Rt>m3^y`(Lfo4LyCU_b8IYrz%(<6}QecE4V0wBDq+|y3fnsSOY_-WC2NH>{zgijHXc$E+HoPL3pr_tu{`5D^b&%z}A_^EywI03E~(&SpfRk^htUsi;Bq za;2m!xc(IH4$cd01^=CySpcnpgA9$Pq+z)m)d*F@hWC5>Zuo6vMjkt)%*X0vXh}#C zA@c*qb@di2>fx{K0p@{tnS}Jk@=|eEEFMU-o(HWZ07Z%fvQceKye~QO&-H1RT%p;t&f^*su5u=&A>lleY31(sV^WGpDA`(s* z+yDdQMb=SuBcWKk&xk%>2lh-M-6#iGU*0J*Lr1qZ&*=jeqJS8iIj-pfgWnj0Atl6{ z?(uZJ7DNE7S`B-bK+lSIQi|cCB_J@=^f%47Ni@K#cLhxeb6xL-mO3)h>0iXEnH`Oz z@L6RRLZStk7J4+QE>yvJ@iM9WK0KH$W0m0|is^4-H4I>nvt_YuQMBI1!kyV}Pyh0Je1^zPI*I)d6g-h^)r@~UInyFBY_0?2@^C7;7wZ89ZcCK^iK1$ z<614oagksrT7_Y8mw^$RoR%oVJXj-?g@F3AWN|j#ug$B7Bd0eT`YG)?iqyW_{dT@* zQ8-!DH&(6Uyk>>4sZ4a$gP$&M5{emK9X)+FD2xvL!zHnrvrXZfXkK z8)j-}4h>av%DzWMQ3m3COQn2wmoGhS&y6~38;DT8k%t<)j1SUEd^D|kA22IKybz8m zs>S}C1nizb^fJEKWl}Y5hXp-S*;{fVETp>S7FZ#cp0y&2v_Kb#!G12@R+Aitp1$*plNg*o=AON%={8|6BAbEJ z@Vl&%vkp&?h^dJ?zer8_(~dlpWiMk#JqWYQ}QY z7XcP2m9z1@+`>40c zgow}d!L+_mN|@ECiF+vPBh`GzAqg?f@J~CHvv;P+ot)a)Sy`1r7QO2#YnOuROvvwr zCKkCb~N5#^^yao|YTIf%8Ic#o{KULP?SKzcj zZFUWJT8AZ~ml$9rX@}qRMUt*nlF5v8sA*9<`eE@}i)UgW2mT4&s@##uw}!0OMo8(Yj#NZY*xigEV356P zdKfEGBu<>j4hKOOD32*hJ>|MEiRk`aCQxjo6X3g$HwVyOF#MICDd*c^S7Ekf^zi!} z9D5b}Lg8|;c6S+Z$tzz(n!^+^Mt*`^dwlMSQayX5xz(!xl7AzfRgA@Sjp_)t_d!1S zrY3aVtNcjPV$-5j5!d*Ik7V4k8FKX8%d8JY57w2H8u}%x@ncsB+5?nR3qUDSKR+V# zV$D@QrSEe#%S5YCMG`S0;526v@md%Zq3nU3XutBs?Ze#UdK;HM4QBjq?tju4h`q?b zeaFd*d4kz&=bdtMdDOZ!rDCAyJpvx;oKMl}zDSk>x6shpT)B>R_KuKGf6gtJy+=(g&S%3hZr+3pZ>CAZ;HXnumQZWWUEtJN|7bdXaR zMk^9ve1&`c;dL$`Ux3|f@srNV<@{l{@XR%!h&vKJ8h#1uLHOy3+R0XrsD`ewiVO3u z8I!!mHeB59k)kE~9m%iXrk=Pstk-Kt?u{c64fl*#g^ST8XW?Nc-bTwQ)C4PRH->*@ zvbiPXUu6Y*lorbBm2h;jct|qX`^2LxEGyR&)?cVw5_S(L#N*TK+)36E@u|>1B~{nG z8ZVHHq~nDx6&B%pTWm#48Wzu)_0HTn=H-E9&W(oeSTk;fLtZ8CV_fzEdfhA6_n21? zS%uSEC!*20(pP~et`c@BAZnSsB=&&SP`bOo$lg&k{ipB@raIJ_u0XN^UX<5;82!!w zXYv=B)3bW2HxdFS@zBuiKXGJ&3PQiWPPP>*hD*11ymxc!EZfQqE79|4QL`;`%cC}| zOQ2w9xSQ7W(OdShw_W>I>%$-EaeUH_!%Pc$;wpTa%1>^8u*~{`_mOrDRn{b=J;%HG z6o;C(SXm%O*JY)@k;O$I{n3>DYNKO4ehRvJvrR7i(JIe&?3WmQH=_Fn+RZ!ouCit! zZObD9lpCaN*J=jjWTHcVq&G(HJcGLkuHO{JT3xhb3P7>T;NWY+*TI+3t>+Ty;4##i z7g$v3iwaw04W{E<=^9mS;*&{iuyAj$YE)q)h+%HpBBIzKRsQrW=_%J!aSGk7k{E%? zdEy3DSNs^(hiOR;*SWA*t7Sem7NLCk0P}%4(=xtQF}qI?S3pZaq%Hh3dznGWZH??F z&P>N_Vr*>r{moo=M?3n|&fC?c(xqmjp!!VFq7H4z#Wc1w1+ALNa#m%F_jmf#te^_! z@h#DveQ&*rx}H?&@hnk_s!)7zU?5x&+B;NR={RH^H?C4?qhn@B&T}33IVseg;qhTJ zecC_k4S3;!G-+F}n}m8P;<)X7ZFfYhllog%2p&d`_W_8e&AzE&?}Az z_lBb|@Oqm{4Y_{RmJyt|NeA0Ia-l1%^1xq;Z%cjnRG@^8e!>jS$@ zAOVUm`-AKK(HOL%6uB!AEFNoI*x3b-C%gT??8+Fc-6>yI8;(GLbyd;Yx5e%#lFRuf z&5xDqYQ-Rv(i99kAN|mFbI>(ahxFZnMj3Jkc)gBq%b$N3G`_9^?eL7Yaq=PY4$Gww zq!TR0@+>!N-W;yy5IrUZ?-kvzrRk#s2FP~ugJVflJ6qOMQJ=gG)43<|0ZQ`B78v3YH?!< zEOvadb=;hMTD7~d9N12`UxI#`pfWXcJ-neIgZZ6DQ0IUj_boO1OwR##&mZDhQ1~aJ zP#V}0_R@&slsYrmTuh(!ov^fj7JCUkQEOG)6vdLAj#`cyI#i<#894Yu6RFUNJC`9i zdI9pf^q~Q{@njZBDIe0pvE5HhwV^M7Y}b^VsT52Ldfsy|Xqrq#`VLi5L`SwUR)PjrXad&Sa@P8(!*M??h+HcLSHXL7 z@_^yO zGk9JyRBb*}kea2m8?%OX-_X%H-Iqev5ythJ`(|Lui)H-IaTW9HPxR)bJr%p8Jf~Uu zVG2{PR%1mwm!s_FM3)^J#OT+?eQvyJshnmDs@$<1Ivqoo%b+OED`#0Q4=v!_{h_bB zLfZcZ{sPQd`ZG~%t?;u&?N8^K*_#=5KWF>tY7PAPRouwqc!%DM3)^X$7K32EfzkS` z3}bop3UlPQX!~7~-@E7-eZ!mPV5f3|11AYldyq+d4sz8Si#;Fb_%LmZ>fD1{F^kJt zj)mIc!>FmAyUg`XdSqyS5ccn8`Ha`y6$*Rc z3KqP9X>s6pe`o2)qbl-_VO*(y3FCpbsAykR$ZIhdkwq_b1@Fi;+-nvZ+FrPU<3o?$ z@*=%oItsdmU8rCu;c1*m#1`EiVx3+}$j)mFPCI~9WuO_NW~3SnF|`j671=Mn%u(YI zwu@OL8LNaUns;E|$kC+r$hMd1l3X$gt|dGa;6=g8Q8tKO^l~=azzAsy?&tH85ev4w zQ}L?QpZL}u(~k``T-nMLGYr#L#@Pl>2`6%2_h=ZpbtEY9)zKC6DMY9}R*tC@=!KAk z#Fd7U)8xL-dNE0oW+@sQu}m&l)y%zPOd543M5kiL+|qBq!BzKeJ{3x^a=Lwk0dbaq z@q$ls#@c;g4z`oqN}X8@#6}lt)0DCdk`5j*SR8Y-3buTZOWwmvnSGn9Lm~%|nL1a< z8z`dF65GHtoMBqEIH07teMhz9D~=U?CYj`h-9k#erU_%yzQuaPSqe3Hcg-fp&!-u+@0{($INaqJAF4l*_)rQ9TseRS8=Mc>$ zDI);MsL2%i?X(E&)uF6bxi8U@&wPvyIZEi%`_Z24aaB{8vB{7#HarpXI$?va+kC@$ zM-fbFw3VM#M#ne7sm|OM>P$VwQ`Slw9?D~dyj(lU(5%&K6 zJrNqxgN5zIj_Ht@{*dWu`Q)kMuF^O7%#$duFj_xClL61(eb?gZ_}~<^9PiH>#bW-* z9o~mQty>(fF@aO>sYp$jLmn&(4G)szm!Qc>_gLY{WCi4OZXtp0b}}b|LW7)eo~U8G zPBr|||4nmz{{`>(LWt)BE0vmrNN2gm6lIRVYdyxfKFsfQvpM3;K4S!bGsyd9k9XfX z`)x-1SMrax5hfAW(b=+>AxZyH-D<>p)&oN zR_5WeG992NEf%#(TQ3&F&m5aQ6a0mgM;--r!_E-4CvA+}gJfOod^pkmy%ML9b*X8O z<)8c*Q-(C8N37dz#!u&d(IraK>;4WG_(pWUa?5^=*KbHDa-UEp$!|b+-+CCHKRnv^ zl()iXit($RUe(W%(}#!2U%yy9sHZlP%@JT*9ie^vX1*m-l_jxTuHvbv?`MLa-j~PU$;F9Hl0nC@{o?_Q+^KB&wgd-NhrQ9xoG{P06pI0Y0-49^1#MNUwMQiOGN231$3Hs z1ERzWRK@Q%PO;&>`})#*X7W9VqsSfYdE009v{=R5R_&6!cKDlBqV*XM6eXJBbxddu z_cSsYLnDjGl#H@5Y$R3hE49917aAY$PS&8S{z}@6-EH;qS|1usOC2iwTmhnPi}&!T z28bl1j-NtB02JTSYKmA}J7PaN#F)$+vr-g=!kwQ8W@F->`3Z80ZvhIcH)I*cvMA=I z7I#HWKTuUSIiASu)Gt>y3ITzJ16T&{wb$=`Y%qjInCFtTDcQ8sjdHTa`!n4shQqhOQ0Y!+i$UQ+QIzG0&bBEtx_j;snT9kA5;s zH%ngMz2gPKS{5vTrz`j=dd>q{6@{G|D&LCJZygWtQ|kq4>;Vp_bzqg0Oh9Q)V2)tM z_97-#{kG=En~j-YT%^8FVb5Ng9Rh+Bmud+eUu)u`3n33G+^37s0!$ zyi~}@02-O8(Shu3o&wJQNRDrEN~i!00GPxB0B&7z*3#9^_UvqBfwmgB{{g<2+;_3g z{b@i>u;QU3u{!Y=l2leqn=i}T4(4D&?6$}6mTUKScf0bb-n>e6OpV4G?d=NKP zTsuBy%(!Pb(qi$HwCFI9V=7tHduJ`&=;uC9trBgbE>$GY>`?r(n&aX%WUPm;__L1=d)sWHY7=qmxYO9jb2ipAiGOz3yo|R)^9&1+B*x!neY^ zL2>Ev-&+n8gNzvLyTT@fOElxYv$9Vsv^sovtEC|S>}e>v`a(3zcY1vq`v&$`gJTgn z+!oa;8=`e_PK;$C*Fv_v7+5ym@c|QMnP%dHP!8I7WaN&eoVQ=u`5laZ8`K~Bz9VbL z%oC=Ncdu~!NxBnyc4NioeraIDnnh&@lBoS-Cd+B!m1qk|%TYeMxg@cfyrVMbyJDEl zxw!`lfL-MhtT#s=6pMx95_eH~zt(iC+p%XKXfvW=7}?6tH4^8bz0A!0z9Ao+zSXFD zWF~HQx-iLV7~h{jm(i*^wv7JKm^g(b5&hm(bZZ z&l7LdXDuv5(bLwCDQegYaa8mUXR<<1mmvGTaQN;U4uHp$8^#uG=RY8B5(l?>vwTOF zkwCpeJ3|kochg$bJbJcvggz$328q*B08}cUkp1~2mK1s0{R^#Cf8WtghtT9W5q7u0 z^9BQNz%cRF2lx+H;%8QyJO{g*bGBQHN1HFh@U9!*q7Iner%&+-pHmleb`W1F-k#Ac z`C^~SKbD%{jt|8$pi*>j5c53#(PeBpq=V+K-M#X;e$uL^so7Aq@fp5afyV5W9Irhmg2v zlRfdr3k=bjnS?u}&(Re$&||nyjR{u))``Bvi3f3?-6w{u4_C<=t$EUUe49Jp>UUU& z?yF^0F&E5uamjS$i-C&-j{OuW^V1d&@Gg1{rKzFr!y}I|ues~O$)lBc zNyn+B_l$(aMBy31=`X{dpTGrCOO8D#(5FGK^L1;AV|{M(PEz+)sb|Cz#A*f@ElF;= z{%{A@eXgszlf+3NV~GDkql>Tb6>mmG?Is=bkhW}r(OuZANR}aIXW00U1CYpCif2@# zxCq8Ocg_V(;g(_T(u{5yq**RWBEozCu%*U$lH^SL#FVWmjdG=Kxkz>!72# z?d!y^lW%8UG}t6Mp~#QOzV(Xg>uMua<@WhFa@eJD?W3Qtz=%zD;j**a>g0UlCfXL- z&;EL;CEp-V=4aiHqT`U!bZ#^7C#3~h$=KlX`ZzPYxgHJtbWzzi-M~-#F23jVgfB** zX-tz`NWJ`HZ|}SKn{A~~;9FqwMg+43=q84F+7c;x^=wVCIVT!4-Egy2zC6KQta}L> zJdd#_Pzqb$dd{W!YOa}ZMY!S_@On$?4*Qfe(-FNjkq0tIgY;BRY~CI2jatPja2;2Y zf#}bOWUHy=(=p~cU}NxgSysjRC&H~rzDSEzz&~JlqcR7f<`gC4FnFF^S5-tp zCPq1n^#Fh`BGv=`v0(yG|MLkSuqtu(t}LS|!7i^N%W={U0Q_C_uZw4YKR|9UI!h0T zC-FZp>AtN0EUGM_A}=eWp?OzX_MgfhUr>JSzn{*O*Znu`{IiVDg)(^O%6$D-s=BJO zMCZz4BV8VJCvvq3bd(R`J07Mz` za}A&RFN!S477BE{Yi?`mXx3bOw-3>i=&I)RXY z2-xj&uqu>GU|@Sokkzkg(NaM-qY&jnF#rJCbF@Cxi!?c)JW2(6Xa2V?|p6!D}uhor~3h`hAAXCU?ngBv3V$r=HoIR`Sv zy##bsZ#V~3u8SaAFBf z^>sjbdnH0Ti$pBvpt?7%hF)zfOk@6AEP`o_Xlwd&+(#sraAjQ0fexW9H=5d9z0ul58a$9CjE z1Y8B>VjKH1T&>^=tLIT7n}6-L%1l>7uQt{%G2Ph~(QlfFfWUl?t9a*X?juK22;?%X z3p9h#A7Sbw!Y$~|VNcjE#X3R2_D;ZG1LeOQCoduzlosn^{hK*{1H95uvMzzgt_Ud# za51}V@%)R6$W*pKO>4wRuuukD0BtXcFeKL1P(;015zTt%9I}Y_B2vp9A%bj8&1`|c zi3WwYxx?QeCT~Z$7bg)QfnUz4q`=_Epi5d)Qb%Od7?Dy?sV@dA3DIA|9w1(>%!C?a zH*R?%D*XacY1VVZ?@GTTUKtGXkW|nV5p$74M7_DsfwLa|A^1uMXoX4V#US`Rh+S;w z_>!8x=U?TrWx<6mn+TU3z`hu0Xm$UTeDxqp2)~h!7-ZJ}N;#H>S153Dy41ku-4Z+ALU9b3h=EUVab}@p zeJNQB3bJ+jH}7m=8o%={`)tjXi*_-KlsNp7_lIi~HN7tIHw5<(^_Y2OT!Y zKB>whoYfT3QG)02cbflS@aDE4#8CT{mS5ef{%C$t_)D+9p@AewfBL(xiC}N^?e#ZrS`2RoK!*jVkf3*GS-(B3y{_lgPx`e!vj5-a?{~o3J MA94U|Nu(MA0HhRM5C8xG literal 0 HcmV?d00001 diff --git a/rpm-core/src/test/resources/signature-my-ring-test-1.0-1.noarch.rpm b/rpm-core/src/test/resources/signature-my-ring-test-1.0-1.noarch.rpm new file mode 100644 index 0000000000000000000000000000000000000000..cd2757602c6ecd26db72df87cc93ef114e73fcbf GIT binary patch literal 1752 zcmaF6`rU121`sIDOwUU!DJ@FX&8^fe%FIjGElDjd(KXaF&@}`rAWSeau%avKTl<0$ zD#p&hz%&8KwgX}iy8%cGFfcIkK=?qCfl(fad4UWz2%7<@m-z*d&jRFofD{1vuYfdA zKcf#s4f7iapD7zi1I=T852S(WnQDMEV<)2w3j-q)0~;$-8MrtFc3tui`=FDn zBgMu3&o3+`z4Mgc8A{FvP73{%m{S*j`1Le}L+p#>`Q~4{ z#9)%+BqjGK<3P;a4SM|2T+RaLo>i;NO=%Nbw6dvj!{S$4)OVccT_{?|zxZAGop|~0 zSvwuouS9;f{=-swT&=@$UCzJMGv2MSn<(~qZIW}u`=E)BIUe6EXUV?KuxYlw z^`s@5x*8P=LT(<&Oo}v0OR+RIOfs}IGDx+sNJ%qHGD|TvFikNwN;EM}NlY=bG)yxv zvPcF7C36Ozu>91-!LVsozFWY<_w#mtna$Stx zjO2`aNe_hY`A?X>=-1}W7TrxEN+!)l_;t>y|C=G(s`~Uy{ zL1|wADL+U6ljj1Um^~1K(lb~d$W8%b4j={rSo#O%2L>i6|0R$H@g<>rP<{aMWug4n zKpL3-85E%WcTm0xln=@uAoUth{zo7UQf~s~e*@AWc?&52FO+Ws<^O~7ogjP`aUczH zuP2lb^cUE?1RxFc7YoRpAbC(&fcR1nKBE?t4{`_4KE^m84fHQd1ds;0hcN}pj|9>{ z{frqV(P=49j-mjE?^fO~)sLr!L1X$3=GeqvE_1}R#ZgHN G6wv@M#oeO- literal 0 HcmV?d00001 diff --git a/rpm-core/src/test/resources/signing-test-1.0-1.noarch.rpm b/rpm-core/src/test/resources/signing-test-1.0-1.noarch.rpm new file mode 100644 index 0000000000000000000000000000000000000000..afa6ce0ba3e9720dbfa6556edfea1517f8c60c21 GIT binary patch literal 1736 zcmb7_3rtg27{_lP0yQdiZWE;u(cvmWbK84Mi-VypMIJs1B9t%|OM8oj(iU2sbea@I zq#C5^B14NBbRsf3U@nY7WH61JWM*#QaL&na8!{CYqgyhh`%Zghx-Cm~nsfi>_dU+( zJ^9YLcbmqpu`tvu`aGlFm=|fKEY?V&0FM-U&Hv}Yz^45@d~fqD25s}lFs2j6uR}*; zJ17^!7&CkfPmB=)eI^Y2d}9o9S+{`Mz=l{)WE%+{QE)%IH!{0rR(;v^ z&V}T`iprLqwYvRs;?0Y@OXBVAF}o_QS1trZOq*&z@|Vlcb+aqyrzG#|5TCEP{DyyNRLwG+r%vcHWOtv;Y~ z$XtTZ=m@n|8UE(NEeCE0zaT%A6=VOa#;*yeQS@&<-88-nEP4ouG} zUEA=UT| z7#n(}{%x`Uqa*DFnQdRZ`rXE>(u9ur?uc+e-I`(6!J5priJ*{^2}eOqxeQ5eL$Y&I95(G~Wh=`o~~?Z~hp~Jetw@ zK=BJ{egKN%BWQjMiu^duPiQ7-o}_uXkJ)oU(f?gZGg=qTr-Tmg#hwR>{05rm`F!MvDA8n)7K!_X3JX>mxr>#1_Qz z=k7fW?mcYk-h&I{uEC^H8#b9NRtQFIm2x#^&>J^z#f&C(v1Zfr>D~pBVUd6B#pXhT zK9`?C6p7a`VL{~phz9md>{ yJ*ZSrBX9})$Q6DSjwODBrJ=KmQkdgOctWD{ctdr8v%Y>23r@?Km5W|RhwM+yRLY