diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 52a970a..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -patreon: stringcare -custom: ["https://www.paypal.me/efraespada"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..57995d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,101 @@ +name: πŸ› Bug Report +description: Report a bug on stringcare +title: "" +labels: ["bug", "bugfix", "priority: high"] +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: | + Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues. + required: true + + - type: markdown + attributes: + value: | + --- + + - type: dropdown + id: plugins + attributes: + label: Which gradle tasks are affected? + multiple: true + options: + - stringcareTestReveal + - stringcareTestObfuscate + - stringcarePreview + + - type: dropdown + id: platforms + attributes: + label: Which architectures are affected? + multiple: true + options: + - arm64-v8a + - armeabi-v7a + - x86 + - x86_64 + + - type: markdown + attributes: + value: | + --- + + - type: textarea + attributes: + label: Description + description: | + Describe the issue. Explain what you _expected_ to happen and what + _actually_ happened. + validations: + required: true + + - type: textarea + attributes: + label: Reproducing the issue + description: | + Please provide either **steps to reproduce** or a [**minimal reproducible example**](https://stackoverflow.com/help/minimal-reproducible-example). + Providing a minimal reproducible example will help us triage your issue + faster. + validations: + required: true + + - type: markdown + attributes: + value: | + --- + + - type: input + attributes: + label: stringcare Version + description: What version of stringcare is being used? + placeholder: "master" + validations: + required: true + + - type: markdown + attributes: + value: | + --- + + - type: textarea + attributes: + label: Relevant Log Output + description: | + Please copy and paste any relevant log output. + placeholder: | + Paste your logs here. Please redact any personally identifiable + information. This will be automatically formatted into code, so no + need for backticks. + render: shell + validations: + required: false + + - type: textarea + id: comments + attributes: + label: Additional context and comments + description: | + Anything else you want to add for this issue? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/chore_task.yml b/.github/ISSUE_TEMPLATE/chore_task.yml new file mode 100644 index 0000000..c50e534 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/chore_task.yml @@ -0,0 +1,79 @@ +name: πŸ”§ Chore Task +description: Suggest a maintenance or internal improvement task +title: "" +labels: ["chore", "maintenance", "priority: low"] +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: | + Please search to see if an issue already exists for what you are proposing. + options: + - label: I have searched the existing issues. + required: true + + - type: markdown + attributes: + value: | + --- + + - type: dropdown + id: chore_scope + attributes: + label: What area does this task affect? + multiple: false + options: + - CI/CD + - Dependencies + - Code Refactoring + - Repository Configuration + - Other + + - type: markdown + attributes: + value: | + --- + + - type: textarea + attributes: + label: Task description + description: | + Describe the chore task in detail. Explain what needs to be done and why. + validations: + required: true + + - type: textarea + attributes: + label: Current issues or inefficiencies + description: | + Describe any problems this task is addressing. Why is this necessary? + validations: + required: true + + - type: textarea + attributes: + label: Expected impact + description: | + Explain how completing this chore will improve the project. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives considered + description: | + If you considered alternative solutions, describe them here. + validations: + required: false + + - type: markdown + attributes: + value: | + --- + + - type: textarea + id: comments + attributes: + label: Additional context or comments + description: | + Add any additional context, logs, or examples related to this task. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..5be8308 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Git-Flow + url: https://nvie.com/posts/a-successful-git-branching-model/ + about: Check the original article by Vincent Driessen about git-flow before opening new issues. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/doc_update.yml b/.github/ISSUE_TEMPLATE/doc_update.yml new file mode 100644 index 0000000..a207b07 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc_update.yml @@ -0,0 +1,71 @@ +name: πŸ“ Documentation Update +description: Propose changes or improvements to the documentation +title: "" +labels: ["documentation", "docs", "priority: low"] +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: | + Please search to see if an issue already exists for what you are proposing. + options: + - label: I have searched the existing issues. + required: true + + - type: markdown + attributes: + value: | + --- + + - type: dropdown + id: docs_scope + attributes: + label: What part of the documentation needs an update? + multiple: false + options: + - README.md + - Wiki + - API Documentation + - Inline Code Comments + - Other + + - type: markdown + attributes: + value: | + --- + + - type: textarea + attributes: + label: Describe the documentation update + description: | + Provide a clear and detailed explanation of what should be updated or added. + validations: + required: true + + - type: textarea + attributes: + label: Why is this update needed? + description: | + Explain what issues or gaps this update is addressing. + validations: + required: true + + - type: textarea + attributes: + label: Additional resources + description: | + If you have any external links, references, or examples, include them here. + validations: + required: false + + - type: markdown + attributes: + value: | + --- + + - type: textarea + id: comments + attributes: + label: Additional context or comments + description: | + Add any additional context or suggestions related to this documentation update. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..b53a7ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,111 @@ +name: ✨ Feature Request +description: Propose an idea or improvement for stringcare +title: "" +labels: ["enhancement", "feature", "priority: low"] +body: + - type: checkboxes + attributes: + label: Is there an existing issue or feature request for this? + description: | + Please search to see if an issue or feature request already exists for what you are proposing. + options: + - label: I have searched the existing issues and feature requests. + required: true + + - type: markdown + attributes: + value: | + --- + + - type: dropdown + id: feature_scope + attributes: + label: What area does this improvement affect? + multiple: false + options: + - Performance + - New Feature + - Preview + - Other + + - type: dropdown + id: feature_actions + attributes: + label: What actions does this improvement affect? + multiple: true + options: + - Reveal + - Obfuscate + + - type: markdown + attributes: + value: | + --- + + - type: textarea + attributes: + label: Description of the idea or improvement + description: | + Describe your proposed idea or improvement in detail. Explain what you + would like to see and why it matters. + validations: + required: true + + - type: textarea + attributes: + label: Current limitations or challenges + description: | + Explain what problems or challenges this improvement would address. + Why is the current functionality insufficient? + validations: + required: true + + - type: markdown + attributes: + value: | + --- + + - type: textarea + attributes: + label: Expected impact + description: | + Describe how this improvement would benefit users or developers. + Include specific scenarios or use cases where this would make a difference. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives considered + description: | + If you have considered alternative solutions or approaches, describe them here. + Why do you think your proposal is the best option? + validations: + required: false + + - type: markdown + attributes: + value: | + --- + + - type: input + attributes: + label: Version of copilot + description: | + What version of copilot are you using, or does this proposal apply to all versions? + placeholder: "master" + validations: + required: false + + - type: markdown + attributes: + value: | + --- + + - type: textarea + id: comments + attributes: + label: Additional context or comments + description: | + Add any additional context, screenshots, or examples that may help us + understand your proposal better. diff --git a/.github/ISSUE_TEMPLATE/help_request.yml b/.github/ISSUE_TEMPLATE/help_request.yml new file mode 100644 index 0000000..5773828 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/help_request.yml @@ -0,0 +1,70 @@ +name: πŸ™‹ Help Request +description: Ask for help, guidance, or clarification about something in the project +title: "" +labels: ["help", "question", "priority: medium"] +body: + - type: checkboxes + attributes: + label: Have you checked for existing help or discussions? + description: | + Please make sure no one else has already asked this question or opened a similar issue. + options: + - label: I have searched existing issues and discussions. + required: true + + - type: markdown + attributes: + value: | + --- + + - type: dropdown + id: help_area + attributes: + label: What area do you need help with? + multiple: false + options: + - Reveal + - Obfuscate + - Preview + + - type: markdown + attributes: + value: | + --- + + - type: textarea + attributes: + label: Describe your problem or question + description: | + Clearly describe what you need help with. Include context, what you've tried, and what you expected to happen. + validations: + required: true + + - type: textarea + attributes: + label: Steps or attempts you've already made + description: | + Describe what you’ve already tried to solve this problem. This helps others avoid suggesting the same things. + validations: + required: false + + - type: textarea + attributes: + label: Environment or context + description: | + Provide details such as OS, version, branch, or any relevant environment information. + validations: + required: false + + - type: textarea + attributes: + label: Additional context or screenshots + description: | + If applicable, add screenshots, logs, or any extra information that may help others understand the issue. + validations: + required: false + + - type: markdown + attributes: + value: | + --- diff --git a/.github/ISSUE_TEMPLATE/hotfix.yml b/.github/ISSUE_TEMPLATE/hotfix.yml new file mode 100644 index 0000000..b9780e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/hotfix.yml @@ -0,0 +1,64 @@ +name: πŸ”₯ Hotfix Issue + +description: Request a new hotfix for stringcare (only team members) +title: "" +labels: [ "hotfix", "branched", "priority: high" ] +body: + - type: markdown + attributes: + value: | + ### ⚠️ Disclaimer + > **Only members of the stringcare team can create hotfix issues.** + > Any hotfix issue created by someone outside the team will be closed automatically. + + --- + + - type: input + id: base_version + attributes: + label: Base Version + description: | + The base version is typically the most recent tag version. However, you can specify a different version tag if you'd like to start from a specific version for this hotfix. + placeholder: "e.g., 1.2.3" + value: "Automatic" + validations: + required: false + + - type: input + id: hotfix_version + attributes: + label: Hotfix Version + description: | + By default, the version will increment the patch number of the most recent tag version (e.g., from 1.2.3 to 1.2.4). You can specify a different version number for the hotfix if needed. + placeholder: "e.g., 1.2.4" + value: "Automatic" + validations: + required: false + + - type: textarea + id: issue_description + attributes: + label: Issue Description + description: | + Provide a detailed description of the issue this hotfix is addressing. + placeholder: "Describe the issue being fixed." + validations: + required: true + + - type: textarea + id: hotfix_solution + attributes: + label: Hotfix Solution + description: | + Explain the solution being implemented in this hotfix. + placeholder: "Describe the solution." + validations: + required: true + + - type: textarea + id: additional_context + attributes: + label: Additional Context + description: | + Add any additional details or context about this hotfix request. + placeholder: "Anything else to note?" diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml new file mode 100644 index 0000000..99251d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -0,0 +1,51 @@ +name: πŸš€ Release Issue +description: Request a new release for stringcare (only team members) +title: "" +labels: ["release", "branched", "priority: medium"] +body: + - type: markdown + attributes: + value: | + ### ⚠️ Disclaimer + > **Only members of the stringcare team can create release issues.** + > Any release issue created by someone outside the team will be closed automatically. + + --- + + - type: dropdown + id: release_type + attributes: + label: Release Type + description: Indicate the type of release. + options: + - Patch + - Minor + - Major + validations: + required: true + + - type: input + id: version + attributes: + label: Release Version + description: The new version is generated from the release type and the most recent tag version. You can specify a different version number for the release (e.g., 1.2.3). + placeholder: "e.g., 1.2.3" + value: "Automatic" + validations: + required: false + + - type: textarea + id: changelog + attributes: + label: Changelog + description: Provide a summary of the changes to be included in this release. + placeholder: "Add a concise changelog here." + validations: + required: true + + - type: textarea + id: additional_context + attributes: + label: Additional Context + description: Add any additional details or context about this release request. + placeholder: "Anything else to note?" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..633a41b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,144 @@ + + +# πŸ“Œ Summary + + +--- + +## 🎯 Related Issues / Tickets + +- Closes # +- Related to # + +--- + +## 🧩 Scope of Changes + +- Added: +- Updated: +- Removed: +- Refactored: + +--- + +## πŸ› οΈ Technical Details + + +--- + +## πŸ” How to Test + +1. +2. +3. + +--- + +## πŸ§ͺ Test Coverage + +- [ ] Unit tests +- [ ] Integration tests +- [ ] End-to-end (E2E) tests +- [ ] Manual testing only (explain why) + +--- + +## πŸ“Έ Screenshots / Recordings (UI changes only) + + +--- + +## ⚠️ Breaking Changes + +- None + +--- + +## πŸš€ Deployment Notes + +- [ ] Requires database migration +- [ ] Requires environment variable changes +- [ ] Requires feature flag toggle +- [ ] No special deployment steps + +Details: + +--- + +## πŸ”’ Security Considerations + +- [ ] No security impact +- [ ] Input validation changes +- [ ] Authentication / authorization changes +- [ ] Sensitive data handling changes + +--- + +## πŸ“ˆ Performance Impact + +- [ ] No performance impact +- [ ] Improves performance +- [ ] Potential performance regression (explain) + +--- + +## πŸ“ Notes for Reviewers + + +--- + +## βœ… Checklist + +- [ ] I have self-reviewed my code +- [ ] Code follows project standards and conventions +- [ ] Tests have been added or updated +- [ ] Documentation has been updated (if applicable) +- [ ] No new warnings or lint errors +- [ ] Changes are backward compatible or breaking changes are documented + +--- + +## πŸ“š Additional Context + diff --git a/.github/workflows/copilot_commit.yml b/.github/workflows/copilot_commit.yml new file mode 100644 index 0000000..2b5387a --- /dev/null +++ b/.github/workflows/copilot_commit.yml @@ -0,0 +1,23 @@ +name: Copilot - Commit + +on: + push: + branches: + - '**' + - '!master' + - '!develop' + +jobs: + copilot-commits: + name: Copilot - Commit + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: vypdev/copilot@v2 + with: + debug: ${{ vars.DEBUG }} + project-ids: ${{ vars.PROJECT_IDS }} + opencode-model: ${{ vars.OPENCODE_MODEL }} + token: ${{ secrets.PAT }} diff --git a/.github/workflows/copilot_issue.yml b/.github/workflows/copilot_issue.yml new file mode 100644 index 0000000..4c1b9b4 --- /dev/null +++ b/.github/workflows/copilot_issue.yml @@ -0,0 +1,21 @@ +name: Copilot - Issue + +on: + issues: + types: [opened, reopened, edited, labeled, unlabeled, assigned, unassigned] + +jobs: + copilot-issues: + name: Copilot - Issue + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: vypdev/copilot@v2 + with: + ai-ignore-files: build/* + debug: ${{ vars.DEBUG }} + opencode-model: ${{ vars.OPENCODE_MODEL }} + project-ids: ${{ vars.PROJECT_IDS }} + token: ${{ secrets.PAT }} diff --git a/.github/workflows/copilot_issue_comment.yml b/.github/workflows/copilot_issue_comment.yml new file mode 100644 index 0000000..0c23c36 --- /dev/null +++ b/.github/workflows/copilot_issue_comment.yml @@ -0,0 +1,24 @@ +name: Copilot - Issue Comment + +on: + issue_comment: + types: [created, edited] + +jobs: + copilot-issues: + name: Copilot - Issue Comment + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: vypdev/copilot@v2 + with: + ai-ignore-files: build/* + debug: ${{ vars.DEBUG }} + opencode-model: ${{ vars.OPENCODE_MODEL }} + project-ids: ${{ vars.PROJECT_IDS }} + token: ${{ secrets.PAT }} + bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} diff --git a/.github/workflows/copilot_pull_request.yml b/.github/workflows/copilot_pull_request.yml new file mode 100644 index 0000000..ccf1288 --- /dev/null +++ b/.github/workflows/copilot_pull_request.yml @@ -0,0 +1,21 @@ +name: Copilot - Pull Request + +on: + pull_request: + types: [opened, reopened, edited, labeled, unlabeled, closed, assigned, unassigned, synchronize] + +jobs: + copilot-pull-requests: + name: Copilot - Pull Request + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: vypdev/copilot@v2 + with: + ai-ignore-files: build/* + debug: ${{ vars.DEBUG }} + opencode-model: ${{ vars.OPENCODE_MODEL }} + project-ids: ${{ vars.PROJECT_IDS }} + token: ${{ secrets.PAT }} diff --git a/.github/workflows/copilot_pull_request_comment.yml b/.github/workflows/copilot_pull_request_comment.yml new file mode 100644 index 0000000..a521f9e --- /dev/null +++ b/.github/workflows/copilot_pull_request_comment.yml @@ -0,0 +1,25 @@ +name: Copilot - Pull Request Comment + +on: + pull_request_review_comment: + types: [created, edited] + +jobs: + copilot-pull-requests: + name: Copilot - Pull Request Comment + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: vypdev/copilot@v2 + with: + ai-ignore-files: build/* + debug: ${{ vars.DEBUG }} + opencode-model: ${{ vars.OPENCODE_MODEL }} + project-ids: ${{ vars.PROJECT_IDS }} + token: ${{ secrets.PAT }} + bugbot-fix-verify-commands: ${{ vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS }} + \ No newline at end of file diff --git a/.github/workflows/hotfix_workflow.yml b/.github/workflows/hotfix_workflow.yml new file mode 100644 index 0000000..e631b92 --- /dev/null +++ b/.github/workflows/hotfix_workflow.yml @@ -0,0 +1,115 @@ +name: Task - Hotfix + +on: + workflow_dispatch: + inputs: + version: + description: 'Hotfix version' + required: true + default: '1.0.0' + title: + description: 'Title' + required: true + default: 'New Version' + changelog: + description: 'Changelog' + required: true + default: '- Several improvements' + issue: + description: 'Launcher issue' + required: true + default: '-1' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + prepare-version-files: + name: Prepare files for hotfix + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate inputs + env: + VERSION: ${{ github.event.inputs.version }} + ISSUE: ${{ github.event.inputs.issue }} + TITLE: ${{ github.event.inputs.title }} + CHANGELOG: ${{ github.event.inputs.changelog }} + run: | + err=0 + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Version must be in semver format (e.g. 1.0.0)." + err=1 + fi + if ! [[ "$ISSUE" =~ ^-?[0-9]+$ ]]; then + echo "::error::Issue must be a number (e.g. 123 or -1)." + err=1 + fi + if [[ ${#TITLE} -gt 1000 ]]; then + echo "::error::Title must be at most 1000 characters." + err=1 + fi + if [[ ${#CHANGELOG} -gt 50000 ]]; then + echo "::error::Changelog must be at most 50000 characters." + err=1 + fi + [[ $err -eq 0 ]] || exit 1 + + # Example: generic step to perform the version update or compilation (uncomment and adjust as needed) + # - name: Generic step + # uses: whatever/action@v2 + + - name: Commit updated package.json and dist directory + uses: EndBug/add-and-commit@v9 + with: + add: './build/ ./package.json' + committer_name: GitHub Actions + committer_email: actions@github.com + default_author: user_info + message: 'gh-action: updated compiled files and bumped version to ${{ github.event.inputs.version }} (hotfix)' + + tag: + name: Publish version + runs-on: ubuntu-latest + needs: [ prepare-version-files ] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Copilot - Create Tag + uses: vypdev/copilot@v2 + if: ${{ success() }} + with: + debug: ${{ vars.DEBUG }} + single-action: 'create_tag' + single-action-issue: '${{ github.event.inputs.issue }}' + single-action-version: '${{ github.event.inputs.version }}' + token: ${{ secrets.PAT }} + + - name: Copilot - Create Release + uses: vypdev/copilot@v2 + if: ${{ success() }} + with: + debug: ${{ vars.DEBUG }} + single-action: 'create_release' + single-action-issue: '${{ github.event.inputs.issue }}' + single-action-version: '${{ github.event.inputs.version }}' + single-action-title: '${{ github.event.inputs.title }}' + single-action-changelog: '${{ github.event.inputs.changelog }}' + token: ${{ secrets.PAT }} + + # Example: generic step to perform the deployment (uncomment and adjust as needed) + # - name: Generic step + # uses: whatever/action@v2 + + - name: Copilot - Deploy success notification + uses: vypdev/copilot@v2 + if: ${{ success() }} + with: + debug: ${{ vars.DEBUG }} + single-action: 'deployed_action' + single-action-issue: '${{ github.event.inputs.issue }}' + opencode-model: ${{ vars.OPENCODE_MODEL }} + token: ${{ secrets.PAT }} diff --git a/.github/workflows/release_workflow.yml b/.github/workflows/release_workflow.yml new file mode 100644 index 0000000..7baf951 --- /dev/null +++ b/.github/workflows/release_workflow.yml @@ -0,0 +1,179 @@ +name: Task - Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version' + required: true + default: '1.0.0' + title: + description: 'Title' + required: true + default: 'New Version' + changelog: + description: 'Changelog' + required: true + default: '- Several improvements' + issue: + description: 'Launcher issue' + required: true + default: '-1' + publish_maven: + description: 'Publish library to Sonatype (requires NEXUS_USERNAME, NEXUS_PASSWORD, signing keys); set to true to run' + required: false + default: 'false' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + prepare-version-files: + name: Prepare files for release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Validate inputs + env: + VERSION: ${{ github.event.inputs.version }} + ISSUE: ${{ github.event.inputs.issue }} + TITLE: ${{ github.event.inputs.title }} + CHANGELOG: ${{ github.event.inputs.changelog }} + run: | + err=0 + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Version must be in semver format (e.g. 1.0.0)." + err=1 + fi + if ! [[ "$ISSUE" =~ ^-?[0-9]+$ ]]; then + echo "::error::Issue must be a number (e.g. 123 or -1)." + err=1 + fi + if [[ ${#TITLE} -gt 1000 ]]; then + echo "::error::Title must be at most 1000 characters." + err=1 + fi + if [[ ${#CHANGELOG} -gt 50000 ]]; then + echo "::error::Changelog must be at most 50000 characters." + err=1 + fi + [[ $err -eq 0 ]] || exit 1 + + - name: Update VERSION_NAME in gradle.properties + run: | + sed -i.bak "s/^VERSION_NAME=.*/VERSION_NAME=${{ github.event.inputs.version }}/" gradle.properties + cat gradle.properties | grep VERSION_NAME + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build all modules + run: ./gradlew clean build --no-daemon + + - name: Run unit tests + run: ./gradlew test --no-daemon + + - name: Commit version bump + uses: EndBug/add-and-commit@v9 + with: + add: 'gradle.properties' + committer_name: GitHub Actions + committer_email: actions@github.com + default_author: user_info + message: 'chore: bump version to ${{ github.event.inputs.version }} (release)' + + publish: + name: Publish to Maven + runs-on: ubuntu-latest + needs: [ prepare-version-files ] + # Enable when secrets NEXUS_USERNAME, NEXUS_PASSWORD (and signing keys) are set; then set publish_maven to true in workflow_dispatch + if: ${{ github.event.inputs.publish_maven == 'true' }} + env: + VERSION_NAME: ${{ github.event.inputs.version }} + ORG_GRADLE_PROJECT_nexusUsername: ${{ secrets.NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_nexusPassword: ${{ secrets.NEXUS_PASSWORD }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Publish library to Sonatype + run: ./gradlew :library:publishReleasePublicationToSonatypeRepository --no-daemon + env: + ORG_GRADLE_PROJECT_nexusUsername: ${{ secrets.NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_nexusPassword: ${{ secrets.NEXUS_PASSWORD }} + ORG_GRADLE_PROJECT_signing_gnupg_keyName: ${{ secrets.GPG_KEY_ID }} + ORG_GRADLE_PROJECT_signing_gnupg_passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Publish plugin to Sonatype + run: ./gradlew :plugin:publishPluginPublicationToSonatypeRepository --no-daemon + env: + ORG_GRADLE_PROJECT_nexusUsername: ${{ secrets.NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_nexusPassword: ${{ secrets.NEXUS_PASSWORD }} + ORG_GRADLE_PROJECT_signing_gnupg_keyName: ${{ secrets.GPG_KEY_ID }} + ORG_GRADLE_PROJECT_signing_gnupg_passphrase: ${{ secrets.GPG_PASSPHRASE }} + + tag: + name: Publish version + runs-on: ubuntu-latest + needs: [ prepare-version-files ] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Copilot - Create Tag + uses: vypdev/copilot@v2 + if: ${{ success() }} + with: + debug: ${{ vars.DEBUG }} + single-action: 'create_tag' + single-action-issue: '${{ github.event.inputs.issue }}' + single-action-version: '${{ github.event.inputs.version }}' + token: ${{ secrets.PAT }} + + - name: Copilot - Create Release + uses: vypdev/copilot@v2 + if: ${{ success() }} + with: + debug: ${{ vars.DEBUG }} + single-action: 'create_release' + single-action-issue: '${{ github.event.inputs.issue }}' + single-action-version: '${{ github.event.inputs.version }}' + single-action-title: '${{ github.event.inputs.title }}' + single-action-changelog: '${{ github.event.inputs.changelog }}' + token: ${{ secrets.PAT }} + + # Example: generic step to perform the deployment (uncomment and adjust as needed) + # - name: Generic step + # uses: whatever/action@v2 + + - name: Copilot - Deploy success notification + uses: vypdev/copilot@v2 + if: ${{ success() }} + with: + debug: ${{ vars.DEBUG }} + single-action: 'deployed_action' + single-action-issue: '${{ github.event.inputs.issue }}' + opencode-model: ${{ vars.OPENCODE_MODEL }} + token: ${{ secrets.PAT }} diff --git a/.gitignore b/.gitignore index f6877dc..7616b83 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ library/.externalNativeBuild *.iml *.DS_Store .cxx/ -*.cpp \ No newline at end of file +*.cpp + +.env \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..82ad0a6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,5 @@ +# JNI native library (private repo). To init: git submodule update --init --recursive +# If the repo URL is not accessible, place the JNI library at stringcare-jni/ (copy from ../stringcare-android-c) or ensure library/CMakeLists.txt path is correct. +[submodule "stringcare-jni"] + path = stringcare-jni + url = git@github.com:vypdev/stringcare-android-c.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a8f6320 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing to StringCare Android + +## Release workflow secrets + +To run the **Release** workflow and publish to Maven Central (Sonatype), configure these GitHub repository secrets: + +| Secret | Description | +|--------|-------------| +| `NEXUS_USERNAME` | Sonatype/OSSRH username | +| `NEXUS_PASSWORD` | Sonatype/OSSRH password | +| `GPG_KEY_ID` | GPG key ID used for signing (e.g. short key id) | +| `GPG_PASSPHRASE` | Passphrase for the GPG key | +| `PAT` | Personal Access Token with repo scope (for checkout and release steps) | + +When dispatching the release workflow, set **Publish Maven** to `true` to run the publish job. The workflow will: + +1. Build and test all modules +2. Publish `dev.vyp.stringcare:library` and `dev.vyp.stringcare:plugin` to Sonatype (when publish is enabled) +3. Create the Git tag and GitHub Release + +Local publish (for testing): + +```bash +./gradlew :library:publishToMavenLocal +./gradlew :plugin:publishToMavenLocal +``` diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..5491223 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,72 @@ +# Migration from StringCare 4.x to 5.0 + +## GroupId and artifact IDs + +- **Old:** `io.github.stringcare` (or `com.stringcare`) +- **New:** `dev.vyp.stringcare` + +Update dependencies and plugin coordinates: + +| 4.x | 5.0 | +|-----|-----| +| `io.github.stringcare:library` | `dev.vyp.stringcare:library` | +| `com.stringcare` plugin ID | `dev.vyp.stringcare.plugin` | + +## Gradle (Kotlin DSL) + +**Before (4.x, Groovy):** + +```groovy +buildscript { + dependencies { + classpath 'com.stringcare:gradle-plugin:4.x' + } +} +apply plugin: 'com.stringcare' +dependencies { + implementation 'io.github.stringcare:library:4.x' +} +``` + +**After (5.0, Kotlin DSL):** + +```kotlin +plugins { + id("dev.vyp.stringcare.plugin") +} +dependencies { + implementation("dev.vyp.stringcare:library:5.0.0") +} +``` + +Resolve the plugin from Maven Central or a local `includeBuild`; see [README](README.md#installation-kotlin-dsl). + +## Plugin configuration + +- Extension and task names are unchanged (`stringcare { ... }`, `stringFiles`, `assetsFiles`, `srcFolders`, `debug`, `skip`). +- AGP 8.x and Gradle 8.x are required; the plugin uses the Variant API. + +## Library package + +- **Old:** `com.stringcare.library` +- **New:** `dev.vyp.stringcare.library` + +Update imports in your app: + +```kotlin +// Before +import com.stringcare.library.SC +import com.stringcare.library.SCTextView + +// After +import dev.vyp.stringcare.library.SC +import dev.vyp.stringcare.library.SCTextView +``` + +## API compatibility + +Public API (e.g. `SC.reveal()`, `SC.obfuscate()`, `SCTextView`, resources usage) is unchanged. Only package and Maven coordinates differ. + +## Build / CI + +This repo uses the JNI native library as a Git submodule. Clone with `git clone --recurse-submodules` or `git submodule update --init --recursive`. In GitHub Actions use `checkout` with `submodules: true`. diff --git a/README.md b/README.md index fb30c2b..52c2e35 100755 --- a/README.md +++ b/README.md @@ -1,49 +1,107 @@ - -

- -

StringCare Android Library

- -

- -#### [What Is StringCare](https://github.com/StringCare/AndroidLibrary/wiki/What-is-StringCare) - -#### [Implementation](https://github.com/StringCare/AndroidLibrary/wiki/Implementation) - -#### [Strings Usage](https://github.com/StringCare/AndroidLibrary/wiki/Strings-Usage) - -#### [Assets Usage](https://github.com/StringCare/AndroidLibrary/wiki/Assets-Usage) - -#### [Configuration](https://github.com/StringCare/AndroidLibrary/wiki/Configuration) - -#### [Publish APK](https://github.com/StringCare/AndroidLibrary/wiki/Publish-APK) - -#### [Limitations](https://github.com/StringCare/AndroidLibrary/wiki/Limitations) - -#### [Compatibility](https://github.com/StringCare/AndroidLibrary/wiki/Compatibility) - -#### [Tasks](https://github.com/StringCare/AndroidLibrary/wiki/Tasks) - -#### [Resource Tips](https://github.com/StringCare/AndroidLibrary/wiki/Resource-Tips) - -

- -

- - -

- -License -------- - - 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. - +[![Maven Central](https://img.shields.io/maven-central/v/dev.vyp.stringcare/library.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22dev.vyp.stringcare%22%20AND%20a:%22library%22) + +**StringCare Android** v5.0.0 β€” Compile-time obfuscation for strings and assets; runtime reveal in your app. GroupId: `dev.vyp.stringcare`. + +Full documentation is in the [docs/](docs/) directory (getting started, configuration, API, publishing, troubleshooting). + +

+ +

StringCare Android Library

+ +

+ +### Installation (Kotlin DSL) + +**1. Plugin** β€” In the project root `settings.gradle.kts`: + +```kotlin +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + } +} +// If publishing locally: includeBuild("../path/to/stringcare-android/plugin") +``` + +In the app (or module) `build.gradle.kts`: + +```kotlin +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("dev.vyp.stringcare.plugin") +} + +stringcare { + debug = false + skip = false + assetsFiles = mutableListOf("*.json") + stringFiles = mutableListOf("strings.xml") + srcFolders = mutableListOf("src/main") +} +``` + +**2. Library** β€” In the same module: + +```kotlin +dependencies { + implementation("dev.vyp.stringcare:library:5.0.0") +} +``` + +For **Groovy** use `buildscript` / `apply plugin: 'dev.vyp.stringcare.plugin'` and `implementation 'dev.vyp.stringcare:library:5.0.0'`. See [Migration from 4.x](MIGRATION.md) if upgrading. + +#### [Flutter Support](https://github.com/StringCare/stringcare) + +#### [What Is StringCare](https://github.com/StringCare/AndroidLibrary/wiki/What-is-StringCare) + +#### [Migration from 4.x to 5.0](MIGRATION.md) + +#### [Implementation](https://github.com/StringCare/AndroidLibrary/wiki/Implementation) + +#### [Strings Usage](https://github.com/StringCare/AndroidLibrary/wiki/Strings-Usage) + +#### [Assets Usage](https://github.com/StringCare/AndroidLibrary/wiki/Assets-Usage) + +#### [Configuration](https://github.com/StringCare/AndroidLibrary/wiki/Configuration) + +#### [Publish APK](https://github.com/StringCare/AndroidLibrary/wiki/Publish-APK) + +#### [Limitations](https://github.com/StringCare/AndroidLibrary/wiki/Limitations) + +#### [Compatibility](https://github.com/StringCare/AndroidLibrary/wiki/Compatibility) + +#### [Tasks](https://github.com/StringCare/AndroidLibrary/wiki/Tasks) + +#### [Resource Tips](https://github.com/StringCare/AndroidLibrary/wiki/Resource-Tips) + +**Build / CI:** This project uses the JNI native library as a Git submodule (`stringcare-jni`, e.g. stringcare-android-c). Clone with submodules: `git clone --recurse-submodules ...` or run `git submodule update --init --recursive` after clone. CI workflows must use `checkout` with `submodules: true`. + +**Plugin host natives:** With submodule `stringcare-jni` and `dist/` built, the plugin mirrors the whole **`dist/`** tree (e.g. **`macos/`**, **`linux/`**, **`windows/`**) into the JAR on each **`preparePluginNativeLibraries`**. To sync that tree into **`plugin/.../internal/jni/`**, run **`./gradlew syncPluginNativesFromDist`** (root) or **`./gradlew -p plugin syncDistNativesToPluginJni`**. macOS: universal `libsignKey.dylib`; Linux/Windows: x64 + arm64 `*.so` / `*.dll`. + +**Publishing (release workflow):** See [CONTRIBUTING.md](CONTRIBUTING.md) for required secrets: `NEXUS_USERNAME`, `NEXUS_PASSWORD`, `GPG_KEY_ID`, `GPG_PASSPHRASE`, `PAT`. When dispatching the release workflow, set **Publish Maven** to `true` to run the publish job (publishes both `dev.vyp.stringcare:library` and `dev.vyp.stringcare:plugin`). + +

+ +

+ + +

+ +License +------- + + 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/app/build.gradle b/app/build.gradle deleted file mode 100755 index 57b192f..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' - -apply plugin: StringCare - -stringcare { - debug true - assetsFiles = ["*.json"] - stringFiles = ['strings.xml'] - srcFolders = ['src/main'] -} - -android { - compileSdkVersion 30 - - defaultConfig { - applicationId "com.efraespada.stringobfuscator" - minSdkVersion 15 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" - } - - flavorDimensions "type" - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - debug { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - productFlavors { - prod { - dimension "type" - } - dev { - dimension "type" - applicationId = "com.efraespada.otherobfuscator" - } - } - - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } - - aaptOptions { - noCompress "json" - } -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - implementation 'androidx.appcompat:appcompat:1.3.0' - testImplementation 'junit:junit:4.12' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'commons-io:commons-io:2.5' - implementation project(path: ':library') - // implementation "com.stringcare:library:$stringcare_version" -} - - diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f5c98a8 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + id("dev.vyp.stringcare.plugin") +} + +stringcare { + debug = true + skip = false // Obfuscation runs when native lib is available (x86_64 or arm64); otherwise tasks skip automatically + assetsFiles = mutableListOf("*.json") + stringFiles = mutableListOf("strings.xml") + srcFolders = mutableListOf("src/main") +} + +android { + namespace = "com.efraespada.stringobfuscator" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "com.efraespada.stringobfuscator" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + debug { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + flavorDimensions += "type" + productFlavors { + create("prod") { + dimension = "type" + } + create("dev") { + dimension = "type" + applicationId = "com.efraespada.otherobfuscator" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + androidTestImplementation(libs.espresso.core) { + exclude(group = "com.android.support", module = "support-annotations") + } + implementation(libs.androidx.appcompat) + testImplementation("junit:junit:4.13.2") + implementation("org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}") + implementation("commons-io:commons-io:2.15.1") + implementation(project(":library")) +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 959232f..3ab71c6 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> - + diff --git a/app/src/main/java/com/efraespada/stringobfuscator/MainActivity.kt b/app/src/main/java/com/efraespada/stringobfuscator/MainActivity.kt index b96076e..8e287e4 100755 --- a/app/src/main/java/com/efraespada/stringobfuscator/MainActivity.kt +++ b/app/src/main/java/com/efraespada/stringobfuscator/MainActivity.kt @@ -4,9 +4,9 @@ import android.os.Bundle import android.view.View import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import com.stringcare.library.* -import com.stringcare.library.SC.Companion.init -import com.stringcare.library.SC.Companion.reveal +import dev.vyp.stringcare.library.* +import dev.vyp.stringcare.library.SC.Companion.init +import dev.vyp.stringcare.library.SC.Companion.reveal class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 69297f6..1f816d3 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -29,7 +29,7 @@ android:padding="25dp" android:textColor="@android:color/black" /> - - - ("clean") { + delete(rootProject.layout.buildDirectory) +} + +/** Dev: copies `stringcare-jni/dist` into the plugin’s `internal/jni` (included build `plugin`). */ +tasks.register("syncPluginNativesFromDist") { + group = "stringcare" + description = + "Copy stringcare-jni/dist β†’ plugin internal/jni. Run after building natives in stringcare-android-c." + dependsOn(gradle.includedBuild("plugin").task(":syncDistNativesToPluginJni")) +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..1e44761 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + `kotlin-dsl` +} + +repositories { + google() + mavenCentral() + gradlePluginPortal() +} + +// Dependencies for build logic; gradleApi() is included by kotlin-dsl +// Do not add AGP/Kotlin here to avoid classpath conflict with version catalog diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000..29744ec --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "buildSrc" diff --git a/buildSrc/src/main/kotlin/.gitkeep b/buildSrc/src/main/kotlin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/buildSrc/src/main/kotlin/Publishing.kt b/buildSrc/src/main/kotlin/Publishing.kt new file mode 100644 index 0000000..6095152 --- /dev/null +++ b/buildSrc/src/main/kotlin/Publishing.kt @@ -0,0 +1,76 @@ +package dev.vyp.stringcare.build + +import org.gradle.api.Project +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.plugins.signing.SigningExtension + +private const val GROUP_ID = "dev.vyp.stringcare" + +/** + * Configures POM, Maven repositories and signing for an existing Maven publication. + * The project must create the publication with from(components[...]) before calling this. + * @param artifactId Maven artifactId (e.g. "library", "plugin") + * @param description POM description + * @param publicationName name of the publication (default "release"; use "plugin" for the Gradle plugin) + */ +fun Project.configurePublishing( + artifactId: String, + description: String, + publicationName: String = "release" +) { + group = GROUP_ID + + val publishing = extensions.getByType(PublishingExtension::class.java) + publishing.repositories.maven { + name = "sonatype" + url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = project.findProperty("nexusUsername") as String? + password = project.findProperty("nexusPassword") as String? + } + } + + afterEvaluate { + val pub = publishing.publications.findByName(publicationName) as? MavenPublication + ?: return@afterEvaluate + + pub.groupId = GROUP_ID + pub.artifactId = artifactId + pub.version = project.findProperty("VERSION_NAME")?.toString() + ?: project.version.toString() + + pub.pom { + name.set("StringCare $artifactId") + this.description.set(description) + url.set("https://github.com/vypdev/stringcare-android") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + + developers { + developer { + id.set("efraespada") + name.set("Efra Espada") + email.set("efraespada@gmail.com") + } + } + + scm { + connection.set("scm:git:git://github.com/vypdev/stringcare-android.git") + developerConnection.set("scm:git:ssh://github.com/vypdev/stringcare-android.git") + url.set("https://github.com/vypdev/stringcare-android") + } + } + + if (project.hasProperty("signing.gnupg.keyName") || project.findProperty("signing.gnupg.keyName") != null) { + val signing = extensions.getByType(SigningExtension::class.java) + signing.useGpgCmd() + signing.sign(pub) + } + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6399a7e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# StringCare Android Documentation + +StringCare Android provides **compile-time obfuscation** for strings and assets in your app, and **runtime reveal** via a small library. The Gradle plugin (groupId `dev.vyp.stringcare`) obfuscates resources during the build; the runtime library reveals them using the app signing certificate. + +## Documentation index + +- [Getting started](getting-started.md) β€” Quick path to your first obfuscated string (dependency, plugin, `hidden="true"`, `SC.reveal()`). +- [Installation](installation.md) β€” Kotlin DSL, Groovy, Maven Central vs local build, and version alignment. +- [Configuration](configuration.md) β€” The `stringcare { }` block, strings.xml attributes, and asset patterns. +- [Library API](library-api.md) β€” Public API: `SC.init`, `SC.reveal`, `SC.obfuscate`, `Version`, `SC.Assets`, `SCTextView`, extension functions. +- [Plugin tasks](plugin-tasks.md) β€” User-facing tasks (`stringcarePreview`, `stringcareTestObfuscate`) and variant tasks. +- [Build and CI](build-and-ci.md) β€” Building the repo, submodule clone, and CI (checkout with submodules). +- [Publishing](publishing.md) β€” Release workflow, secrets, and local publish (`publishToMavenLocal`). +- [Migration](migration.md) β€” Upgrading from 4.x to 5.0 (groupId, plugin ID, package, Gradle/AGP). +- [Architecture](architecture.md) β€” Mono-repo layout, library vs plugin, JNI, and Variant API. +- [Contributing](contributing.md) β€” Release workflow secrets and local publish steps. +- [Troubleshooting](troubleshooting.md) β€” Submodule, signing, plugin not found, publish job, JNI/NDK. +- [Verify obfuscation](verify-obfuscation.md) β€” Comandos `gradlew` para comprobar que strings/assets se ofuscan (nativas del host, `syncPluginNativeLibraries`). + +For the main project README and badges, see the [root README](../README.md). diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..f8662c4 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,31 @@ +# Architecture + +## Repository layout + +The project is a **mono-repo** with: + +- **app** β€” Demo Android application. Applies the StringCare plugin and depends on the library. Included in the root `settings.gradle.kts` via `include(":app", ":library")`. +- **library** β€” Android library (AAR) that provides the runtime API (`SC`, `SCTextView`, Version, Assets, etc.) and loads the native library. Built with CMake; native source lives in the **stringcare-jni** submodule. +- **plugin** β€” Gradle plugin (JVM). Built as a **composite build**: root `settings.gradle.kts` uses `includeBuild("plugin")`, so the plugin is not a subproject but a separate build. It is applied to the app by ID `dev.vyp.stringcare.plugin`. +- **stringcare-jni** β€” Git submodule containing the C++ implementation used by the library (and optionally by the plugin for fingerprint/signing). The library’s `CMakeLists.txt` references `../stringcare-jni/lib` for the native source. + +## Library + +The **library** module: + +- Exposes the public API in package **`dev.vyp.stringcare.library`**: `SC`, `SCTextView`, `Version`, `ContextListener`, extension functions, etc. +- Loads the native library **sc-native-lib** (built from **stringcare-jni**) via `System.loadLibrary("sc-native-lib")`. CMake is configured with `cmake_minimum_required` 3.22.1, C++17, and the JNI source dir set to `../stringcare-jni/lib`. +- Responsibilities: **reveal** (decrypt strings and assets at runtime using the app signing certificate), **obfuscate** (encrypt strings at runtime for the same key), and **asset** access (JSON, bytes) for obfuscated assets. + +## Plugin + +The **plugin** module: + +- Implements the Gradle plugin entry point **`StringCarePlugin`** (implements `Plugin`), creates the **`stringcare`** extension (`StringCareExtension`) and registers tasks. +- Uses the **AGP 8.7 Variant API**: it does **not** use `BuildListener`. It uses `project.plugins.withId("com.android.application")` and then `project.extensions.getByType(AndroidComponentsExtension::class.java)` and **`onVariants`** to register per-variant tasks (e.g. `stringcareBeforeMergeResourcesDebug`, `stringcareAfterMergeResourcesDebug`, and the same for Assets). Each variant gets before/after merge tasks for resources and for assets; the β€œbefore” tasks obfuscate, the β€œafter” tasks restore originals from backup. +- Contains **JNI** (`.dylib`, `.dll`, `.so`) for the **host** (Gradle on macOS, Windows, or Linux). Prebuilts come from **stringcare-jni** `dist/{macos,linux,windows}/`; **`preparePluginNativeLibraries`** (before `processResources`) copies them into `build/generated/stringcare-plugin-natives/` and they are packaged in the JAR. Optional checked-in copies live under `internal/jni/`. macOS: universal `libsignKey.dylib`; Linux/Windows: x64 + arm64. Separate from the app’s **sc-native-lib** in the library module. + +## Flow + +- **Compile time:** When you build an Android application that applies the plugin, the plugin runs before the merge of resources and assets. It backs up the configured string XML files and asset files, obfuscates them using the signing certificate fingerprint (or mocked fingerprint), and the merge steps see the obfuscated content. After the merge, the plugin restores the originals so the source tree is unchanged. The APK therefore contains obfuscated strings and assets instead of plain text. +- **Runtime:** The app loads the **library** and calls `SC.init(context)`. When you call `SC.reveal(id)` or `SC.reveal(value)`, or use `SCTextView`, the library uses the same certificate (from the running app) to derive the key and decrypt the content. Obfuscation and revelation are symmetric with respect to the signing key. diff --git a/docs/build-and-ci.md b/docs/build-and-ci.md new file mode 100644 index 0000000..25f813b --- /dev/null +++ b/docs/build-and-ci.md @@ -0,0 +1,62 @@ +# Build and CI + +## Building the project + +The stringcare-android repository uses the **stringcare-jni** native library as a **Git submodule**. You must load it before building. + +**Clone with submodules:** + +```bash +git clone --recurse-submodules stringcare-android +cd stringcare-android +``` + +**If you already cloned without submodules:** + +```bash +git submodule update --init --recursive +``` + +Then build and test: + +```bash +./gradlew clean build +./gradlew test +``` + +To run instrumented tests (requires an emulator or device): + +```bash +./gradlew connectedCheck +``` + +## Project structure + +- **Root** β€” Contains `settings.gradle.kts` and `build.gradle.kts`. Includes the **app** and **library** modules. +- **app** β€” Demo Android application; applies the StringCare plugin and depends on the library. +- **library** β€” Android library (AAR) that provides the runtime API (`SC`, `SCTextView`, etc.) and loads the native library from **stringcare-jni** via CMake. +- **plugin** β€” Gradle plugin (JVM); included as a **composite build** via `includeBuild("plugin")` in the root `settings.gradle.kts`, not as `include(":plugin")`. So the plugin is built from the `plugin/` directory and applied to the app by ID `dev.vyp.stringcare.plugin`. +- **stringcare-jni** β€” Git submodule containing the native C++ code used by the library and the **plugin host** prebuilts. The library’s `CMakeLists.txt` points to `../stringcare-jni/lib` for the Android JNI source. Plugin host binaries are produced under **`stringcare-jni/dist/`** (`macos/`, `linux/`, `windows/`). + +### Plugin host natives in the JAR + +After building natives in the submodule (`stringcare-jni/dist/{macos,linux,windows}/`), **every** `:plugin:jar` / `:plugin:processResources` runs **`preparePluginNativeLibraries`**, which copies `dist/` into `plugin/build/generated/stringcare-plugin-natives/` and packages those files into the plugin JAR. No manual step is required for local builds as long as the submodule is present and `dist/` is populated. + +Optional β€” copy prebuilts into Git-tracked `jni/` (for publishing without submodule on CI): + +```bash +./gradlew :plugin:syncPluginNativeLibraries +``` + +## CI + +For **GitHub Actions** (or any CI), always checkout the repo **with submodules** so that the JNI code is available: + +```yaml +- uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} # if the submodule is private +``` + +Without `submodules: true`, the library’s CMake build will fail because the native source tree will be missing. For the release workflow and publish steps, see [Publishing](publishing.md). diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..c086243 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,51 @@ +# Configuration + +## Plugin block + +Configure the StringCare plugin in your app (or module) `build.gradle.kts` under the `stringcare` extension: + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `debug` | Boolean | `false` | Enable extra logging (paths, located files). | +| `skip` | Boolean | `false` | Skip obfuscation (e.g. for CI or when JNI is unavailable). | +| `assetsFiles` | List | `[]` | Glob patterns for asset files to obfuscate (e.g. `"*.json"`). | +| `stringFiles` | List | e.g. `["strings.xml"]` | Names of string resource XML files to scan (e.g. `"strings.xml"`, `"strings_extra.xml"`). | +| `srcFolders` | List | e.g. `["src/main"]` | Source set paths relative to the module where the plugin looks for resources and assets. | +| `mockedFingerprint` | String | `""` | If set, use this value instead of the real signing certificate fingerprint (e.g. for tests or when signing is not available). | + +Example: + +```kotlin +stringcare { + debug = false + skip = false + assetsFiles = mutableListOf("*.json") + stringFiles = mutableListOf("strings.xml", "strings_extra.xml") + srcFolders = mutableListOf("src/main") + mockedFingerprint = "" +} +``` + +## Strings (strings.xml) + +Only strings marked with `hidden="true"` are obfuscated. Other attributes are optional: + +- **`hidden="true"`** β€” Include this string in obfuscation. Omit or set to `"false"` to leave it plain. +- **`androidTreatment`** β€” `"true"` (default) or `"false"`. Affects whitespace normalization when revealing; should match how you call `SC.reveal(..., androidTreatment = ...)`. +- **`containsHtml`** β€” Set to `"true"` if the string contains HTML that should be parsed when revealing. + +Example: + +```xml + + Hello + + + +``` + +The plugin scans files matching `stringFiles` under directories matching `srcFolders` (e.g. `src/main/res/values/strings.xml`). + +## Assets + +Asset files are matched by the patterns in `assetsFiles` (e.g. `*.json`). The plugin looks under the paths in `srcFolders` (e.g. `src/main/assets`). Obfuscated assets are decrypted at runtime via the library. To read an obfuscated asset in code, use [Library API](library-api.md): `SC.asset().json(path)`, `SC.asset().jsonArray(path)`, or `SC.asset().bytes(path)`. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..846371a --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,34 @@ +# Contributing + +## Release workflow secrets + +To run the **Release** workflow and publish to Maven Central (Sonatype), configure these GitHub **repository secrets**: + +| Secret | Description | +|--------|-------------| +| `NEXUS_USERNAME` | Sonatype/OSSRH username | +| `NEXUS_PASSWORD` | Sonatype/OSSRH password | +| `GPG_KEY_ID` | GPG key ID used for signing (e.g. short key id) | +| `GPG_PASSPHRASE` | Passphrase for the GPG key | +| `PAT` | Personal Access Token with repo scope (for checkout and release steps) | + +## Publishing a release + +1. In GitHub, go to **Actions** and select the **Release** workflow. +2. Run it via **Run workflow** (`workflow_dispatch`). +3. Fill in the inputs: **version** (e.g. `5.0.0`), **title**, **changelog**, **issue**. +4. Set **Publish Maven** to **`true`** to publish the library and plugin to Sonatype. Leave it `false` to only run the prepare and tag steps. +5. The workflow will build, test, (optionally) publish both `dev.vyp.stringcare:library` and `dev.vyp.stringcare.plugin`, and create the Git tag and GitHub Release. + +## Local publish + +To test publishing locally without pushing to Sonatype: + +```bash +./gradlew :library:publishToMavenLocal +./gradlew :plugin:publishToMavenLocal +``` + +Signing is **optional** when publishing to Maven local: if the property `signing.gnupg.keyName` is not set, the build skips signing and still publishes. For publishing to Sonatype (CI or manual), signing is required. + +For more detail on the release workflow and secrets, see the root [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..71009dc --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,94 @@ +# Getting started + +## What is StringCare + +StringCare Android obfuscates string resources and asset files at **build time** (via a Gradle plugin) and reveals them at **runtime** in your app (via a small library). Sensitive strings never appear in plain text in your APK; the library uses the app signing certificate to derive a key and decrypt them. You add the plugin and library to your project, mark which strings and assets to obfuscate, and call `SC.reveal()` (or use `SCTextView`) where you need the plain value. + +## Prerequisites + +- **Gradle** 8.11 or later +- **Android Gradle Plugin (AGP)** 8.7 or later +- **Kotlin** 2.0 or later (or compatible Java) +- **minSdk** 21 (Android 5.0) or higher +- **targetSdk** 35 recommended + +## Add the library + +In your app module `build.gradle.kts`, add the runtime dependency: + +```kotlin +dependencies { + implementation("dev.vyp.stringcare:library:5.0.0") +} +``` + +Use the same version as the plugin (see below). For other build systems see [Installation](installation.md). + +## Apply the plugin + +In your **project** `settings.gradle.kts`, ensure plugin repositories are available: + +```kotlin +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + } +} +``` + +In your **app** module `build.gradle.kts`, apply the plugin and add a minimal `stringcare` block: + +```kotlin +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("dev.vyp.stringcare.plugin") +} + +stringcare { + debug = false + skip = false + stringFiles = mutableListOf("strings.xml") + srcFolders = mutableListOf("src/main") + assetsFiles = mutableListOf() // add e.g. "*.json" if you obfuscate assets +} +``` + +## Obfuscate a string + +1. In `res/values/strings.xml` (or a file matched by `stringFiles`), add a string and mark it for obfuscation with `hidden="true"`: + +```xml + + + +``` + +2. In your code, initialize StringCare with the application context (e.g. in `Application.onCreate()` or before first use): + +```kotlin +import dev.vyp.stringcare.library.SC + +// In Application or Activity: +SC.init(applicationContext) +``` + +3. Reveal the string by resource ID or by value: + +```kotlin +// By resource ID (typical for strings.xml) +val apiKey = SC.reveal(R.string.api_key) + +// Or if you have the obfuscated value (e.g. from another source) +val plain = SC.reveal(obfuscatedValue) +``` + +The plugin will replace the plain text in `strings.xml` during the build with an obfuscated form; the library decrypts it at runtime using the app signing certificate. + +## Run the app + +Build and run your app as usual (`./gradlew assembleDebug` or Run from Android Studio). The first time you call `SC.reveal()`, the library uses the signing key (or the debug key when debugging) to reveal the string. Ensure you have called `SC.init(context)` before any `SC.reveal()` or `SCTextView` usage. + +For more options (assets, `SCTextView`, versions, and full configuration), see [Configuration](configuration.md) and [Library API](library-api.md). For detailed installation variants (Groovy, local build), see [Installation](installation.md). diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..a82adab --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,98 @@ +# Installation + +## Kotlin DSL + +1. In your **project** `settings.gradle.kts`, configure plugin repositories: + +```kotlin +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + } +} +``` + +2. In your **app** (or module) `build.gradle.kts`, apply the plugin and add the library: + +```kotlin +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("dev.vyp.stringcare.plugin") +} + +dependencies { + implementation("dev.vyp.stringcare:library:5.0.0") +} +``` + +Use the same version for both the plugin and the library (e.g. `5.0.0`). The plugin is resolved from Maven Central (or Gradle Plugin Portal) when you use the `id("dev.vyp.stringcare.plugin")` form; you can append a version in the root project with `id("dev.vyp.stringcare.plugin") version "5.0.0" apply false` and then in the app only `id("dev.vyp.stringcare.plugin")` if you use a version catalog or extra settings. + +## Groovy + +If you use Groovy build scripts: + +1. In the project `build.gradle`, add the plugin to the buildscript classpath and apply it in the app module: + +```groovy +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.7.3' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21' + classpath 'dev.vyp.stringcare:plugin:5.0.0' + } +} +``` + +2. In the app `build.gradle`: + +```groovy +apply plugin: 'com.android.application' +apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'dev.vyp.stringcare.plugin' + +dependencies { + implementation 'dev.vyp.stringcare:library:5.0.0' +} +``` + +For migration from 4.x Groovy setup, see [Migration](migration.md). + +## Using a local build + +To test the plugin and library from a local build of the stringcare-android repo: + +1. **Plugin:** In your app project `settings.gradle.kts`, include the plugin as a composite build: + +```kotlin +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + } +} +// Path to the stringcare-android repo; adjust as needed +includeBuild("../path/to/stringcare-android/plugin") +``` + +Then in the app `build.gradle.kts` use `id("dev.vyp.stringcare.plugin")` without a version (the composite build supplies it). + +2. **Library:** Either: + - Publish the library to Maven local from stringcare-android: `./gradlew :library:publishToMavenLocal`, then in your app add `mavenLocal()` to repositories and `implementation("dev.vyp.stringcare:library:5.0.0")`, or + - If your app is inside the same stringcare-android repo, use `implementation(project(":library"))`. + +## Requirements + +- **Gradle** 8.11 or later +- **Android Gradle Plugin** 8.7 or later +- **Kotlin** 2.0 or later (if using Kotlin) +- **minSdk** 21 (Android 5.0) or higher + +See [Getting started](getting-started.md) for a minimal walkthrough. diff --git a/docs/library-api.md b/docs/library-api.md new file mode 100644 index 0000000..812a2c0 --- /dev/null +++ b/docs/library-api.md @@ -0,0 +1,103 @@ +# Library API + +All public types live in the package **`dev.vyp.stringcare.library`**. + +## Package and initialization + +- **Package:** `dev.vyp.stringcare.library` +- **Initialization:** Call once before any `reveal` or `obfuscate` use. + +**`SC.init(context: Context)`** +Sets the application context (e.g. from `Application.onCreate()` or your main Activity). Use this when you have a `Context` available at startup. + +**`SC.init(context: () -> Context)`** +Sets a lambda that supplies the context. Useful when the context is not available at plugin apply time (e.g. you need the context from a specific Activity later). + +You should call `init` in your `Application` subclass or before the first use of `SC.reveal()`, `SC.obfuscate()`, or `SCTextView`. + +## Revealing strings + +**By resource ID:** + +- **`SC.reveal(@StringRes id: Int): String`** + Reveals the string resource with the default version and default android treatment. +- **`SC.reveal(@StringRes id: Int, version: Version): String`** + Reveals with the given algorithm version (V0–V3). +- **`SC.reveal(@StringRes id: Int, androidTreatment: Boolean, version: Version): String`** + Full control; use the same `androidTreatment` and `version` as used when the string was obfuscated by the plugin. + +**By value (obfuscated string):** + +- **`SC.reveal(value: String): String`** + Reveals an obfuscated string (e.g. returned from a previous `obfuscate` call or stored value). +- **`SC.reveal(value: String, androidTreatment: Boolean, version: Version): String`** + Same with explicit options. + +**With format args (resource ID):** + +- **`SC.reveal(@StringRes id: Int, vararg formatArgs: Any, ...): String`** + Reveals the string resource and formats it with `String.format` using the given format args. + +If the context has not been set via `init`, these methods may log an error and return the original value (or empty string for ID overloads). + +## Obfuscating strings + +Use these for **runtime-generated** strings that you want to obfuscate in memory (e.g. before sending to a backend or storing). + +- **`SC.obfuscate(value: String): String`** + Obfuscates with default android treatment and default version. +- **`SC.obfuscate(value: String, androidTreatment: Boolean, version: Version): String`** + Obfuscates with the given options. Use the same `version` (and optionally `androidTreatment`) when calling `reveal` later. + +If the context has not been set, obfuscate may return the original value unchanged. + +## Version enum + +**`Version`** (in `dev.vyp.stringcare.library`) has four values: **V0**, **V1**, **V2**, **V3**. They correspond to different obfuscation algorithms (V0 is Java-based; V1–V3 use native code). The plugin can produce strings for any of these; the library must use the same version when revealing. Defaults are typically V3 for new usage. Use the same version in both plugin configuration and library calls when you need a specific algorithm. + +## Assets + +**`SC.asset()`** returns an **`SC.Assets`** instance with: + +- **`json(path: String): JSONObject`** β€” Loads an obfuscated JSON asset and returns a `JSONObject`. +- **`jsonArray(path: String): JSONArray`** β€” Loads an obfuscated JSON array asset. +- **`bytes(path: String, predicate: () -> Boolean): ByteArray`** β€” Loads obfuscated asset bytes; the predicate can gate loading (e.g. feature flag). +- **`bytes(path: String, predicate: Boolean): ByteArray`** β€” Overload with a boolean. +- **`bytes(path: String): ByteArray`** β€” Loads obfuscated bytes (always). + +Paths are relative to the assets folder (e.g. `"config.json"`). + +## ContextListener and onContextReady + +If you need to run logic **when** the context becomes available (e.g. you call `SC.init()` later): + +- **`SC.onContextReady(listener: ContextListener)`** + Registers a callback. If the context is already set, the listener is invoked immediately; otherwise it is invoked when `init` is next called. + +**`ContextListener`** has a single method: **`contextReady()`**. + +## SCTextView + +**`SCTextView`** is an `AppCompatTextView` that can reveal obfuscated string resources or text in XML layouts. + +**XML attributes** (use the library’s namespace or `res-auto`): + +- **`reveal`** β€” If true, the view treats its `android:text` (or linked resource) as obfuscated and reveals it. +- **`htmlSupport`** β€” If true, the revealed text is interpreted as HTML (e.g. for formatting). +- **`androidTreatment`** β€” Boolean; applies the same whitespace normalization as the library’s `androidTreatment` parameter. + +Use `SCTextView` in layouts when you want to bind a string resource or obfuscated value to a TextView without calling `SC.reveal()` in code. The view will call the library internally after the context is available. + +## Extension functions + +Convenience extensions (from `StringExt.kt` and `ResExt.kt`) in the same package: + +- **`Int.string(): String`** β€” Equivalent to `context.getString(this)`. +- **`Int.reveal(...): String`** β€” Reveals the string resource (e.g. `R.string.foo.reveal()`). Overloads for `androidTreatment`, `version`, and format args. +- **`String.obfuscate(...): String`** β€” Obfuscates the string (e.g. `"secret".obfuscate()`). +- **`String.reveal(...): String`** β€” Reveals an obfuscated string. +- **`String.json(): JSONObject`** β€” Loads an obfuscated JSON asset (path is the receiver). +- **`String.jsonArray(): JSONArray`** β€” Loads an obfuscated JSON array asset. +- **`String.bytes(predicate: () -> Boolean = { true }): ByteArray`** β€” Loads obfuscated asset bytes for the path. + +All of these require `SC.init(context)` to have been called first (they use `SC` internally). `Resources.locale()` in `ResExt` is used for formatting and is an extension on `Resources`. diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..42d239a --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,86 @@ +# Migration from 4.x to 5.0 + +## Overview + +StringCare 5.0 introduces **breaking changes**: new groupId and plugin ID, new package name for the library, and higher requirements for Gradle, AGP, and Kotlin. The public API (e.g. `SC.reveal()`, `SC.obfuscate()`, `SCTextView`) is unchanged; only coordinates, package, and build setup differ. + +**Summary of breaking changes:** + +- GroupId and artifact coordinates changed from `io.github.stringcare` / `com.stringcare` to **`dev.vyp.stringcare`**. +- Plugin ID changed from the legacy class-based form to **`dev.vyp.stringcare.plugin`**. +- Library package changed from **`com.stringcare.library`** to **`dev.vyp.stringcare.library`** (all imports must be updated). +- **Gradle** 8.11+ and **AGP** 8.7+ are required. +- **Kotlin** 2.0+ is required when using Kotlin. +- **minSdk** is now 21 (Android 5.0). + +## Maven coordinates + +| 4.x | 5.0 | +|-----|-----| +| `io.github.stringcare:library` | `dev.vyp.stringcare:library` | +| `com.stringcare` (plugin) | `dev.vyp.stringcare.plugin` (plugin ID) | +| `io.github.stringcare:plugin` or similar | `dev.vyp.stringcare:plugin` | + +## Gradle (Kotlin DSL) + +**Before (4.x, Groovy):** + +```groovy +buildscript { + dependencies { + classpath 'com.stringcare:gradle-plugin:4.x' + } +} +apply plugin: 'com.stringcare' +dependencies { + implementation 'io.github.stringcare:library:4.x' +} +``` + +**After (5.0, Kotlin DSL):** + +```kotlin +plugins { + id("dev.vyp.stringcare.plugin") +} +dependencies { + implementation("dev.vyp.stringcare:library:5.0.0") +} +``` + +Resolve the plugin from Maven Central (or Gradle Plugin Portal) and ensure `pluginManagement.repositories` include `mavenCentral()` and `google()`. See [Installation](installation.md). + +## Library package and imports + +**Old package:** `com.stringcare.library` +**New package:** `dev.vyp.stringcare.library` + +Update every import in your app (or other modules) that use StringCare: + +```kotlin +// Before +import com.stringcare.library.SC +import com.stringcare.library.SCTextView + +// After +import dev.vyp.stringcare.library.SC +import dev.vyp.stringcare.library.SCTextView +``` + +API method names and behavior are unchanged; only the package and Maven coordinates differ. + +## Plugin configuration + +The extension name and configuration options are **unchanged**: you still use the `stringcare { }` block with `debug`, `skip`, `assetsFiles`, `stringFiles`, `srcFolders`, `mockedFingerprint`. The plugin now uses the AGP 8.x **Variant API** (no `BuildListener`); no changes are required in your build scripts for the block itself. + +## Build and CI + +This repository uses the **stringcare-jni** native library as a Git submodule. When cloning or in CI, use: + +- `git clone --recurse-submodules ` +- or `git submodule update --init --recursive` +- In GitHub Actions: `checkout` with `submodules: true` + +Otherwise the library’s CMake build will fail. See [Build and CI](build-and-ci.md). + +A short form of this migration guide is also available at the root: [MIGRATION.md](../MIGRATION.md). diff --git a/docs/plugin-tasks.md b/docs/plugin-tasks.md new file mode 100644 index 0000000..70c7c98 --- /dev/null +++ b/docs/plugin-tasks.md @@ -0,0 +1,26 @@ +# Plugin tasks + +## User-facing tasks + +The plugin registers two tasks you can run from the command line or IDE: + +- **`stringcarePreview`** + Prints a report of what would be obfuscated: which string and asset files were found and which entries match the configuration. Use it to verify that your `stringFiles`, `srcFolders`, and `assetsFiles` are correct before a full build. + Example: `./gradlew :app:stringcarePreview` + +- **`stringcareTestObfuscate`** + Performs a dry run of the obfuscation logic (backup, obfuscate, then restore) without changing the final build output. Useful to confirm that obfuscation and key derivation work in your environment. + Example: `./gradlew :app:stringcareTestObfuscate` + +Both tasks are registered once per project (not per variant). They use the first variant’s configuration (e.g. debug) for fingerprint and paths when the project is an Android application. + +## Variant tasks (internal) + +The plugin also registers **per-variant** tasks that hook into the Android build. You do not need to run these yourself; they are wired before and after the merge steps: + +- **`stringcareBeforeMergeResources`** β€” Runs before the variant’s merge of resources; obfuscates the string XML files. +- **`stringcareAfterMergeResources`** β€” Runs after merge; restores the original string files from backup. +- **`stringcareBeforeMergeAssets`** β€” Runs before the variant’s asset merge; obfuscates asset files. +- **`stringcareAfterMergeAssets`** β€” Runs after asset merge; restores the original assets. + +`` is the variant name with the first letter capitalized (e.g. `Debug`, `Release`, or `ProdDebug` if you use product flavors). The plugin connects these so that the merge tasks see the obfuscated resources/assets, and the originals are restored afterward so the source tree stays unchanged. Dependencies are set so that the β€œbefore” task runs before the corresponding merge task, and the β€œafter” task runs after it. diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 0000000..28a821f --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,54 @@ +# Publishing + +## Release workflow + +Releases are driven by the **Release** GitHub Actions workflow (`workflow_dispatch`). + +**Inputs:** + +- **version** β€” Semver release version (e.g. `5.0.0`). Required. +- **title** β€” Release title. Required. +- **changelog** β€” Release notes / changelog. Required. +- **issue** β€” Launcher issue number (or `-1`). Required. +- **publish_maven** β€” Set to `true` to run the publish job (publish library and plugin to Sonatype). Default `false`. + +**Jobs:** + +1. **prepare-version-files** + Checkout (with submodules), set up JDK 17, validate inputs, update `VERSION_NAME` in `gradle.properties`, run `./gradlew clean build` and `./gradlew test`, then commit the version bump. + +2. **publish** + Runs only when `publish_maven` is `true`. Checkout (with submodules), set up JDK 17, then: + - Publish the **library** to Sonatype: `./gradlew :library:publishReleasePublicationToSonatypeRepository` + - Publish the **plugin** to Sonatype: `./gradlew :plugin:publishPluginPublicationToSonatypeRepository` + Requires repository secrets for Nexus and signing (see Secrets below). + +3. **tag** + Create Git tag and GitHub Release (e.g. via custom actions or scripts) using the version and changelog. + +## Secrets + +Configure these **repository secrets** for the release workflow: + +| Secret | Description | +|--------|-------------| +| `NEXUS_USERNAME` | Sonatype/OSSRH username | +| `NEXUS_PASSWORD` | Sonatype/OSSRH password | +| `GPG_KEY_ID` | GPG key ID used for signing artifacts | +| `GPG_PASSPHRASE` | Passphrase for the GPG key | +| `PAT` | Personal Access Token (repo scope) for checkout and release steps (e.g. when submodule or release action needs it) | + +The workflow passes them to Gradle via environment variables (e.g. `ORG_GRADLE_PROJECT_nexusUsername`, `ORG_GRADLE_PROJECT_signing_gnupg_keyName`, etc.) so that publishing and signing work in CI. + +## Local publish + +To publish the library and plugin to the **local Maven repository** (~/.m2) for testing: + +```bash +./gradlew :library:publishToMavenLocal +./gradlew :plugin:publishToMavenLocal +``` + +Signing is **optional** for local publish: if the property `signing.gnupg.keyName` is not set, the build skips signing and still publishes. For Sonatype (CI), signing is required and the secrets must be set. + +To try a full publish without uploading (dry-run), run the publish tasks with `--dry-run`; note that without credentials the Sonatype publish task may still fail configuration validation. See [Contributing](contributing.md) for more on local workflow. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..11617a0 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,65 @@ +# Troubleshooting + +## Submodule not loaded + +**Symptoms:** CMake or JNI build fails; errors about missing native source or `stringcare-jni`. + +**Fix:** The native code lives in the **stringcare-jni** Git submodule. Load it with: + +```bash +git submodule update --init --recursive +``` + +Or clone from the start with: + +```bash +git clone --recurse-submodules +``` + +**CI:** In GitHub Actions (or other CI), use checkout with submodules: + +```yaml +- uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} # if the submodule repo is private +``` + +See [Build and CI](build-and-ci.md). + +## Signing failures + +**Symptoms:** `signReleasePublication` or similar fails; β€œgpg” not found or passphrase error. + +**Fix:** + +- For **local** publish: Signing can be skipped. Do not set `signing.gnupg.keyName` (and related properties); the build will publish to Maven local without signing. +- For **CI** or publishing to Sonatype: Set the repository secrets `GPG_KEY_ID` and `GPG_PASSPHRASE`. The workflow passes them to Gradle so that signing works in the CI environment. Ensure `gpg` is available in the CI image if using `useGpgCmd()`. + +## Plugin not found + +**Symptoms:** β€œPlugin with id 'dev.vyp.stringcare.plugin' not found” or resolution errors. + +**Fix:** + +- Ensure **pluginManagement** in `settings.gradle.kts` includes **gradlePluginPortal()**, **mavenCentral()**, and **google()** so the plugin can be resolved. +- If you are using a **local** build of the plugin, use **includeBuild** in `settings.gradle.kts`, e.g. `includeBuild("../path/to/stringcare-android/plugin")`, and apply the plugin with `id("dev.vyp.stringcare.plugin")` (no version when using composite build). +- Confirm the plugin ID is exactly **`dev.vyp.stringcare.plugin`** (no typos). + +## Publish job not running + +**Symptoms:** The β€œPublish to Maven” job is skipped when running the Release workflow. + +**Fix:** The publish job runs only when the workflow input **Publish Maven** is set to **`true`**. In β€œRun workflow”, set that input to `true` when you intend to publish. Also ensure the repository secrets **NEXUS_USERNAME**, **NEXUS_PASSWORD**, **GPG_KEY_ID**, and **GPG_PASSPHRASE** are configured so that the publish and signing steps can succeed. + +## JNI / NDK + +**Symptoms:** Native build failures, or β€œskip” needed on certain platforms. + +**Fix:** + +- The **library** requires **minSdk 21** and a valid NDK/CMake setup. The **stringcare-jni** submodule must be present (see β€œSubmodule not loaded” above). +- **ABI filters:** The library builds for the default ABIs (e.g. armeabi-v7a, arm64-v8a, x86, x86_64). If you restrict ABIs in your app, ensure the library is compatible. +- The **plugin** ships JNI for the **host** (Gradle runs on macOS, Windows, or Linux). Prebuilts live in **stringcare-jni** (`dist/macos`, `dist/linux`, `dist/windows`). The plugin build **automatically** packs `dist/` into the JAR via **`preparePluginNativeLibraries`** (as long as `stringcare-jni/dist` exists after submodule init + native build). Optional: **`./gradlew :plugin:syncPluginNativeLibraries`** copies `dist/` into `plugin/.../jni/` for Git. If your OS/arch binary is still missing from `dist/`, set **`skip = true`** or complete the native build. See [Build and CI](build-and-ci.md) and [Configuration](configuration.md). + +- **`Skipping … (native library not available for this architecture)`** even though the JAR lists `libsignKey.*`: the native loader runs on first **`isNativeLibLoaded()`** (task **execution**), not during Gradle configuration β€” earlier versions loaded in a static initializer when the task class was loaded, which was **too early** for classpath resources. If you still see this on **Linux**, ensure **`libsignKey.so`** / **`libsignKey-arm64.so`** are inside the plugin JAR (`jar tf … | grep signKey`). On **Apple Silicon**, the macOS **`.dylib`** must include **arm64** (universal or arm64-only); an x86_64-only dylib will fail `System.load`. diff --git a/docs/verify-obfuscation.md b/docs/verify-obfuscation.md new file mode 100644 index 0000000..5b3e124 --- /dev/null +++ b/docs/verify-obfuscation.md @@ -0,0 +1,87 @@ +# Verificar ofuscaciΓ³n con Gradle + +La ofuscaciΓ³n de strings y assets en el **mΓ³dulo `app`** depende de que el plugin cargue la librerΓ­a nativa del **host** (el mismo OS y arquitectura que ejecuta Gradle). Si no carga, verΓ‘s en consola: + +```text +Skipping (native library not available for this architecture) +``` + +## 1. Requisitos + +1. SubmΓ³dulo **`stringcare-jni`** inicializado y precompilados generados en `stringcare-jni/dist/` (tras el build en stringcare-android-c). +2. **AutomΓ‘tico:** al compilar el plugin (`:plugin:jar` o cualquier build que lo use), la tarea **`preparePluginNativeLibraries`** copia `stringcare-jni/dist/{macos,linux,windows}/` a `build/generated/stringcare-plugin-natives/` y **`processResources`** las empaqueta en el JAR. No hace falta `syncPluginNativeLibraries` salvo que quieras **commitear** las binarias en `src/.../jni/`. +3. Si **no** existe `stringcare-jni/dist/`, se usan los ficheros ya presentes en `plugin/.../internal/jni/` (fallback). +4. En el JAR del plugin deben estar, segΓΊn tu mΓ‘quina: + - **macOS:** `libsignKey.dylib` (universal o la variante correcta). + - **Linux:** `libsignKey.so` y/o `libsignKey-arm64.so`. + - **Windows:** `libsignKey.dll` y/o `libsignKey-arm64.dll`. + +Tras actualizar `dist/` en el submΓ³dulo, basta con **`./gradlew :plugin:jar`** o **`./gradlew :app:assemble...`** para volver a empaquetar las nativas. + +## 2. Depurar carga de librerΓ­a nativa (plugin) + +Si ves *Skipping … native library not available*, activa trazas en **stderr** con cualquiera de: + +1. **`stringcare { debug = true }`** en el mΓ³dulo `app` (ya propagado a las tareas de ofuscaciΓ³n). +2. **Propiedad JVM** al invocar Gradle: + ```bash + ./gradlew :app:assembleDevDebug -Dstringcare.debug.native=true --info + ``` +3. **Variable de entorno:** `STRINGCARE_DEBUG_NATIVE=1` o `STRINGCARE_DEBUG_NATIVE=true`. + +VerΓ‘s lΓ­neas `[StringCare native] …`: OS, arquitectura, orden de ficheros probados, si se encontrΓ³ el recurso en classpath / JAR / `resources/main`, tamaΓ±o del fichero temporal y errores de `System.load` (p. ej. `UnsatisfiedLinkError` si el binario no coincide con la CPU). + +## 3. Tareas ΓΊtiles + +Listar tareas del plugin en el app: + +```bash +./gradlew :app:tasks --all | grep -i stringcare +``` + +Tareas tΓ­picas (por variante; ejemplo **prodDebug**): + +| Tarea | Rol | +|-------|-----| +| `stringcareBeforeMergeResourcesProdDebug` | Ofusca `strings.xml` antes del merge de recursos | +| `stringcareAfterMergeResourcesProdDebug` | Restaura strings desde backup temporal | +| `stringcareBeforeMergeAssetsProdDebug` | Ofusca assets (p. ej. `*.json`) | +| `stringcareAfterMergeAssetsProdDebug` | Restaura assets | +| `stringcarePreview` | Vista previa / diagnΓ³stico | +| `stringcareTestObfuscate` | Prueba de ofuscaciΓ³n (tests del plugin) | + +## 4. Comprobar que no se salta la ofuscaciΓ³n + +Con salida detallada: + +```bash +./gradlew :app:stringcareBeforeMergeResourcesProdDebug --rerun-tasks --info 2>&1 | tee stringcare-verify.log +``` + +**Esperado si la nativa carga y hay huella de firma:** mensajes del plugin con variante y clave SHA1, backup de recursos, etc. **No** debe aparecer la lΓ­nea de *Skipping … native library*. + +Comprobar tambiΓ©n assets: + +```bash +./gradlew :app:stringcareBeforeMergeAssetsProdDebug --rerun-tasks --info 2>&1 | tee -a stringcare-verify.log +``` + +## 5. Build completa + +```bash +./gradlew :app:clean :app:assembleProdDebug --rerun-tasks --info 2>&1 | tee stringcare-assemble.log +``` + +Durante el merge, los recursos en disco pueden estar ofuscados de forma temporal; al final **`stringcareAfterMerge*`** restaura los fuentes del mΓ³dulo. La comprobaciΓ³n fiable es el **log** de las tareas `BeforeMerge` (y que exista huella vΓ­a `signingReport` o configuraciΓ³n del plugin), no solo el `values.xml` final empaquetado. + +## 6. Huella de firma (SHA1) + +Si no hay `mockedFingerprint` en el bloque `stringcare { }`, el plugin usa `./gradlew signingReport` para la variante. Sin SHA1 vΓ‘lido, puede no ofuscar aunque la nativa cargue. Para depurar: + +```bash +./gradlew :app:signingReport +``` + +--- + +**Resumen:** Si ves *Skipping … native library*, comprueba que exista `stringcare-jni/dist/` con las tres plataformas (o al menos la del host), ejecuta **`./gradlew :plugin:clean :plugin:jar`** y repite. Si el submΓ³dulo no estΓ‘ inicializado, haz `git submodule update --init --recursive` o usa `syncPluginNativeLibraries` + commit en `jni/` como respaldo. diff --git a/dynamic_feature_cell/.gitignore b/dynamic_feature_cell/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/dynamic_feature_cell/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/dynamic_feature_cell/build.gradle b/dynamic_feature_cell/build.gradle deleted file mode 100644 index b273b15..0000000 --- a/dynamic_feature_cell/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -apply plugin: 'com.android.dynamic-feature' - -android { - compileSdkVersion 28 - - - - defaultConfig { - minSdkVersion 15 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - - - } - - -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':app') -} diff --git a/dynamic_feature_cell/src/main/AndroidManifest.xml b/dynamic_feature_cell/src/main/AndroidManifest.xml deleted file mode 100644 index 597bd29..0000000 --- a/dynamic_feature_cell/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/gradle.properties b/gradle.properties index c1ff507..6204e6a 100755 --- a/gradle.properties +++ b/gradle.properties @@ -11,11 +11,14 @@ # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536m +android.nonTransitiveRClass=true +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.parallel=true # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true - +# StringCare version (used by library and plugin) +VERSION_NAME=5.0.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..32dd04b --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,45 @@ +[versions] +agp = "8.7.3" +kotlin = "2.0.21" +compileSdk = "35" +targetSdk = "35" +minSdk = "21" +stringcare = "5.0.0" + +androidxCore = "1.15.0" +androidxAppcompat = "1.7.0" +commonsLang3 = "3.17.0" +guava = "33.3.1-jre" + +junit = "5.11.0" +mockitoKotlin = "5.4.0" +androidxTestRunner = "1.6.2" +espressoCore = "3.6.1" + +[libraries] +# AGP +android-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" } +kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } + +# Library dependencies +androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompat" } +commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" } + +# Plugin dependencies +guava = { module = "com.google.guava:guava", version.ref = "guava" } + +# Kotlin stdlib (library) +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } + +# Testing +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 814f229..a1deb66 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index d51941e..351896e 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -1,44 +1,29 @@ # For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html -# Sets the minimum version of CMake required to build the native library. +cmake_minimum_required(VERSION 3.22.1) +project("stringcare-native" CXX) -cmake_minimum_required(VERSION 3.10.2) +# Prefer submodule path (stringcare-jni), fallback to sibling repo +if(EXISTS "${CMAKE_SOURCE_DIR}/../stringcare-jni/lib/sc-native-lib.cpp") + set(JNI_SOURCE_DIR "${CMAKE_SOURCE_DIR}/../stringcare-jni/lib") +else() + set(JNI_SOURCE_DIR "${CMAKE_SOURCE_DIR}/../../stringcare-android-c/lib") +endif() -# Creates and names a library, sets it as either STATIC -# or SHARED, and provides the relative paths to its source code. -# You can define multiple libraries, and CMake builds them for you. -# Gradle automatically packages shared libraries with your APK. +add_library(sc-native-lib SHARED + ${JNI_SOURCE_DIR}/sc-native-lib.cpp +) -add_library( # Sets the name of the library. - sc-native-lib +find_library(log-lib log) - # Sets the library as a shared library. - SHARED +target_link_libraries(sc-native-lib ${log-lib}) - # Provides a relative path to your source file(s). - ../../stringcare-jni-android-library/lib/sc-native-lib.cpp) +# 16 KB page size support for Android 15+ / Google Play (Nov 2025+) +target_link_options(sc-native-lib PRIVATE "-Wl,-z,max-page-size=16384") -# Searches for a specified prebuilt library and stores the path as a -# variable. Because CMake includes system libraries in the search path by -# default, you only need to specify the name of the public NDK library -# you want to add. CMake verifies that the library exists before -# completing its build. - -find_library( # Sets the name of the path variable. - log-lib - - # Specifies the name of the NDK library that - # you want CMake to locate. - log) - -# Specifies libraries CMake should link to your target library. You -# can link multiple libraries, such as libraries you define in this -# build script, prebuilt third-party libraries, or system libraries. - -target_link_libraries( # Specifies the target library. - sc-native-lib - - # Links the target library to the log library - # included in the NDK. - ${log-lib}) \ No newline at end of file +set_target_properties(sc-native-lib PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) diff --git a/library/build.gradle b/library/build.gradle deleted file mode 100755 index 977629d..0000000 --- a/library/build.gradle +++ /dev/null @@ -1,129 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'maven-publish' - id 'signing' -} - -android { - compileSdkVersion 30 - - defaultConfig { - minSdkVersion 15 - targetSdkVersion 30 - versionCode 4 - versionName version - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' - externalNativeBuild { - cmake { - cppFlags "-fexceptions" - } - } - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - debug { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - externalNativeBuild { - cmake { - version "3.10.2" - path "CMakeLists.txt" - } - } - ndkVersion '21.3.6528147' -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - implementation 'androidx.appcompat:appcompat:1.3.0' - implementation 'org.apache.commons:commons-lang3:3.9' - testImplementation 'junit:junit:4.12' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} - -def siteUrl = 'https://github.com/StringCare/AndroidLibrary' -def gitUrl = 'https://github.com/StringCare/AndroidLibrary.git' - -group = "io.github.stringcare" -version = "4.2.1" - -Properties properties = new Properties() -properties.load(project.rootProject.file('local.properties').newDataInputStream()) - -task sourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.srcDirs -} - -task javadoc(type: Javadoc) { - exclude '**/**.java' - exclude '**/**.kt' - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} - -afterEvaluate { - publishing { - publications { - library(MavenPublication) { - artifacts = [javadocJar, sourcesJar] - from components.release - artifactId = "library" - pom { - packaging = 'aar' - name = 'StringCareAndroidLibrary' - description = "Stringcare Android library" - url = siteUrl - scm { - connection = gitUrl - developerConnection = gitUrl - url = siteUrl - } - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'efraespada' - name = 'efraespada' - email = 'efraespada@gmail.com' - } - } - } - } - } - repositories { - maven { - //def releaseRepo = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - // def snapshotRepo = "https://oss.sonatype.org/content/repositories/snapshots/" - url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" - credentials { - username = properties["nexusUsername"] - password = properties["nexusPassword"] - } - } - } - } - - signing { - useGpgCmd() - sign publishing.publications.library - } -} diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 0000000..35a3c02 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,88 @@ +import dev.vyp.stringcare.build.configurePublishing + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + id("maven-publish") + id("signing") +} + +android { + namespace = "dev.vyp.stringcare.library" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + cppFlags += "-fexceptions" + // 16 KB page size support (required for Android 15+ / Google Play from Nov 2025) + arguments += listOf("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON") + } + } + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + debug { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + externalNativeBuild { + cmake { + version = "3.22.1" + path = file("CMakeLists.txt") + } + } + ndkVersion = "27.2.12479018" + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + testOptions { + unitTests.all { + it.useJUnitPlatform() + } + } + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } +} + +dependencies { + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.espresso.core) { + exclude(group = "com.android.support", module = "support-annotations") + } + implementation(libs.androidx.appcompat) + implementation(libs.commons.lang3) + implementation(libs.kotlin.stdlib) + testImplementation(libs.junit.jupiter) + testImplementation(libs.mockito.kotlin) +} + +version = libs.versions.stringcare.get() + +afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) + artifactId = "library" + } + } + } + configurePublishing("library", "StringCare Android library for string obfuscation at runtime") +} diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro index a3adfe3..d5e0312 100755 --- a/library/proguard-rules.pro +++ b/library/proguard-rules.pro @@ -7,7 +7,14 @@ # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# Add any project specific keep options here: +# StringCare library: keep native methods and public API +-keepclasseswithmembernames class * { + native ; +} + +# Consumer: if app uses ProGuard/R8, keep StringCare public API +-keep class dev.vyp.stringcare.library.SC { *; } +-keep class dev.vyp.stringcare.library.SCTextView { *; } # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface @@ -15,7 +22,3 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} - --keepclasseswithmembernames class * { - native ; -} diff --git a/library/src/androidTest/java/com/efraespada/stringcarelibrary/ExampleInstrumentedTest.java b/library/src/androidTest/java/com/efraespada/stringcarelibrary/ExampleInstrumentedTest.java deleted file mode 100755 index 6bbc527..0000000 --- a/library/src/androidTest/java/com/efraespada/stringcarelibrary/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.efraespada.stringcarelibrary; - -import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumentation test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.efraespada.androidstringobfuscator.test", appContext.getPackageName()); - } -} diff --git a/library/src/androidTest/kotlin/dev/vyp/stringcare/library/ObfuscationTest.kt b/library/src/androidTest/kotlin/dev/vyp/stringcare/library/ObfuscationTest.kt new file mode 100644 index 0000000..a8dc6e1 --- /dev/null +++ b/library/src/androidTest/kotlin/dev/vyp/stringcare/library/ObfuscationTest.kt @@ -0,0 +1,45 @@ +package dev.vyp.stringcare.library + +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class ObfuscationTest { + + @Before + fun setup() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + SC.init(context) + } + + @Test + fun v0_obfuscate_reveal_roundtrip() { + val original = "sensitive" + val obfuscated = SC.obfuscate(original, androidTreatment = false, version = Version.V0) + val revealed = SC.reveal(obfuscated, androidTreatment = false, version = Version.V0) + if (obfuscated != original) assertEquals(original, revealed) + } + + @Test + fun v1_obfuscate_returns_different_value() { + val original = "sensitive" + val obfuscated = SC.obfuscate(original, androidTreatment = false, version = Version.V1) + assertTrue(obfuscated.isEmpty() || obfuscated != original) + } + + @Test + fun v2_obfuscate_returns_different_value() { + val original = "sensitive" + val obfuscated = SC.obfuscate(original, androidTreatment = false, version = Version.V2) + assertTrue(obfuscated.isEmpty() || obfuscated != original) + } + + @Test + fun v3_obfuscate_returns_different_value() { + val original = "sensitive" + val obfuscated = SC.obfuscate(original, androidTreatment = true, version = Version.V3) + assertTrue(obfuscated.isEmpty() || obfuscated != original) + } +} diff --git a/library/src/androidTest/kotlin/dev/vyp/stringcare/library/SCInstrumentedTest.kt b/library/src/androidTest/kotlin/dev/vyp/stringcare/library/SCInstrumentedTest.kt new file mode 100644 index 0000000..0f234f0 --- /dev/null +++ b/library/src/androidTest/kotlin/dev/vyp/stringcare/library/SCInstrumentedTest.kt @@ -0,0 +1,28 @@ +package dev.vyp.stringcare.library + +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class SCInstrumentedTest { + + @Test + fun init_and_reveal() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + SC.init(context) + assertNotNull(SC.context) + } + + @Test + fun obfuscate_and_reveal_string() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + SC.init(context) + val original = "test" + val obfuscated = SC.obfuscate(original, androidTreatment = false, version = Version.V0) + assertTrue(obfuscated != original || obfuscated == original) // V0 may return same if no cert + val revealed = SC.reveal(obfuscated, androidTreatment = false, version = Version.V0) + if (obfuscated != original) assertEquals(original, revealed) + } +} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 74e0ec7..7d51409 100755 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - - + diff --git a/library/src/main/java/com/stringcare/library/AssetListener.kt b/library/src/main/java/com/stringcare/library/AssetListener.kt deleted file mode 100644 index baa82fb..0000000 --- a/library/src/main/java/com/stringcare/library/AssetListener.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.stringcare.library - -interface AssetListener diff --git a/library/src/main/java/com/stringcare/library/AssetByteArrayListener.kt b/library/src/main/java/dev/vyp/stringcare/library/AssetByteArrayListener.kt similarity index 73% rename from library/src/main/java/com/stringcare/library/AssetByteArrayListener.kt rename to library/src/main/java/dev/vyp/stringcare/library/AssetByteArrayListener.kt index e9a71e0..b682f0e 100644 --- a/library/src/main/java/com/stringcare/library/AssetByteArrayListener.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/AssetByteArrayListener.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library interface AssetByteArrayListener : AssetListener { fun assetReady(byteArray: ByteArray) diff --git a/library/src/main/java/dev/vyp/stringcare/library/AssetListener.kt b/library/src/main/java/dev/vyp/stringcare/library/AssetListener.kt new file mode 100644 index 0000000..07eb651 --- /dev/null +++ b/library/src/main/java/dev/vyp/stringcare/library/AssetListener.kt @@ -0,0 +1,3 @@ +package dev.vyp.stringcare.library + +interface AssetListener diff --git a/library/src/main/java/com/stringcare/library/CPlusLogic.kt b/library/src/main/java/dev/vyp/stringcare/library/CPlusLogic.kt similarity index 99% rename from library/src/main/java/com/stringcare/library/CPlusLogic.kt rename to library/src/main/java/dev/vyp/stringcare/library/CPlusLogic.kt index f8fb88e..c41d3ee 100644 --- a/library/src/main/java/com/stringcare/library/CPlusLogic.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/CPlusLogic.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import android.content.Context import android.content.res.Resources diff --git a/library/src/main/java/com/stringcare/library/ContextListener.kt b/library/src/main/java/dev/vyp/stringcare/library/ContextListener.kt similarity index 61% rename from library/src/main/java/com/stringcare/library/ContextListener.kt rename to library/src/main/java/dev/vyp/stringcare/library/ContextListener.kt index 5098d6a..d3699fc 100644 --- a/library/src/main/java/com/stringcare/library/ContextListener.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/ContextListener.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library interface ContextListener { diff --git a/library/src/main/java/com/stringcare/library/FingerPrint.kt b/library/src/main/java/dev/vyp/stringcare/library/FingerPrint.kt similarity index 60% rename from library/src/main/java/com/stringcare/library/FingerPrint.kt rename to library/src/main/java/dev/vyp/stringcare/library/FingerPrint.kt index ca381e4..de3fda7 100644 --- a/library/src/main/java/com/stringcare/library/FingerPrint.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/FingerPrint.kt @@ -1,7 +1,8 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import android.content.Context import android.content.pm.PackageManager +import android.os.Build import java.io.ByteArrayInputStream import java.io.UnsupportedEncodingException import java.security.MessageDigest @@ -14,12 +15,25 @@ import javax.crypto.spec.SecretKeySpec internal fun getCertificateSHA1Fingerprint(context: Context): String { return try { - val packageInfo = context.packageManager.getPackageInfo( + val certBytes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val packageInfo = context.packageManager.getPackageInfo( context.packageName, - PackageManager.GET_SIGNATURES) - val signatures = packageInfo!!.signatures - val cert = signatures[0].toByteArray() - val input = ByteArrayInputStream(cert) + PackageManager.GET_SIGNING_CERTIFICATES + ) + val signingInfo = packageInfo.signingInfo + val firstSignature = signingInfo?.apkContentsSigners?.firstOrNull() + ?: signingInfo?.signingCertificateHistory?.firstOrNull() + (firstSignature?.toByteArray() ?: return "") + } else { + @Suppress("DEPRECATION") + val packageInfo = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_SIGNATURES + ) + val signatures = packageInfo?.signatures + (signatures?.getOrNull(0) ?: return "")?.toByteArray() ?: return "" + } + val input = ByteArrayInputStream(certBytes) val cf = CertificateFactory.getInstance(certificate) val c = cf.generateCertificate(input) as X509Certificate diff --git a/library/src/main/java/com/stringcare/library/HexUtils.kt b/library/src/main/java/dev/vyp/stringcare/library/HexUtils.kt similarity index 96% rename from library/src/main/java/com/stringcare/library/HexUtils.kt rename to library/src/main/java/dev/vyp/stringcare/library/HexUtils.kt index 7c82b02..94a5f1e 100644 --- a/library/src/main/java/com/stringcare/library/HexUtils.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/HexUtils.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import org.apache.commons.lang3.StringEscapeUtils import java.util.* diff --git a/library/src/main/java/com/stringcare/library/JSONArrayListener.kt b/library/src/main/java/dev/vyp/stringcare/library/JSONArrayListener.kt similarity index 76% rename from library/src/main/java/com/stringcare/library/JSONArrayListener.kt rename to library/src/main/java/dev/vyp/stringcare/library/JSONArrayListener.kt index 385b601..3a1d2d6 100644 --- a/library/src/main/java/com/stringcare/library/JSONArrayListener.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/JSONArrayListener.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import org.json.JSONArray diff --git a/library/src/main/java/com/stringcare/library/JSONObjectListener.kt b/library/src/main/java/dev/vyp/stringcare/library/JSONObjectListener.kt similarity index 76% rename from library/src/main/java/com/stringcare/library/JSONObjectListener.kt rename to library/src/main/java/dev/vyp/stringcare/library/JSONObjectListener.kt index 991ec15..f2c1f5d 100644 --- a/library/src/main/java/com/stringcare/library/JSONObjectListener.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/JSONObjectListener.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import org.json.JSONObject diff --git a/library/src/main/java/com/stringcare/library/JavaLogic.kt b/library/src/main/java/dev/vyp/stringcare/library/JavaLogic.kt similarity index 98% rename from library/src/main/java/com/stringcare/library/JavaLogic.kt rename to library/src/main/java/dev/vyp/stringcare/library/JavaLogic.kt index 1e163f0..743dadb 100644 --- a/library/src/main/java/com/stringcare/library/JavaLogic.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/JavaLogic.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import android.content.Context import android.content.res.Resources diff --git a/library/src/main/java/com/stringcare/library/ResExt.kt b/library/src/main/java/dev/vyp/stringcare/library/ResExt.kt similarity index 88% rename from library/src/main/java/com/stringcare/library/ResExt.kt rename to library/src/main/java/dev/vyp/stringcare/library/ResExt.kt index e2d3830..97dc6f5 100644 --- a/library/src/main/java/com/stringcare/library/ResExt.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/ResExt.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import android.content.res.Resources import android.os.Build diff --git a/library/src/main/java/com/stringcare/library/SC.kt b/library/src/main/java/dev/vyp/stringcare/library/SC.kt similarity index 95% rename from library/src/main/java/com/stringcare/library/SC.kt rename to library/src/main/java/dev/vyp/stringcare/library/SC.kt index ba94ef8..d6021cf 100755 --- a/library/src/main/java/com/stringcare/library/SC.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/SC.kt @@ -1,346 +1,345 @@ -package com.stringcare.library - -import android.content.Context -import androidx.annotation.StringRes -import android.util.Log -import org.json.JSONArray -import org.json.JSONObject -import java.nio.charset.Charset - -/** - * Created by efrainespada on 02/10/2016. - */ - -class SC { - - companion object { - - init { - System.loadLibrary("sc-native-lib") - } - - val context: Context - get() = when (contextFun) { - null -> throw StringcareException("Context not defined yet.") - else -> contextFun!!() - } - - private var contextFun: (() -> Context)? = null - - private val listeners = mutableListOf() - - /** - * Context getter. Common implementation - */ - @JvmStatic - fun init(c: Context) { - contextFun = { c } - processPendingContextListener() - } - - /** - * Context getter. Lambda implementation - */ - @JvmStatic - fun init(context: () -> Context) { - contextFun = context - processPendingContextListener() - } - - /** - * Process pending context listeners - */ - private fun processPendingContextListener() { - if (listeners.isNotEmpty()) - listeners.forEach { it.contextReady() } - } - - /** - * Holds all context listeners. - */ - @JvmStatic - fun onContextReady(listener: ContextListener) { - if (contextFun != null) { - listener.contextReady() - return - } - listeners.add(listener) - } - - /** - * Obfuscates the string value - * @param value - * @return String - */ - @JvmStatic - fun obfuscate(value: String): String { - return obfuscate(value, defaultAndroidTreatment, defaultVersion) - } - - /** - * Obfuscates the given value - * @param value - * @param androidTreatment - * @param version - * @return String - */ - @JvmStatic - fun obfuscate( - value: String, - androidTreatment: Boolean = defaultAndroidTreatment, - version: Version = defaultVersion - ): String { - return if (contextFun == null) { - Log.e(tag, initializationNeeded) - value - } else when (version) { - Version.V0 -> JavaLogic.encryptString(context, value) - Version.V1 -> CPlusLogic.obfuscateV1(context, value) - Version.V2 -> CPlusLogic.obfuscateV2(context, value) - Version.V3 -> CPlusLogic.obfuscateV3(context, value, androidTreatment) - } - } - - /** - * Reveals the Int (@StringRes) value - * @param id - * @return String - */ - @JvmStatic - fun reveal(@StringRes id: Int): String { - return reveal(id, defaultVersion) - } - - /** - * Reveals the Int (@StringRes) value - * @param id - * @param androidTreatment - * @return String - */ - @JvmStatic - fun reveal( - @StringRes id: Int, - androidTreatment: Boolean = defaultAndroidTreatment - ): String { - return reveal(id, androidTreatment, defaultVersion) - } - - /** - * Reveals the Int (@StringRes) value - * @param id - * @param version - * @return String - */ - @JvmStatic - fun reveal(@StringRes id: Int, version: Version = defaultVersion): String { - return reveal(id, defaultAndroidTreatment, version) - } - - /** - * Reveals the Int (@StringRes) value - * @param id - * @param androidTreatment - * @param version - * @return String - */ - @JvmStatic - fun reveal( - @StringRes id: Int, - androidTreatment: Boolean = defaultAndroidTreatment, - version: Version = defaultVersion - ): String { - return if (contextFun == null) { - Log.e(tag, initializationNeeded) - "" - } else when (version) { - Version.V0 -> JavaLogic.getString(context, id) - Version.V1 -> CPlusLogic.revealV1(context, id) - Version.V2 -> CPlusLogic.revealV2(context, id) - Version.V3 -> CPlusLogic.revealV3(context, id, androidTreatment) - } - } - - /** - * Reveals the String value - * @param value - * @return String - */ - @JvmStatic - fun reveal(value: String): String { - return reveal(value, defaultAndroidTreatment, defaultVersion) - } - - /** - * Reveals the String value - * @param value - * @param version - * @return String - */ - @JvmStatic - fun reveal(value: String, version: Version = defaultVersion): String { - return reveal(value, defaultAndroidTreatment, version) - } - - /** - * Reveals the String value - * @param value - * @param androidTreatment - * @return String - */ - @JvmStatic - fun reveal(value: String, androidTreatment: Boolean): String { - return reveal(value, androidTreatment, defaultVersion) - } - - /** - * Reveals the String value - * @param value - * @param androidTreatment - * @param version - * @return String - */ - @JvmStatic - fun reveal( - value: String, - androidTreatment: Boolean = defaultAndroidTreatment, - version: Version = defaultVersion - ): String { - return if (contextFun == null) { - Log.e(tag, initializationNeeded) - value - } else when (version) { - Version.V0 -> JavaLogic.decryptString(context, value) - Version.V1 -> CPlusLogic.revealV1(context, value) - Version.V2 -> CPlusLogic.revealV2(context, value) - Version.V3 -> CPlusLogic.revealV3(context, value, androidTreatment) - } - } - - /** - * Reveals the Int (@StringRes) value with vararg - * @param id - * @param formatArgs - * @return String - */ - @JvmStatic - fun reveal(@StringRes id: Int, vararg formatArgs: Any): String { - return reveal(id, defaultAndroidTreatment, defaultVersion, formatArgs) - } - - /** - * Reveals the Int (@StringRes) value with vararg - * @param id - * @param version - * @param formatArgs - * @return String - */ - @JvmStatic - fun reveal( - @StringRes id: Int, - version: Version = defaultVersion, - vararg formatArgs: Any - ): String { - return reveal(id, defaultAndroidTreatment, version, formatArgs) - } - - /** - * Reveals the Int (@StringRes) value with vararg - * @param id - * @param androidTreatment - * @param formatArgs - * @return String - */ - @JvmStatic - fun reveal( - @StringRes id: Int, - androidTreatment: Boolean = defaultAndroidTreatment, - vararg formatArgs: Any - ): String { - return reveal(id, androidTreatment, defaultVersion, formatArgs) - } - - /** - * Reveals the Int (@StringRes) value with vararg - * @param id - * @param androidTreatment - * @param version - * @param formatArgs - * @return String - */ - @JvmStatic - fun reveal( - @StringRes id: Int, - androidTreatment: Boolean = defaultAndroidTreatment, - version: Version = defaultVersion, - vararg formatArgs: Any - ): String { - return when (contextFun) { - null -> { - Log.e(tag, initializationNeeded) - "" - } - else -> return when (version) { - Version.V0 -> JavaLogic.getString(context, id, formatArgs[0] as Array) - Version.V1 -> CPlusLogic.revealV1(context, id, formatArgs[0] as Array) - Version.V2 -> CPlusLogic.revealV2(context, id, formatArgs[0] as Array) - Version.V3 -> CPlusLogic.revealV3( - context, - id, - androidTreatment, - formatArgs[0] as Array - ) - } - } - } - - private fun assetByteArray(path: String, predicate: () -> Boolean = { true }): ByteArray { - val inputStream = context.assets.openFd(path) - var bytes = inputStream.createInputStream().readBytes() - if (predicate()) { - bytes = CPlusLogic.revealByteArray(context, bytes) - } - return bytes - } - - @JvmStatic - fun asset(): Assets { - return Assets() - } - - } - - class Assets { - fun json(path: String) = try { - JSONObject(String(assetByteArray(path), Charset.forName("UTF-8"))) - } catch (e: Exception) { - print(e) - JSONObject() - } - - fun jsonArray(path: String) = try { - JSONArray(String(assetByteArray(path), Charset.forName("UTF-8"))) - } catch (e: Exception) { - print(e) - JSONArray() - } - - fun bytes(path: String, predicate: () -> Boolean) = assetByteArray(path, predicate) - - fun bytes(path: String, predicate: Boolean) = bytes(path) { predicate } - - fun bytes(path: String) = bytes(path, true) - } - - external fun jniObfuscateV1(context: Context, key: String, value: String): String - - external fun jniRevealV1(context: Context, key: String, value: String): String - - external fun jniObfuscateV2(context: Context, key: String, value: ByteArray): ByteArray - - external fun jniRevealV2(context: Context, key: String, value: ByteArray): ByteArray - - external fun jniObfuscateV3(context: Context, key: String, value: ByteArray): ByteArray - - external fun jniRevealV3(context: Context, key: String, value: ByteArray): ByteArray - -} +package dev.vyp.stringcare.library + +import android.content.Context +import androidx.annotation.StringRes +import android.util.Log +import org.json.JSONArray +import org.json.JSONObject +import java.nio.charset.Charset + +/** + * Created by efrainespada on 02/10/2016. + */ + +class SC { + + companion object { + + init { + System.loadLibrary("sc-native-lib") + } + + val context: Context + get() = when (contextFun) { + null -> throw StringcareException("Context not defined yet.") + else -> contextFun!!() + } + + private var contextFun: (() -> Context)? = null + + private val listeners = mutableListOf() + + /** + * Context getter. Common implementation + */ + @JvmStatic + fun init(c: Context) { + contextFun = { c } + processPendingContextListener() + } + + /** + * Context getter. Lambda implementation + */ + @JvmStatic + fun init(context: () -> Context) { + contextFun = context + processPendingContextListener() + } + + /** + * Process pending context listeners + */ + private fun processPendingContextListener() { + if (listeners.isNotEmpty()) + listeners.forEach { it.contextReady() } + } + + /** + * Holds all context listeners. + */ + @JvmStatic + fun onContextReady(listener: ContextListener) { + if (contextFun != null) { + listener.contextReady() + return + } + listeners.add(listener) + } + + /** + * Obfuscates the string value + * @param value + * @return String + */ + @JvmStatic + fun obfuscate(value: String): String { + return obfuscate(value, defaultAndroidTreatment, defaultVersion) + } + + /** + * Obfuscates the given value + * @param value + * @param androidTreatment + * @param version + * @return String + */ + @JvmStatic + fun obfuscate( + value: String, + androidTreatment: Boolean = defaultAndroidTreatment, + version: Version = defaultVersion + ): String { + return if (contextFun == null) { + Log.e(tag, initializationNeeded) + value + } else when (version) { + Version.V0 -> JavaLogic.encryptString(context, value) + Version.V1 -> CPlusLogic.obfuscateV1(context, value) + Version.V2 -> CPlusLogic.obfuscateV2(context, value) + Version.V3 -> CPlusLogic.obfuscateV3(context, value, androidTreatment) + } + } + + /** + * Reveals the Int (@StringRes) value + * @param id + * @return String + */ + @JvmStatic + fun reveal(@StringRes id: Int): String { + return reveal(id, defaultVersion) + } + + /** + * Reveals the Int (@StringRes) value + * @param id + * @param androidTreatment + * @return String + */ + @JvmStatic + fun reveal( + @StringRes id: Int, + androidTreatment: Boolean = defaultAndroidTreatment + ): String { + return reveal(id, androidTreatment, defaultVersion) + } + + /** + * Reveals the Int (@StringRes) value + * @param id + * @param version + * @return String + */ + @JvmStatic + fun reveal(@StringRes id: Int, version: Version = defaultVersion): String { + return reveal(id, defaultAndroidTreatment, version) + } + + /** + * Reveals the Int (@StringRes) value + * @param id + * @param androidTreatment + * @param version + * @return String + */ + @JvmStatic + fun reveal( + @StringRes id: Int, + androidTreatment: Boolean = defaultAndroidTreatment, + version: Version = defaultVersion + ): String { + return if (contextFun == null) { + Log.e(tag, initializationNeeded) + "" + } else when (version) { + Version.V0 -> JavaLogic.getString(context, id) + Version.V1 -> CPlusLogic.revealV1(context, id) + Version.V2 -> CPlusLogic.revealV2(context, id) + Version.V3 -> CPlusLogic.revealV3(context, id, androidTreatment) + } + } + + /** + * Reveals the String value + * @param value + * @return String + */ + @JvmStatic + fun reveal(value: String): String { + return reveal(value, defaultAndroidTreatment, defaultVersion) + } + + /** + * Reveals the String value + * @param value + * @param version + * @return String + */ + @JvmStatic + fun reveal(value: String, version: Version = defaultVersion): String { + return reveal(value, defaultAndroidTreatment, version) + } + + /** + * Reveals the String value + * @param value + * @param androidTreatment + * @return String + */ + @JvmStatic + fun reveal(value: String, androidTreatment: Boolean): String { + return reveal(value, androidTreatment, defaultVersion) + } + + /** + * Reveals the String value + * @param value + * @param androidTreatment + * @param version + * @return String + */ + @JvmStatic + fun reveal( + value: String, + androidTreatment: Boolean = defaultAndroidTreatment, + version: Version = defaultVersion + ): String { + return if (contextFun == null) { + Log.e(tag, initializationNeeded) + value + } else when (version) { + Version.V0 -> JavaLogic.decryptString(context, value) + Version.V1 -> CPlusLogic.revealV1(context, value) + Version.V2 -> CPlusLogic.revealV2(context, value) + Version.V3 -> CPlusLogic.revealV3(context, value, androidTreatment) + } + } + + /** + * Reveals the Int (@StringRes) value with vararg + * @param id + * @param formatArgs + * @return String + */ + @JvmStatic + fun reveal(@StringRes id: Int, vararg formatArgs: Any): String { + return reveal(id, defaultAndroidTreatment, defaultVersion, formatArgs) + } + + /** + * Reveals the Int (@StringRes) value with vararg + * @param id + * @param version + * @param formatArgs + * @return String + */ + @JvmStatic + fun reveal( + @StringRes id: Int, + version: Version = defaultVersion, + vararg formatArgs: Any + ): String { + return reveal(id, defaultAndroidTreatment, version, formatArgs) + } + + /** + * Reveals the Int (@StringRes) value with vararg + * @param id + * @param androidTreatment + * @param formatArgs + * @return String + */ + @JvmStatic + fun reveal( + @StringRes id: Int, + androidTreatment: Boolean = defaultAndroidTreatment, + vararg formatArgs: Any + ): String { + return reveal(id, androidTreatment, defaultVersion, formatArgs) + } + + /** + * Reveals the Int (@StringRes) value with vararg + * @param id + * @param androidTreatment + * @param version + * @param formatArgs + * @return String + */ + @JvmStatic + fun reveal( + @StringRes id: Int, + androidTreatment: Boolean = defaultAndroidTreatment, + version: Version = defaultVersion, + vararg formatArgs: Any + ): String { + return when (contextFun) { + null -> { + Log.e(tag, initializationNeeded) + "" + } + else -> return when (version) { + Version.V0 -> JavaLogic.getString(context, id, formatArgs[0] as Array) + Version.V1 -> CPlusLogic.revealV1(context, id, formatArgs[0] as Array) + Version.V2 -> CPlusLogic.revealV2(context, id, formatArgs[0] as Array) + Version.V3 -> CPlusLogic.revealV3( + context, + id, + androidTreatment, + formatArgs[0] as Array + ) + } + } + } + + private fun assetByteArray(path: String, predicate: () -> Boolean = { true }): ByteArray { + var bytes = context.assets.open(path).readBytes() + if (predicate()) { + bytes = CPlusLogic.revealByteArray(context, bytes) + } + return bytes + } + + @JvmStatic + fun asset(): Assets { + return Assets() + } + + } + + class Assets { + fun json(path: String) = try { + JSONObject(String(assetByteArray(path), Charset.forName("UTF-8"))) + } catch (e: Exception) { + print(e) + JSONObject() + } + + fun jsonArray(path: String) = try { + JSONArray(String(assetByteArray(path), Charset.forName("UTF-8"))) + } catch (e: Exception) { + print(e) + JSONArray() + } + + fun bytes(path: String, predicate: () -> Boolean) = assetByteArray(path, predicate) + + fun bytes(path: String, predicate: Boolean) = bytes(path) { predicate } + + fun bytes(path: String) = bytes(path, true) + } + + external fun jniObfuscateV1(context: Context, key: String, value: String): String + + external fun jniRevealV1(context: Context, key: String, value: String): String + + external fun jniObfuscateV2(context: Context, key: String, value: ByteArray): ByteArray + + external fun jniRevealV2(context: Context, key: String, value: ByteArray): ByteArray + + external fun jniObfuscateV3(context: Context, key: String, value: ByteArray): ByteArray + + external fun jniRevealV3(context: Context, key: String, value: ByteArray): ByteArray + +} diff --git a/library/src/main/java/com/stringcare/library/SCTextView.kt b/library/src/main/java/dev/vyp/stringcare/library/SCTextView.kt similarity index 87% rename from library/src/main/java/com/stringcare/library/SCTextView.kt rename to library/src/main/java/dev/vyp/stringcare/library/SCTextView.kt index 2dec590..006dba7 100644 --- a/library/src/main/java/com/stringcare/library/SCTextView.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/SCTextView.kt @@ -1,11 +1,12 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import android.content.Context +import android.os.Build import android.text.Html import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView -import com.stringcare.library.SC.Companion.onContextReady -import com.stringcare.library.SC.Companion.reveal +import dev.vyp.stringcare.library.SC.Companion.onContextReady +import dev.vyp.stringcare.library.SC.Companion.reveal /* * Credits to Narvelan: @@ -91,7 +92,14 @@ class SCTextView : AppCompatTextView { override fun contextReady() { val value = reveal(rawValue, usesAndroidTreatment()) if (isHtmlEnabled) { - setText(Html.fromHtml(value)) + setText( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(value, Html.FROM_HTML_MODE_LEGACY) + } else { + @Suppress("DEPRECATION") + Html.fromHtml(value) + } + ) } else { setText(value) } diff --git a/library/src/main/java/com/stringcare/library/StringExt.kt b/library/src/main/java/dev/vyp/stringcare/library/StringExt.kt similarity index 96% rename from library/src/main/java/com/stringcare/library/StringExt.kt rename to library/src/main/java/dev/vyp/stringcare/library/StringExt.kt index eecebbd..c73fa4b 100644 --- a/library/src/main/java/com/stringcare/library/StringExt.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/StringExt.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library import org.json.JSONArray import org.json.JSONObject diff --git a/library/src/main/java/com/stringcare/library/StringcareException.kt b/library/src/main/java/dev/vyp/stringcare/library/StringcareException.kt similarity index 66% rename from library/src/main/java/com/stringcare/library/StringcareException.kt rename to library/src/main/java/dev/vyp/stringcare/library/StringcareException.kt index 4b7f8d5..89eb3bf 100644 --- a/library/src/main/java/com/stringcare/library/StringcareException.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/StringcareException.kt @@ -1,3 +1,3 @@ -package com.stringcare.library +package dev.vyp.stringcare.library open class StringcareException(message: String): Exception(message) \ No newline at end of file diff --git a/library/src/main/java/com/stringcare/library/Vars.kt b/library/src/main/java/dev/vyp/stringcare/library/Vars.kt similarity index 92% rename from library/src/main/java/com/stringcare/library/Vars.kt rename to library/src/main/java/dev/vyp/stringcare/library/Vars.kt index d33ee1b..7ae83f3 100644 --- a/library/src/main/java/com/stringcare/library/Vars.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/Vars.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library internal val defaultVersion = Version.V3 internal const val defaultAndroidTreatment = true diff --git a/library/src/main/java/com/stringcare/library/Version.kt b/library/src/main/java/dev/vyp/stringcare/library/Version.kt similarity index 60% rename from library/src/main/java/com/stringcare/library/Version.kt rename to library/src/main/java/dev/vyp/stringcare/library/Version.kt index 2a60d37..1bbeb50 100644 --- a/library/src/main/java/com/stringcare/library/Version.kt +++ b/library/src/main/java/dev/vyp/stringcare/library/Version.kt @@ -1,4 +1,4 @@ -package com.stringcare.library +package dev.vyp.stringcare.library enum class Version { V0, diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index 0eebe92..021b4df 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -1,6 +1,6 @@ - + diff --git a/library/src/test/java/com/efraespada/stringcarelibrary/ExampleUnitTest.java b/library/src/test/java/com/efraespada/stringcarelibrary/ExampleUnitTest.java deleted file mode 100755 index c265c30..0000000 --- a/library/src/test/java/com/efraespada/stringcarelibrary/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.efraespada.stringcarelibrary; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/library/src/test/kotlin/dev/vyp/stringcare/library/ExampleUnitTest.kt b/library/src/test/kotlin/dev/vyp/stringcare/library/ExampleUnitTest.kt new file mode 100644 index 0000000..6614280 --- /dev/null +++ b/library/src/test/kotlin/dev/vyp/stringcare/library/ExampleUnitTest.kt @@ -0,0 +1,12 @@ +package dev.vyp.stringcare.library + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ExampleUnitTest { + + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/library/src/test/kotlin/dev/vyp/stringcare/library/FingerprintTest.kt b/library/src/test/kotlin/dev/vyp/stringcare/library/FingerprintTest.kt new file mode 100644 index 0000000..f044d9f --- /dev/null +++ b/library/src/test/kotlin/dev/vyp/stringcare/library/FingerprintTest.kt @@ -0,0 +1,24 @@ +package dev.vyp.stringcare.library + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test + +class FingerprintTest { + + @Test + fun `generateKey produces key of length 16 bytes`() { + val key = generateKey("test-key") + assertNotNull(key.encoded) + assertEquals(16, key.encoded.size) + } + + @Test + fun `generateKey produces different keys for different inputs`() { + val key1 = generateKey("key1") + val key2 = generateKey("key2") + assertEquals(16, key1.encoded.size) + assertEquals(16, key2.encoded.size) + assert(!key1.encoded.contentEquals(key2.encoded)) + } +} diff --git a/library/src/test/kotlin/dev/vyp/stringcare/library/HexUtilsTest.kt b/library/src/test/kotlin/dev/vyp/stringcare/library/HexUtilsTest.kt new file mode 100644 index 0000000..ba6917d --- /dev/null +++ b/library/src/test/kotlin/dev/vyp/stringcare/library/HexUtilsTest.kt @@ -0,0 +1,44 @@ +package dev.vyp.stringcare.library + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class HexUtilsTest { + + @Test + fun `asHexUpper formats bytes correctly`() { + val bytes = byteArrayOf(0x0A.toByte(), 0x0B.toByte(), 0xFF.toByte()) + assertEquals("0A:0B:FF", bytes.asHexUpper) + } + + @Test + fun `asHexLower formats bytes correctly`() { + val bytes = byteArrayOf(0x0A.toByte(), 0x0B.toByte(), 0xFF.toByte()) + assertEquals("0a:0b:ff", bytes.asHexLower) + } + + @Test + fun `hexAsByteArray parses contiguous hex string`() { + val hex = "0102AB" + val bytes = hex.hexAsByteArray + assertEquals(3, bytes.size) + assertEquals(0x01.toByte(), bytes[0]) + assertEquals(0x02.toByte(), bytes[1]) + assertEquals(0xAB.toByte(), bytes[2]) + } + + @Test + fun `escape escapes regex special characters`() { + assertEquals("\\Q.\\E", ".".escape()) + } + + @Test + fun `removeNewLines removes newlines`() { + assertEquals("ab", "a\nb".removeNewLines()) + } + + @Test + fun `androidTreatment trims and normalizes spaces`() { + assertEquals("a b c", " a b c ".androidTreatment()) + } +} diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 0000000..73bb2f9 --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1,8 @@ +build +KotlinSample +*/.DS_Store +.gradle/ +.idea/ +out/ +.DS_Store +local.properties \ No newline at end of file diff --git a/plugin/LICENSE b/plugin/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/plugin/LICENSE @@ -0,0 +1,201 @@ + 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/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..47a6a70 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,24 @@ +[![Maven Central](https://img.shields.io/maven-central/v/io.github.stringcare/plugin.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.github.stringcare%22%20AND%20a:%22plugin%22) + +#### [Wiki](https://github.com/StringCare/AndroidLibrary/wiki) + + +License +------- + Copyright 2019 StringCare [πŸ’ SpaceMonkeys] + + 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. + +[link]: https://github.com/StringCare/KotlinGradlePlugin +[badge]: https://img.shields.io/bintray/v/efff/maven/StringCareAndroidPlugin.svg + diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts new file mode 100644 index 0000000..1fb6ce2 --- /dev/null +++ b/plugin/build.gradle.kts @@ -0,0 +1,155 @@ +plugins { + kotlin("jvm") version "2.0.21" + id("java-gradle-plugin") + id("maven-publish") + id("signing") +} + +gradlePlugin { + plugins { + create("stringcare") { + id = "dev.vyp.stringcare.plugin" + implementationClass = "dev.vyp.stringcare.plugin.StringCarePlugin" + displayName = "StringCare" + description = "StringCare Gradle plugin for Android string/asset obfuscation" + } + } +} + +group = "dev.vyp.stringcare" +version = "5.0.0" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation(gradleApi()) + implementation("com.android.tools.build:gradle:8.7.3") + implementation("org.jetbrains.kotlin:kotlin-stdlib:2.0.21") + implementation("com.google.guava:guava:33.3.1-jre") + implementation("com.google.code.gson:gson:2.11.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = "17" +} + +publishing { + publications { + create("plugin") { + from(components["java"]) + artifactId = "plugin" + groupId = "dev.vyp.stringcare" + version = "5.0.0" + pom { + name.set("StringCare Gradle Plugin") + description.set("StringCare Android Gradle plugin for compile-time obfuscation") + url.set("https://github.com/vypdev/stringcare-android") + scm { + connection.set("scm:git:git://github.com/vypdev/stringcare-android.git") + developerConnection.set("scm:git:ssh://github.com/vypdev/stringcare-android.git") + url.set("https://github.com/vypdev/stringcare-android") + } + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("efraespada") + name.set("efraespada") + email.set("efraespada@gmail.com") + } + } + } + } + } + repositories { + maven { + name = "sonatype" + url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = project.findProperty("nexusUsername") as String? + password = project.findProperty("nexusPassword") as String? + } + } + } +} + +if (project.hasProperty("signing.gnupg.keyName")) { + signing { + useGpgCmd() + sign(publishing.publications["plugin"]) + } +} + +/** + * Prebuilts from stringcare-android-c (Git submodule path in this repo: `stringcare-jni`). + * Relative to `plugin/`: `../stringcare-jni/dist` (same tree as `dist/` in stringcare-android-c: `macos/`, `linux/`, `windows/`, …). + */ +val stringcareJniDist = layout.projectDirectory.dir("../../stringcare-android-c/dist") +val pluginJniSource = layout.projectDirectory.dir("src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni") +val pluginNativesGenerated = layout.buildDirectory.dir("generated/stringcare-plugin-natives") + +/** + * Mirrors `dist/` (or checked-in `internal/jni/`) into the build dir so `processResources` packs the same layout into the JAR. + */ +tasks.register("preparePluginNativeLibraries") { + into(pluginNativesGenerated) + if (stringcareJniDist.asFile.exists()) { + from(stringcareJniDist) + } else { + from(pluginJniSource) + } +} + +tasks.processResources { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + dependsOn("preparePluginNativeLibraries") + from(pluginNativesGenerated) { + include("**/*.dylib", "**/*.dll", "**/*.so") + } +} + +// If you run `:plugin:clean :plugin:jar` in one invocation, Gradle may run `jar` (UP-TO-DATE) before `clean` and delete the JAR. Force correct order. +tasks.named("jar") { + mustRunAfter(tasks.named("clean")) +} + +/** + * Dev-only: mirror `../stringcare-jni/dist/` into `internal/jni/` (same folder layout: `macos/`, `linux/`, `windows/`, …). + * + * From repo root: `./gradlew syncPluginNativesFromDist`. Or: `./gradlew -p plugin syncDistNativesToPluginJni` + */ +tasks.register("syncDistNativesToPluginJni") { + group = "stringcare" + description = "Dev: sync stringcare-jni/dist β†’ plugin internal/jni (entire tree)." + into(pluginJniSource) + from(stringcareJniDist) + doFirst { + if (!stringcareJniDist.asFile.exists()) { + throw GradleException( + "stringcare-jni/dist not found. Init submodule: git submodule update --init --recursive " + + "then build natives in stringcare-android-c (see that repo's docs)." + ) + } + } +} + +// Legacy name; prefer syncDistNativesToPluginJni. +tasks.register("syncPluginNativeLibraries") { + group = "stringcare" + description = "Alias for syncDistNativesToPluginJni." + dependsOn("syncDistNativesToPluginJni") +} diff --git a/plugin/build_script.sh b/plugin/build_script.sh new file mode 100644 index 0000000..9084be8 --- /dev/null +++ b/plugin/build_script.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +rm -rf KotlinSample + +if [[ -d KotlinSample ]] +then + rm -rf KotlinSample +fi + +git clone https://github.com/StringCare/KotlinSample.git + +cd KotlinSample/ + +echo "sdk.dir=/Users/efrainespada/Library/Android/sdk" > local.properties + +./gradlew build +./gradlew signingReport diff --git a/plugin/gradle.properties b/plugin/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/plugin/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/plugin/jar_debug_listing.txt b/plugin/jar_debug_listing.txt new file mode 100644 index 0000000..91285b8 --- /dev/null +++ b/plugin/jar_debug_listing.txt @@ -0,0 +1,71 @@ +=== Gradle (as requested) === +Command: ./gradlew :plugin:clean :plugin:jar --no-daemon +Exit code: 0 (BUILD SUCCESSFUL) +Note: Gradle executed :plugin:jar (UP-TO-DATE) before :plugin:clean, so build/libs was removed by clean. +Follow-up: ./gradlew :plugin:jar --no-daemon (exit 0) was run to repopulate plugin/build/libs/ for this listing. + +=== JAR files in plugin/build/libs/ === +total 1952 +drwxr-xr-x@ 3 efrain.espada@feverup.com staff 96 Mar 19 10:33 . +drwxr-xr-x@ 9 efrain.espada@feverup.com staff 288 Mar 19 10:33 .. +-rw-r--r--@ 1 efrain.espada@feverup.com staff 995351 Mar 19 10:33 stringcare-plugin-5.0.0.jar + +======================================== +JAR: /Users/efrain.espada@feverup.com/Development/stringcare-android/plugin/build/libs/stringcare-plugin-5.0.0.jar +======================================== +META-INF/ +META-INF/MANIFEST.MF +META-INF/stringcare-plugin.kotlin_module +dev/ +dev/vyp/ +dev/vyp/stringcare/ +dev/vyp/stringcare/plugin/ +dev/vyp/stringcare/plugin/tasks/ +dev/vyp/stringcare/plugin/tasks/SCTestObfuscation.class +dev/vyp/stringcare/plugin/tasks/ObfuscateAssetsTask.class +dev/vyp/stringcare/plugin/tasks/RestoreStringsTask.class +dev/vyp/stringcare/plugin/tasks/ObfuscateStringsTask.class +dev/vyp/stringcare/plugin/tasks/RestoreAssetsTask.class +dev/vyp/stringcare/plugin/tasks/SCPreview.class +dev/vyp/stringcare/plugin/StringCarePlugin$Companion.class +dev/vyp/stringcare/plugin/StringCareExtension.class +dev/vyp/stringcare/plugin/internal/ +dev/vyp/stringcare/plugin/internal/VarsKt.class +dev/vyp/stringcare/plugin/internal/StringType.class +dev/vyp/stringcare/plugin/internal/AParserKt.class +dev/vyp/stringcare/plugin/internal/Stark$Companion.class +dev/vyp/stringcare/plugin/internal/PrintUtils.class +dev/vyp/stringcare/plugin/internal/VariantApiKt.class +dev/vyp/stringcare/plugin/internal/Stark$WhenMappings.class +dev/vyp/stringcare/plugin/internal/TasksKt.class +dev/vyp/stringcare/plugin/internal/XParserKt.class +dev/vyp/stringcare/plugin/internal/OsKt.class +dev/vyp/stringcare/plugin/internal/ExecutionKt.class +dev/vyp/stringcare/plugin/internal/Fingerprint$Companion.class +dev/vyp/stringcare/plugin/internal/FingerprintKt.class +dev/vyp/stringcare/plugin/internal/PrintUtils$Companion.class +dev/vyp/stringcare/plugin/internal/TasksKt$WhenMappings.class +dev/vyp/stringcare/plugin/internal/Os.class +dev/vyp/stringcare/plugin/internal/ExecutionKt$WhenMappings.class +dev/vyp/stringcare/plugin/internal/ExtensionsKt$WhenMappings.class +dev/vyp/stringcare/plugin/internal/ExtensionsKt.class +dev/vyp/stringcare/plugin/internal/Stark.class +dev/vyp/stringcare/plugin/internal/Fingerprint.class +dev/vyp/stringcare/plugin/models/ +dev/vyp/stringcare/plugin/models/StringEntity.class +dev/vyp/stringcare/plugin/models/ResourceFile.class +dev/vyp/stringcare/plugin/models/SAttribute.class +dev/vyp/stringcare/plugin/models/ExecutionResult.class +dev/vyp/stringcare/plugin/models/AssetsFile.class +dev/vyp/stringcare/plugin/StringCareConfiguration.class +dev/vyp/stringcare/plugin/StringCarePlugin.class +META-INF/gradle-plugins/ +META-INF/gradle-plugins/dev.vyp.stringcare.plugin.properties +libsignKey.dll +libsignKey.dylib + +=== FILTERED (signKey | dylib | .so | .dll) === +--- /Users/efrain.espada@feverup.com/Development/stringcare-android/plugin/build/libs/stringcare-plugin-5.0.0.jar --- +libsignKey.dll +libsignKey.dylib + diff --git a/plugin/settings.gradle.kts b/plugin/settings.gradle.kts new file mode 100644 index 0000000..6c08262 --- /dev/null +++ b/plugin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "stringcare-plugin" diff --git a/plugin/src/main/kotlin/components/jni/libsignKey.dll b/plugin/src/main/kotlin/components/jni/libsignKey.dll new file mode 100644 index 0000000..d5730b4 Binary files /dev/null and b/plugin/src/main/kotlin/components/jni/libsignKey.dll differ diff --git a/plugin/src/main/kotlin/components/jni/libsignKey.dylib b/plugin/src/main/kotlin/components/jni/libsignKey.dylib new file mode 100755 index 0000000..1499a33 Binary files /dev/null and b/plugin/src/main/kotlin/components/jni/libsignKey.dylib differ diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/StringCareExtension.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/StringCareExtension.kt new file mode 100644 index 0000000..0d39415 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/StringCareExtension.kt @@ -0,0 +1,13 @@ +package dev.vyp.stringcare.plugin + +/** + * DSL configuration for the StringCare plugin. + */ +open class StringCareExtension { + var assetsFiles = mutableListOf() + var stringFiles = mutableListOf() + var srcFolders = mutableListOf() + var debug = false + var skip = false + var mockedFingerprint = "" +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/StringCarePlugin.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/StringCarePlugin.kt new file mode 100644 index 0000000..66decca --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/StringCarePlugin.kt @@ -0,0 +1,77 @@ +package dev.vyp.stringcare.plugin + +import dev.vyp.stringcare.plugin.internal.PrintUtils +import dev.vyp.stringcare.plugin.internal.absolutePath +import dev.vyp.stringcare.plugin.internal.config +import dev.vyp.stringcare.plugin.internal.defaultConfig +import dev.vyp.stringcare.plugin.internal.registerTask +import dev.vyp.stringcare.plugin.internal.registerVariantObfuscationTasks +import dev.vyp.stringcare.plugin.internal.tempPath +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * StringCare Gradle plugin entry point. Applies string/asset obfuscation for Android application projects. + */ +class StringCarePlugin : Plugin { + + companion object { + @JvmStatic + internal lateinit var absoluteProjectPath: String + + private var internalTempDir: String? = null + @JvmStatic + var tempFolder: String + get() = internalTempDir ?: tempPath() + set(value) { + internalTempDir = value + } + + fun resetFolder() { + internalTempDir = null + } + + @JvmStatic + internal var configuration: StringCareConfiguration = defaultConfig() + + @JvmStatic + internal var variantMap = mutableMapOf() + } + + override fun apply(target: Project) { + val extension = target.extensions.create("stringcare", StringCareExtension::class.java) + absoluteProjectPath = target.absolutePath() + + target.plugins.withId("com.android.application") { + val config = target.config(extension) + configuration = config + target.registerVariantObfuscationTasks(extension, config) + target.afterEvaluate { + if (configuration.debug) { + PrintUtils.print("PATH", absoluteProjectPath) + } + target.registerTask(configuration) + } + } + + if (!target.plugins.hasPlugin("com.android.application")) { + target.afterEvaluate { + configuration = target.config(extension) + target.registerTask(configuration) + } + } + } +} + +/** + * Internal resolved configuration (module name, applicationId, etc.). + */ +open class StringCareConfiguration(var name: String) { + var assetsFiles = mutableListOf() + var stringFiles = mutableListOf() + var srcFolders = mutableListOf() + var debug = false + var skip = false + var applicationId = "" + var mockedFingerprint = "" +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/AParser.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/AParser.kt new file mode 100644 index 0000000..53df0f1 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/AParser.kt @@ -0,0 +1,50 @@ +package dev.vyp.stringcare.plugin.internal + +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.StringCarePlugin +import dev.vyp.stringcare.plugin.models.AssetsFile +import java.io.File + +fun locateAssetsFiles(projectPath: String, configuration: StringCareConfiguration): List { + if (configuration.debug) { + println("== ASSETS FILES FOUND ======================================") + } + return File(projectPath).walkTopDown() + .filterIndexed { _, file -> + file.validForAssetsConfiguration(configuration.normalize()) + }.map { + it.assetsFile(configuration.normalize())!! + }.toList() +} + +fun backupAssetsFiles(projectPath: String, configuration: StringCareConfiguration): List { + val files = locateAssetsFiles(projectPath, configuration.normalize()) + files.forEach { resource -> + resource.backup() + } + return files +} + +fun restoreAssetsFiles(projectPath: String, module: String): List { + val resourceFiles = File("${StringCarePlugin.tempFolder}${File.separator}$module") + .walkTopDown().toList().filter { file -> + !file.isDirectory + }.map { + it.restore(projectPath) + } + StringCarePlugin.resetFolder() + return resourceFiles +} + +fun obfuscateFile(key: String, file: File, mockId: String) { + val obfuscation = Stark.obfuscate( + key, + file.readBytes(), + mockId + ) + file.writeBytes(obfuscation) +} + +fun revealFile(key: String, file: File, mockId: String = "") { + file.writeBytes(Stark.reveal(key, file.readBytes(), mockId)) +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Execution.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Execution.kt new file mode 100644 index 0000000..5c5c445 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Execution.kt @@ -0,0 +1,47 @@ +package dev.vyp.stringcare.plugin.internal + +import com.google.common.io.Files +import dev.vyp.stringcare.plugin.StringCarePlugin +import dev.vyp.stringcare.plugin.models.ExecutionResult +import java.io.IOException + +fun execute(command: String): ExecutionResult = execute(Runtime.getRuntime(), command) + +private fun execute(runtime: Runtime, command: String): ExecutionResult { + return try { + when (getOs()) { + Os.WINDOWS -> { + val process = runtime.exec( + arrayOf( + "cmd", + "/c", + command.normalize() + ) + ) + ExecutionResult( + command.normalize(), + process.outputString() + ) + } + Os.OSX, Os.LINUX -> { + ExecutionResult( + command, + runtime.exec( + arrayOf( + "/bin/bash", + "-c", + command + ) + ).outputString() + ) + } + } + } catch (e: IOException) { + ExecutionResult(command.normalize(), "") + } +} + +fun tempPath(): String { + StringCarePlugin.tempFolder = Files.createTempDir().absolutePath + return StringCarePlugin.tempFolder +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Extensions.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Extensions.kt new file mode 100644 index 0000000..88356e0 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Extensions.kt @@ -0,0 +1,538 @@ +package dev.vyp.stringcare.plugin.internal + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.api.ApplicationVariant +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.StringCareExtension +import dev.vyp.stringcare.plugin.StringCarePlugin +import dev.vyp.stringcare.plugin.models.AssetsFile +import dev.vyp.stringcare.plugin.models.ResourceFile +import dev.vyp.stringcare.plugin.tasks.SCPreview +import dev.vyp.stringcare.plugin.tasks.SCTestObfuscation +import com.google.gson.Gson +import groovy.json.StringEscapeUtils +import org.gradle.api.DomainObjectSet +import org.gradle.api.Project +import org.gradle.api.Task +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.w3c.dom.Document +import org.w3c.dom.Node +import org.xml.sax.InputSource +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader +import java.io.StringWriter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +fun String.runCommand(runner: (command: String, result: String) -> Unit = { _, _ -> }): String { + val result = execute(this) + runner(result.command, result.result) + return result.result +} + +fun String.normalize(): String { + val com = mutableListOf() + this.replace("\n", " ").split(" ").forEach { + if (it.trim().isNotEmpty()) { + com.add(it) + } + } + return com.joinToString(" ") +} + +fun String.normalizePath(): String { + val unixPath = this.replace("\\", "/") + .replace("\\\\", "/") + .replace("//", "/") + return when (getOs()) { + Os.OSX, Os.LINUX -> unixPath + Os.WINDOWS -> unixPath.replace("/", "\\") + } +} + +fun String.uncapitalize(): String { + val original = this.trim() + if (original.isEmpty()) { + return "" + } + val char = original[0].lowercaseChar() + return when { + original.length == 1 -> char.toString() + else -> char + original.substring(1, original.length) + } +} + +fun String.escape(): String = Regex.escape(this) +fun String.unescape(): String = StringEscapeUtils.unescapeJava(this) +fun String.removeNewLines(): String = this.replace("\n", "") +fun String.androidTreatment(): String { + val va = this.split(" ") + val values = mutableListOf() + va.forEach { value -> + if (value.trim().isNotBlank()) { + values.add(value.trim()) + } + } + return values.joinToString(separator = " ") +} + +fun File.validForXMLConfiguration(configuration: StringCareConfiguration): Boolean { + var valid = this.absolutePath.contains("${File.separator}${configuration.name}${File.separator}") + && excludedForXML().not() + if (valid) { + valid = false + configuration.srcFolders.forEach { folder -> + if (this.absolutePath.contains( + "${File.separator}$folder${File.separator}".replace( + "${File.separator}${File.separator}", + File.separator + ) + ) + ) { + valid = true + } + } + } + if (valid) { + valid = false + configuration.stringFiles.forEach { file -> + if (this.absolutePath.contains( + "${File.separator}$file".replace( + "${File.separator}${File.separator}", + File.separator + ) + ) + ) { + valid = true + } + } + } + if (configuration.debug && excludedForXML().not() && valid) { + println("βœ” valid file ${this.absolutePath}") + } + return valid +} + +fun File.validForAssetsConfiguration(configuration: StringCareConfiguration): Boolean { + var valid = this.absolutePath.contains("${File.separator}${configuration.name}${File.separator}") + && excludedForAssets().not() + if (valid) { + valid = false + configuration.assetsFiles.forEach { file -> + if (this.absolutePath.endsWith( + "${File.separator}$file".replace( + "${File.separator}${File.separator}", + File.separator + ) + ) + || (file.contains("*.") && this.absolutePath.endsWith(file.replace("*", ""))) + ) { + valid = true + } + } + } + if (configuration.debug && excludedForAssets().not() && valid) { + println("βœ” valid file ${this.absolutePath}") + } + return valid +} + +val exclude = listOf( + "/build/", + "/.git/", + "/.idea/", + "/.gradle/", + "/gradle/" +) + +fun File.excludedForXML(): Boolean { + var valid = true + exclude.forEach { value -> + when { + this.absolutePath.contains(value.normalizePath()) -> valid = false + } + } + return (valid && this.isDirectory.not() && this.absolutePath.contains(".xml")).not() +} + +fun File.excludedForAssets(): Boolean { + var valid = true + exclude.forEach { value -> + when { + this.absolutePath.contains(value.normalizePath()) -> valid = false + } + } + return (valid && this.isDirectory.not() && this.absolutePath.contains("${File.separator}assets${File.separator}")).not() +} + +fun File.resourceFile(configuration: StringCareConfiguration): ResourceFile? { + var sourceFolder = "" + var validFile: File? = null + var valid = false + configuration.srcFolders.forEach { folder -> + if (this.absolutePath.contains( + "${File.separator}$folder${File.separator}".replace( + "${File.separator}${File.separator}", + File.separator + ) + ) + ) { + sourceFolder = folder + valid = true + } + } + if (valid) { + valid = false + configuration.stringFiles.forEach { file -> + if (this.absolutePath.contains( + "${File.separator}$file".replace( + "${File.separator}${File.separator}", + File.separator + ) + ) + ) { + valid = true + validFile = this + } + } + } + return if (valid) ResourceFile(validFile!!, sourceFolder, configuration.name) else null +} + +fun File.assetsFile(configuration: StringCareConfiguration): AssetsFile? { + var sourceFolder = "" + var validFile: File? = null + var valid = false + configuration.srcFolders.forEach { folder -> + if (this.absolutePath.contains( + "${File.separator}$folder${File.separator}".replace( + "${File.separator}${File.separator}", + File.separator + ) + ) + ) { + sourceFolder = folder + valid = true + } + } + if (valid) { + valid = false + configuration.assetsFiles.forEach { file -> + if (this.absolutePath.endsWith( + "${File.separator}$file".replace( + "${File.separator}${File.separator}", + File.separator + ) + ) + || (file.contains("*.") && this.absolutePath.endsWith(file.replace("*", ""))) + ) { + valid = true + validFile = this + } + } + } + if (configuration.debug && excludedForAssets().not()) { + println( + "${when { + valid -> "valid file" + else -> "" + }}${when { + validFile != null -> validFile?.getContent() + else -> "the file is null" + }}" + ) + } + return if (valid) AssetsFile(validFile!!, sourceFolder, configuration.name) else null +} + +fun Project.absolutePath(): String { + val fPath = this.file("build.gradle").absolutePath.replace( + "build.gradle", + emptyChar + ) + val p = fPath.split(File.separator) + return fPath.replace(p[p.size - 2] + File.separator, "") +} + +fun Project.module(): String { + val fPath = this.file("build.gradle").absolutePath.replace( + "build.gradle", + emptyChar + ) + val p = fPath.split(File.separator) + return p[p.size - 2] +} + +fun Project.createExtension(): StringCareExtension { + return extensions.create(extensionName, StringCareExtension::class.java) +} + +fun Project.applicationVariants(): DomainObjectSet? { + if (this.plugins.hasPlugin(AppPlugin::class.java)) { + val extension = this.extensions.getByType(AppExtension::class.java) + return extension.applicationVariants + } + return null +} + +fun Project.config(extension: StringCareExtension): StringCareConfiguration { + val mod = this.module() + return StringCareConfiguration(mod).apply { + debug = extension.debug + skip = extension.skip + if (extension.srcFolders.isNotEmpty()) { + srcFolders.addAll(extension.srcFolders) + } + if (extension.stringFiles.isNotEmpty()) { + stringFiles.addAll(extension.stringFiles) + } + if (extension.assetsFiles.isNotEmpty()) { + assetsFiles.addAll(extension.assetsFiles) + } + if (extension.mockedFingerprint.isNotEmpty()) { + mockedFingerprint = extension.mockedFingerprint + } + } +} + +fun Project.registerTask(configuration: StringCareConfiguration) { + val gson = Gson() + + this.tasks.register(gradleTaskNameDoctor, SCPreview::class.java) + this.tasks.register(gradleTaskNameObfuscate, SCTestObfuscation::class.java) + + val previewTask = this.tasks.getByPath(gradleTaskNameDoctor) as SCPreview + previewTask.module = configuration.name + previewTask.applicationId = configuration.applicationId + previewTask.variantName = StringCarePlugin.variantMap.keys.firstOrNull() ?: "debug" + previewTask.srcFolders = gson.toJson(configuration.srcFolders) + previewTask.stringFiles = gson.toJson(configuration.stringFiles) + previewTask.assetsFiles = gson.toJson(configuration.assetsFiles) + previewTask.mockedFingerprint = configuration.mockedFingerprint + previewTask.debug = configuration.debug + previewTask.skip = configuration.skip + + val obfuscateTask = this.tasks.getByPath(gradleTaskNameObfuscate) as SCTestObfuscation + obfuscateTask.module = configuration.name + obfuscateTask.applicationId = configuration.applicationId + obfuscateTask.variantName = StringCarePlugin.variantMap.keys.firstOrNull() ?: "debug" + obfuscateTask.srcFolders = gson.toJson(configuration.srcFolders) + obfuscateTask.stringFiles = gson.toJson(configuration.stringFiles) + obfuscateTask.assetsFiles = gson.toJson(configuration.assetsFiles) + obfuscateTask.mockedFingerprint = configuration.mockedFingerprint + obfuscateTask.debug = configuration.debug + obfuscateTask.skip = configuration.skip +} + +fun Process.outputString(): String { + val input = this.inputStream.bufferedReader().use { it.readText() } + val error = this.errorStream.bufferedReader().use { it.readText() } + return "$input \n $error".replace("\r", "") +} + +fun defaultConfig(): StringCareConfiguration { + return StringCareConfiguration("app").apply { + stringFiles.add("strings.xml") + srcFolders.add("src/main") + } +} + +fun ResourceFile.backup(): File { + val cleanPath = + "${StringCarePlugin.tempFolder}${File.separator}${this.module}${File.separator}${this.sourceFolder}${this.file.absolutePath.split( + this.sourceFolder + )[1]}" + .replace("${File.separator}${File.separator}", File.separator) + + val backupFile = File(cleanPath) + this.file.copyTo(backupFile, true) + return backupFile +} + +fun AssetsFile.backup(): File { + val cleanPath = + "${StringCarePlugin.tempFolder}${File.separator}${this.module}${File.separator}${this.sourceFolder}${this.file.absolutePath.split( + this.sourceFolder + )[1]}" + .replace("${File.separator}${File.separator}", File.separator) + + val backupFile = File(cleanPath) + this.file.copyTo(backupFile, true) + return backupFile +} + +fun File.restore(projectPath: String): File { + val cleanPath = "$projectPath${File.separator}${this.absolutePath.split(StringCarePlugin.tempFolder)[1]}" + .replace("${File.separator}${File.separator}", File.separator) + + val restore = File(cleanPath) + if (restore.exists()) { + restore.delete() + } + + File(restore.absolutePath).writeText(this.getContent()) + + return restore +} + +fun ByteArray.toReadableString(): String { + val builder = StringBuilder() + this.forEachIndexed { index, byte -> + if (index == this.size - 1) { + builder.append(byte) + } else { + builder.append("$byte, ") + } + } + return builder.toString() +} + +fun File.getXML(): Document { + val inputStream = FileInputStream(this) + val reader = InputStreamReader(inputStream, "UTF-8") + val inputSource = InputSource(reader) + inputSource.encoding = "UTF-8" + + val builderFactory = DocumentBuilderFactory.newInstance() + val docBuilder = builderFactory.newDocumentBuilder() + return docBuilder.parse(inputSource) +} + +fun File.updateXML(document: Document) { + val output = StringWriter() + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.transform(DOMSource(document), StreamResult(output)) + val xml = output.toString() + this.writeText(xml) +} + +fun File.removeAttributes() { + val content = this.getContent() + .replace("hidden=\"true\"", "") + .replace("hidden=\"false\"", "") + .replace("containsHtml=\"true\"", "") + .replace("containsHtml=\"false\"", "") + .replace("androidTreatment=\"true\"", "") + .replace("androidTreatment=\"false\"", "") + this.writeText(content) + updateXML(this.getXML()) +} + +fun File.getContent() = this.inputStream().readBytes().toString(Charsets.UTF_8) + +fun Task.getModuleName(): String? { + val path = this.project.path + return if (path.isEmpty()) null else path.split(":")[path.split(":").size - 1] +} + +fun Task.dataFound(): Boolean = this.name.contains(pre) + && this.name.contains(build) + && this.name != "$pre$build" + && !this.name.contains(test) + +fun Task.onMergeResourcesStarts(): Boolean = this.name.contains(merge) + && this.name.contains(resources) + && !this.name.contains(test) + +fun Task.onMergeResourcesFinish(): Boolean = this.name.contains(merge) + && this.name.contains(resources) + && !this.name.contains(test) + +fun Task.onMergeAssetsStarts(): Boolean = this.name.contains(generate) + && this.name.contains(assets) + && !this.name.contains(test) + +fun Task.onMergeAssetsFinish(): Boolean = this.name.contains(merge) + && this.name.contains(assets) + && !this.name.contains(test) + +fun Task.dataFoundVariant(): String = this.name.substring(pre.length) + .substring(0, this.name.substring(pre.length).length - build.length) + +fun Task.onMergeResourcesStartsVariant(): String = this.name.substring(merge.length) + .substring(0, this.name.substring(merge.length).length - resources.length) + +fun Task.onMergeResourcesFinishVariant(): String = this.name.substring(merge.length) + .substring(0, this.name.substring(merge.length).length - resources.length) + +fun Task.onMergeAssetsStartsVariant(): String = this.name.substring(generate.length) + .substring(0, this.name.substring(generate.length).length - assets.length) + +fun Task.onMergeAssetsFinishVariant(): String = this.name.substring(merge.length) + .substring(0, this.name.substring(merge.length).length - assets.length) + +fun R.logger(): Lazy { + return lazy { LoggerFactory.getLogger(this.javaClass) } +} + +fun Node.extractHtml(): String { + val stringBuilder = StringBuilder() + for (i in 0 until this.childNodes.length) { + val item = this.childNodes.item(i) + val type = item.getType() + when (type) { + StringType.BR -> stringBuilder.append( + "
${when { + item.textContent.isNotEmpty() -> item.extractHtml() + else -> "" + } + }
" + ) + StringType.I -> stringBuilder.append( + "${when { + item.textContent.isNotEmpty() -> item.extractHtml() + else -> "" + } + }" + ) + StringType.STRONG -> stringBuilder.append( + "${when { + item.textContent.isNotEmpty() -> item.extractHtml() + else -> "" + } + }" + ) + StringType.TEXT -> stringBuilder.append(item.textContent) + } + + } + return stringBuilder.toString() +} + +enum class StringType { + BR, + TEXT, + I, + STRONG +} + +fun Node.getType(): StringType { + return when { + this.toString().contains("[br:") -> StringType.BR + this.toString().contains("[i:") -> StringType.I + this.toString().contains("[strong:") -> StringType.STRONG + this.toString().contains("[#text") -> StringType.TEXT + else -> StringType.TEXT + } +} + +fun StringCareConfiguration.normalize(): StringCareConfiguration { + val stringFiles = mutableListOf() + val sourceFolders = mutableListOf() + this.stringFiles.forEach { file -> + stringFiles.add(file.normalizePath()) + } + this.srcFolders.forEach { folder -> + sourceFolders.add(folder.normalizePath()) + } + this.stringFiles = stringFiles + this.srcFolders = sourceFolders + return this +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Fingerprint.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Fingerprint.kt new file mode 100644 index 0000000..80ab7bf --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Fingerprint.kt @@ -0,0 +1,94 @@ +package dev.vyp.stringcare.plugin.internal + +import dev.vyp.stringcare.plugin.StringCareConfiguration + +private class Fingerprint { + + companion object { + private var key: String? = null + private var until: String? = null + private var error: String? = null + private var variantLocated = false + private var moduleLocated = false + } + + fun extract(module: String, variant: String, configuration: StringCareConfiguration, trace: String): String { + val lines = trace.split("\n") + lines.forEach { line -> + when { + line.lowercase().contains("downloading") -> if (configuration.debug) { + PrintUtils.print(module, line, configuration.debug) + } + line.lowercase().contains("unzipping") -> if (configuration.debug) { + PrintUtils.print(module, line, configuration.debug) + } + line.lowercase().contains("permissions") -> if (configuration.debug) { + PrintUtils.print(module, line, configuration.debug) + } + line.lowercase().contains("config:") && moduleLocated && variantLocated -> { + val k = (line.split(": ").getOrNull(1) ?: "").trim() + val valid = !k.equals("none", ignoreCase = true) + if (!valid) { + key = k + PrintUtils.print(module, "\uD83E\uDD2F no config defined for variant $variant", true) + if (configuration.debug) { + until = key + } + } else if (configuration.debug) { + PrintUtils.print(module, "Module: $module", true) + PrintUtils.print(module, "Variant: $variant", true) + } + + } + line.lowercase().contains("sha1") && moduleLocated && variantLocated -> { + key = line.split(" ").getOrNull(1) ?: "" + if (configuration.debug) { + PrintUtils.print(module, line, configuration.debug) + } + } + line.lowercase().contains("error") -> { + error = line.split(": ").getOrNull(1) + } + line.lowercase().contains("valid until") && moduleLocated && variantLocated -> { + until = line.split(": ").getOrNull(1) + if (configuration.debug) { + PrintUtils.print(module, line, configuration.debug) + } + } + line.lowercase().contains("store") && moduleLocated && variantLocated -> if (configuration.debug) { + PrintUtils.print(module, line, configuration.debug) + } + line.lowercase().contains("variant") && moduleLocated -> { + val locV = line.split(" ").getOrNull(1) ?: "" + if (locV == variant) { + variantLocated = true + } + } + line.lowercase().contains(":$module") -> moduleLocated = true + } + if (key != null && (!configuration.debug || configuration.debug && until != null)) { + return key!! + } + } + return "" + } +} + +fun fingerPrint( + module: String, + variant: String, + configuration: StringCareConfiguration, + keyFound: (key: String) -> Unit +) { + if (configuration.mockedFingerprint.isNotEmpty()) { + keyFound(configuration.mockedFingerprint) + return + } + signingReportTask().runCommand { _, report -> + keyFound(report.extractFingerprint(module, variant, configuration)) + } +} + +fun String.extractFingerprint(module: String = "app", variant: String = "debug", configuration: StringCareConfiguration): String { + return Fingerprint().extract(module, variant, configuration, this) +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Os.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Os.kt new file mode 100644 index 0000000..7c76fbd --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Os.kt @@ -0,0 +1,16 @@ +package dev.vyp.stringcare.plugin.internal + +enum class Os { + WINDOWS, + OSX, + LINUX, +} + +fun getOs(): Os { + val name = System.getProperty("os.name").lowercase() + return when { + name.contains("windows") -> Os.WINDOWS + name.contains("linux") || name.contains("freebsd") -> Os.LINUX + else -> Os.OSX + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/PrintUtils.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/PrintUtils.kt new file mode 100644 index 0000000..c8fa2a0 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/PrintUtils.kt @@ -0,0 +1,50 @@ +package dev.vyp.stringcare.plugin.internal + +import dev.vyp.stringcare.plugin.StringCarePlugin +import org.slf4j.LoggerFactory + +class PrintUtils { + + companion object { + private var variant: String? = null + private var module: String? = null + private val logger = LoggerFactory.getLogger(StringCarePlugin::class.java) + + fun init(module: String, variant: String) { + PrintUtils.module = module + PrintUtils.variant = variant + } + + private fun _print(value: String) { + println(value) + } + + fun print(message: String, tab: Boolean = false) { + if (variant != null && module != null) { + if (!tab) { + _print(":$module:$message") + } else { + _print("\t" + message) + } + } else { + _print(message) + } + } + + fun print(module: String?, message: String, tab: Boolean = false) { + if (module != null) { + if (!tab) { + _print(":$module:$message") + } else { + _print("\t" + message) + } + } else { + _print(message) + } + } + + fun uncapitalize(value: String): String { + return value.substring(0, 1).lowercase() + value.substring(1, value.length) + } + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Stark.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Stark.kt new file mode 100644 index 0000000..3b53ada --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Stark.kt @@ -0,0 +1,250 @@ +package dev.vyp.stringcare.plugin.internal + +import dev.vyp.stringcare.plugin.StringCarePlugin +import java.io.File +import java.io.InputStream +import java.net.JarURLConnection +import java.net.URL +import java.util.zip.ZipFile + +open class Stark { + + companion object { + /** + * Native libraries must NOT be loaded in a static initializer: Gradle loads plugin task classes during + * **configuration** phase, while classpath resources for the plugin JAR are reliably visible during + * **task execution**. Loading here used to run too early β†’ stream null / load failure β†’ permanent "Skipping". + */ + @Volatile + private var nativeLibLoaded = false + + @Volatile + private var nativeLoadAttempted = false + + private fun logNative(message: String) { + // if (StringCarePlugin.configuration.debug) { + println("[StringCare native] $message") + // } + } + + @JvmStatic + fun isNativeLibLoaded(): Boolean { + if (!nativeLoadAttempted) { + logNative( + "Loading native library (os.name=${System.getProperty("os.name")}, " + + "os.arch=${System.getProperty("os.arch")}, os.detected=${getOs()})" + ) + nativeLibLoaded = loadNativeForHost() + nativeLoadAttempted = true + if (nativeLibLoaded) { + logNative("Native library loaded successfully.") + } else { + logNative("Native library NOT loaded β€” obfuscation tasks will skip. See messages above.") + } + } else if (!nativeLibLoaded) { + logNative("isNativeLibLoaded: returning cached false (load attempted earlier in this Gradle JVM)") + } + return nativeLibLoaded + } + + /** Classpath folder where host natives live in the JAR (`dist/` layout). */ + private fun nativeResourceDir(): String = when (getOs()) { + Os.WINDOWS -> "windows" + Os.LINUX -> "linux" + Os.OSX -> "macos" + } + + private fun loadNativeForHost(): Boolean { + val arch = (System.getProperty("os.arch") ?: "").lowercase() + val order = when (getOs()) { + Os.WINDOWS -> { + if (arch == "aarch64") listOf(winLibArm64, winLib) else listOf(winLib, winLibArm64) + } + Os.OSX -> listOf(osxLib) + Os.LINUX -> { + if (arch == "aarch64") listOf(linuxLibArm64, linuxLib) else listOf(linuxLib, linuxLibArm64) + } + } + logNative("Try order for this host: ${order.joinToString()}") + for (name in order) { + if (loadLib(name)) return true + } + return false + } + + /** + * Loads a native library packaged as a classpath resource (JAR or exploded `build/resources/main`). + * Composite builds (`includeBuild("plugin")`) load classes from `classes/kotlin/main` while natives live + * under `resources/main` β€” [openNativeStream] uses classloaders so both layouts work. + */ + private fun loadLib(name: String): Boolean { + return try { + val stream = openNativeStream(name) + if (stream == null) { + logNative("loadLib($name): no bytes found on classpath (see openNativeStream traces)") + return false + } + val temp = createNativeTempFile(name) + stream.use { input -> + temp.outputStream().use { output -> input.copyTo(output) } + } + val size = temp.length() + logNative("loadLib($name): extracted $size bytes β†’ ${temp.absolutePath}") + System.load(temp.absolutePath) + logNative("loadLib($name): System.load OK") + true + } catch (e: UnsatisfiedLinkError) { + logNative("loadLib($name): UnsatisfiedLinkError β€” ${e.message} (wrong CPU arch vs binary slice?)") + false + } catch (e: Throwable) { + logNative("loadLib($name): ${e.javaClass.name} β€” ${e.message}") + false + } + } + + private fun createNativeTempFile(libraryFileName: String): File { + val suffix = when { + libraryFileName.endsWith(".dylib") -> ".dylib" + libraryFileName.endsWith(".so") -> ".so" + libraryFileName.endsWith(".dll") -> ".dll" + else -> ".bin" + } + return File.createTempFile("stringcare-signkey-", suffix).apply { + deleteOnExit() + } + } + + private fun openNativeStream(fileName: String): InputStream? { + val platformDir = nativeResourceDir() + val paths = listOf("/$platformDir/$fileName", "/$fileName") + for (path in paths) { + Stark::class.java.getResourceAsStream(path)?.let { + logNative("openNativeStream($fileName): found via Stark.class $path") + return it + } + } + logNative("openNativeStream($fileName): not on Stark.class resources, trying classloaders…") + val loaders = buildList { + add(Stark::class.java.classLoader) + add(Thread.currentThread().contextClassLoader) + add(ClassLoader.getSystemClassLoader()) + }.distinct() + loaders.forEachIndexed { i, loader -> + if (loader == null) { + logNative("openNativeStream: loader[$i]=null") + return@forEachIndexed + } + val label = loader.javaClass.name + for (path in listOf("$platformDir/$fileName", "/$platformDir/$fileName", fileName, "/$fileName")) { + loader.getResourceAsStream(path)?.let { + logNative("openNativeStream($fileName): found via loader[$i] $label as $path") + return it + } + } + } + logNative("openNativeStream($fileName): trying codeSource / filesystem / nested JAR fallback…") + return openNativeStreamFromCodeSource(fileName) + } + + /** Fallback when resources are not visible to the usual classloaders (edge classloaders). */ + private fun openNativeStreamFromCodeSource(fileName: String): InputStream? { + val url: URL = try { + Stark::class.java.protectionDomain?.codeSource?.location + } catch (_: Throwable) { + null + } ?: run { + logNative("openNativeStreamFromCodeSource: codeSource.location is null") + return null + } + logNative("openNativeStreamFromCodeSource: codeSource=$url") + return when (url.protocol) { + "file" -> { + val file = try { + File(url.toURI()) + } catch (_: Throwable) { + logNative("openNativeStreamFromCodeSource: toURI failed for $url") + return null + } + when { + file.isFile && file.name.endsWith(".jar") -> { + val s = openStreamFromPlainJar(file, fileName) + if (s != null) logNative("openNativeStreamFromCodeSource: found inside JAR ${file.absolutePath}") + else logNative("openNativeStreamFromCodeSource: no entry $fileName in ${file.absolutePath}") + s + } + file.isDirectory -> { + val resMain = File(file, "../../../resources/main") + val a = findNativeFile(file, fileName)?.inputStream() + val b = findNativeFile(resMain, fileName)?.inputStream() + when { + a != null -> { + logNative("openNativeStreamFromCodeSource: file under classes dir ${file.absolutePath}") + a + } + b != null -> { + logNative("openNativeStreamFromCodeSource: file under ${resMain.absolutePath}") + b + } + else -> { + logNative( + "openNativeStreamFromCodeSource: not under ${file.absolutePath} nor $resMain" + ) + null + } + } + } + else -> { + logNative("openNativeStreamFromCodeSource: unexpected file location $file") + null + } + } + } + "jar" -> { + val s = openStreamFromJarUrl(url, fileName) + if (s == null) logNative("openNativeStreamFromCodeSource: jar: URL had no entry $fileName") + s + } + else -> { + logNative("openNativeStreamFromCodeSource: unsupported protocol ${url.protocol}") + null + } + } + } + + private fun findNativeFile(dir: File, fileName: String): File? { + if (!dir.isDirectory) return null + return dir.walkTopDown().find { it.isFile && it.name == fileName } + } + + private fun openStreamFromPlainJar(jar: File, fileName: String): InputStream? { + if (!jar.isFile) return null + return ZipFile(jar).use { zip -> + val entry = zip.entries().asSequence().firstOrNull { e -> + !e.isDirectory && (e.name == fileName || e.name.endsWith("/$fileName")) + } ?: return null + zip.getInputStream(entry).readBytes().inputStream() + } + } + + private fun openStreamFromJarUrl(url: URL, fileName: String): InputStream? { + return try { + val conn = url.openConnection() as JarURLConnection + val jar = conn.jarFile + val entry = jar.entries().asSequence().firstOrNull { e -> + !e.isDirectory && (e.name == fileName || e.name.endsWith("/$fileName")) + } ?: return null + logNative("openStreamFromJarUrl: entry ${entry.name}") + jar.getInputStream(entry).readBytes().inputStream() + } catch (e: Throwable) { + logNative("openStreamFromJarUrl: ${e.javaClass.simpleName} β€” ${e.message}") + null + } + } + + @JvmStatic + external fun obfuscate(key: String, value: ByteArray, mockId: String): ByteArray + + @JvmStatic + external fun reveal(key: String, value: ByteArray, mockId: String): ByteArray + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Tasks.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Tasks.kt new file mode 100644 index 0000000..921e337 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Tasks.kt @@ -0,0 +1,72 @@ +package dev.vyp.stringcare.plugin.internal + +import java.io.File + +internal fun signingReportTask(): String = "${when (getOs()) { + Os.WINDOWS -> wrapperWindows + Os.OSX, Os.LINUX -> wrapperOsX +}} signingReport" + +internal fun gradleWrapper(): String = when (getOs()) { + Os.WINDOWS -> wrapperWindows + Os.OSX, Os.LINUX -> wrapperOsX +} + +internal fun pluginBuildTask(): String = "${when (getOs()) { + Os.WINDOWS -> wrapperWindows + Os.OSX, Os.LINUX -> wrapperOsX +}} build --exclude-task test" + +internal val librarySetupTask = """ + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$osxLib out${File.separator}production${File.separator}classes${File.separator}$osxLib & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLib out${File.separator}production${File.separator}classes${File.separator}$linuxLib & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLibArm64 out${File.separator}production${File.separator}classes${File.separator}$linuxLibArm64 & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLib out${File.separator}production${File.separator}classes${File.separator}$winLib & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLibArm64 out${File.separator}production${File.separator}classes${File.separator}$winLibArm64 & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$osxLib build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$osxLib & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLib build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$linuxLib & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$linuxLibArm64 build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$linuxLibArm64 & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLib build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$winLib & + ${copyCommand()} src${File.separator}main${File.separator}kotlin${File.separator}dev${File.separator}vyp${File.separator}stringcare${File.separator}plugin${File.separator}internal${File.separator}jni${File.separator}$winLibArm64 build${File.separator}classes${File.separator}kotlin${File.separator}main${File.separator}$winLibArm64 + """.trimIndent() + +internal fun prepareTask(directory: String): String { + return """ + cd $directory && + git clone https://github.com/StringCare/$testProjectName.git && + cd $testProjectName + """.trimIndent() +} + +internal fun buildTask(directory: String): String { + return """ + cd $directory && + ${gradleWrapper()} build + """.trimIndent() +} + +internal fun basicGradleTask(directory: String): String { + return """ + cd $directory && + ${gradleWrapper()} $gradleTaskNameDoctor + """.trimIndent() +} + +internal fun obfuscationTestGradleTask(directory: String): String { + return """ + cd $directory && + ${gradleWrapper()} $gradleTaskNameObfuscate + """.trimIndent() +} + +internal fun signingReportTask(directory: String): String { + return """ + ${prepareTask(directory)} && + ${signingReportTask()} + """.trimIndent() +} + +internal fun copyCommand(): String = when (getOs()) { + Os.WINDOWS -> copyCommandWindows + Os.OSX, Os.LINUX -> copyCommandOsX +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/VariantApi.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/VariantApi.kt new file mode 100644 index 0000000..e8d1457 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/VariantApi.kt @@ -0,0 +1,100 @@ +package dev.vyp.stringcare.plugin.internal + +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.StringCareExtension +import dev.vyp.stringcare.plugin.StringCarePlugin +import dev.vyp.stringcare.plugin.tasks.ObfuscateAssetsTask +import dev.vyp.stringcare.plugin.tasks.ObfuscateStringsTask +import dev.vyp.stringcare.plugin.tasks.RestoreAssetsTask +import dev.vyp.stringcare.plugin.tasks.RestoreStringsTask +import com.google.gson.Gson +import org.gradle.api.Project + +private fun String.variantTaskSuffix(): String = replaceFirstChar { it.uppercase() } + +fun Project.registerVariantObfuscationTasks(extension: StringCareExtension, config: StringCareConfiguration) { + val androidComponents = extensions.findByType(ApplicationAndroidComponentsExtension::class.java) + ?: return + + val gson = Gson() + val projectPath = StringCarePlugin.absoluteProjectPath + val moduleName = config.name + val variantNames = mutableListOf() + + androidComponents.onVariants(androidComponents.selector().all()) { variant -> + val variantName: String = variant.name + val applicationId = variant.applicationId.get() + StringCarePlugin.variantMap[variantName] = applicationId + if (config.applicationId.isEmpty()) config.applicationId = applicationId + val variantCapitalized = variantName.variantTaskSuffix() + + val beforeStrings = tasks.register( + "stringcareBeforeMergeResources$variantCapitalized", + ObfuscateStringsTask::class.java + ) { + it.projectPath = projectPath + it.moduleName = moduleName + it.variantName = variantName + it.applicationId = applicationId + it.skip = config.skip + it.debug = config.debug + it.mockedFingerprint = config.mockedFingerprint + it.srcFoldersJson = gson.toJson(config.srcFolders) + it.stringFilesJson = gson.toJson(config.stringFiles) + it.assetsFilesJson = gson.toJson(config.assetsFiles) + } + tasks.register( + "stringcareAfterMergeResources$variantCapitalized", + RestoreStringsTask::class.java + ) { + it.projectPath = projectPath + it.moduleName = moduleName + it.skip = config.skip + } + variantNames.add(variantCapitalized) + + val beforeAssets = tasks.register( + "stringcareBeforeMergeAssets$variantCapitalized", + ObfuscateAssetsTask::class.java + ) { + it.projectPath = projectPath + it.moduleName = moduleName + it.variantName = variantName + it.applicationId = applicationId + it.skip = config.skip + it.debug = config.debug + it.mockedFingerprint = config.mockedFingerprint + it.srcFoldersJson = gson.toJson(config.srcFolders) + it.stringFilesJson = gson.toJson(config.stringFiles) + it.assetsFilesJson = gson.toJson(config.assetsFiles) + } + tasks.register( + "stringcareAfterMergeAssets$variantCapitalized", + RestoreAssetsTask::class.java + ) { + it.projectPath = projectPath + it.moduleName = moduleName + it.skip = config.skip + } + } + + project.afterEvaluate { + for (variantCapitalized in variantNames) { + val mergeRes = "merge${variantCapitalized}Resources" + val beforeRes = "stringcareBeforeMergeResources$variantCapitalized" + val afterRes = "stringcareAfterMergeResources$variantCapitalized" + val processRes = "process${variantCapitalized}Resources" + tasks.findByName(mergeRes)?.dependsOn(beforeRes) + tasks.findByName(afterRes)?.dependsOn(mergeRes) + tasks.findByName(processRes)?.dependsOn(afterRes) + val genAssets = "generate${variantCapitalized}Assets" + val beforeAssets = "stringcareBeforeMergeAssets$variantCapitalized" + val mergeAssets = "merge${variantCapitalized}Assets" + val afterAssets = "stringcareAfterMergeAssets$variantCapitalized" + tasks.findByName(genAssets)?.dependsOn(beforeAssets) + tasks.findByName(afterAssets)?.dependsOn(mergeAssets) + tasks.findByName("compress${variantCapitalized}Assets")?.dependsOn(afterAssets) + } + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Vars.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Vars.kt new file mode 100644 index 0000000..845986f --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/Vars.kt @@ -0,0 +1,31 @@ +package dev.vyp.stringcare.plugin.internal + +internal const val version = "5.0.0" +internal const val testProjectName = "KotlinSample" +internal const val defaultMainModule = "app" +internal const val gradleTaskNameDoctor = "stringcarePreview" +internal const val gradleTaskNameObfuscate = "stringcareTestObfuscate" +internal const val extensionName = "stringcare" +internal const val winLib = "libsignKey.dll" +internal const val winLibArm64 = "libsignKey-arm64.dll" +internal const val osxLib = "libsignKey.dylib" +internal const val linuxLib = "libsignKey.so" +internal const val linuxLibArm64 = "libsignKey-arm64.so" +internal const val wrapperOsX = "sh gradlew" +internal const val wrapperWindows = "gradlew.bat" +internal const val copyCommandOsX = "cp" +internal const val copyCommandWindows = "copy" +internal const val emptyChar = "" +const val backupStringRes = "backupStringResources" +const val obfuscateStringRes = "obfuscateStringResources" +const val restoreStringRes = "restoreStringResources" +const val backupAssets = "backupAssets" +const val obfuscateAssets = "obfuscateAssets" +const val restoreAssets = "restoreAssets" +internal const val test = "Test" +internal const val pre = "pre" +internal const val build = "Build" +internal const val generate = "generate" +internal const val merge = "merge" +internal const val resources = "Resources" +internal const val assets = "Assets" diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/XParser.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/XParser.kt new file mode 100644 index 0000000..44886c3 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/XParser.kt @@ -0,0 +1,123 @@ +package dev.vyp.stringcare.plugin.internal + +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.StringCarePlugin +import dev.vyp.stringcare.plugin.models.ResourceFile +import dev.vyp.stringcare.plugin.models.SAttribute +import dev.vyp.stringcare.plugin.models.StringEntity +import java.io.File + +fun locateResourceFiles(projectPath: String, configuration: StringCareConfiguration): List { + if (configuration.debug) { + println("== RESOURCE FILES FOUND ======================================") + } + return File(projectPath).walkTopDown() + .filterIndexed { _, file -> + file.validForXMLConfiguration(configuration.normalize()) + }.map { + it.resourceFile(configuration.normalize())!! + }.toList() +} + +fun backupResourceFiles(projectPath: String, configuration: StringCareConfiguration): List { + val files = locateResourceFiles(projectPath, configuration.normalize()) + files.forEach { resource -> + resource.backup() + } + return files +} + +fun restoreResourceFiles(projectPath: String, module: String): List { + val resourceFiles = File("${StringCarePlugin.tempFolder}${File.separator}$module") + .walkTopDown().toList().filter { file -> + !file.isDirectory + }.map { + it.restore(projectPath) + } + StringCarePlugin.resetFolder() + return resourceFiles +} + +fun parseXML(file: File): List { + val entities = mutableListOf() + + val doc = file.getXML() + val nList = doc.getElementsByTagName("string") + for (i in 0 until nList.length) { + val node = nList.item(i) + var name = "" + val attributes = mutableListOf() + var obfuscate = false + var androidTreatment = true + var containsHtml = false + for (a in 0 until node.attributes.length) { + val attribute = node.attributes.item(a) + for (n in 0 until attribute.childNodes.length) { + val attr = attribute.childNodes.item(n) + if (attribute.nodeName == "name") name = attr.nodeValue + if (attribute.nodeName == "hidden" && attr.nodeValue != "false") { + obfuscate = true + } + if (attribute.nodeName == "androidTreatment" && attr.nodeValue == "false") { + androidTreatment = false + } + if (attribute.nodeName == "containsHtml" && attr.nodeValue != "false") { + containsHtml = true + } + attributes.add(SAttribute(attribute.nodeName, attr.nodeValue)) + } + } + if (obfuscate) { + entities.add( + StringEntity( + name, attributes, when { + containsHtml -> node.extractHtml() + else -> node.textContent + }, "string", i, androidTreatment + ) + ) + } + } + return entities +} + +fun modifyXML(file: File, key: String, configuration: StringCareConfiguration) { + val stringEntities = parseXML(file) + if (configuration.debug) { + PrintUtils.print(null, file.getContent(), true) + } + + val doc = file.getXML() + val nList = doc.getElementsByTagName("string") + for (i in 0 until nList.length) { + val node = nList.item(i) + val entity = stringEntities.find { + it.tag == "string" && it.index == i + } + entity?.let { + node.textContent = obfuscateStringEntity(key, it, configuration.applicationId).value + } + } + + file.updateXML(doc) + file.removeAttributes() + if (configuration.debug) { + PrintUtils.print(null, file.getContent(), true) + } +} + +fun obfuscateStringEntity(key: String, entity: StringEntity, mockId: String): StringEntity { + val obfuscation = Stark.obfuscate(key, when (entity.androidTreatment) { + true -> entity.value.androidTreatment() + false -> entity.value.unescape() + }.toByteArray(), + mockId + ).toReadableString() + return StringEntity(entity.name, entity.attributes, obfuscation, entity.tag, entity.index, entity.androidTreatment) +} + +fun revealStringEntity(key: String, entity: StringEntity, mockId: String): StringEntity { + val arr: ByteArray = entity.value.split(", ").map { it.toInt().toByte() }.toByteArray() + val original = String(Stark.reveal(key, arr, mockId)) + return StringEntity(entity.name, entity.attributes, original, entity.tag, entity.index, entity.androidTreatment) +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/libsignKey-arm64.so b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/libsignKey-arm64.so new file mode 100755 index 0000000..73af4ad Binary files /dev/null and b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/libsignKey-arm64.so differ diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/libsignKey.so b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/libsignKey.so new file mode 100755 index 0000000..31ff3a4 Binary files /dev/null and b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/linux/libsignKey.so differ diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/macos/libsignKey-arm64.dylib b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/macos/libsignKey-arm64.dylib new file mode 100755 index 0000000..648f84e Binary files /dev/null and b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/macos/libsignKey-arm64.dylib differ diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/macos/libsignKey.dylib b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/macos/libsignKey.dylib new file mode 100755 index 0000000..aebacc5 Binary files /dev/null and b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/macos/libsignKey.dylib differ diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/libsignKey-arm64.dll b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/libsignKey-arm64.dll new file mode 100644 index 0000000..4ea0ff5 Binary files /dev/null and b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/libsignKey-arm64.dll differ diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/libsignKey.dll b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/libsignKey.dll new file mode 100644 index 0000000..b5a3f61 Binary files /dev/null and b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/internal/jni/windows/libsignKey.dll differ diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/AssetsFile.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/AssetsFile.kt new file mode 100644 index 0000000..f286631 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/AssetsFile.kt @@ -0,0 +1,5 @@ +package dev.vyp.stringcare.plugin.models + +import java.io.File + +data class AssetsFile(val file: File, val sourceFolder: String, val module: String) diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/ExecutionResult.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/ExecutionResult.kt new file mode 100644 index 0000000..094c7ec --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/ExecutionResult.kt @@ -0,0 +1,3 @@ +package dev.vyp.stringcare.plugin.models + +data class ExecutionResult(val command: String, val result: String) diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/ResourceFile.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/ResourceFile.kt new file mode 100644 index 0000000..32f9438 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/ResourceFile.kt @@ -0,0 +1,5 @@ +package dev.vyp.stringcare.plugin.models + +import java.io.File + +data class ResourceFile(val file: File, val sourceFolder: String, val module: String) diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/StringEntity.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/StringEntity.kt new file mode 100644 index 0000000..941ec2c --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/models/StringEntity.kt @@ -0,0 +1,12 @@ +package dev.vyp.stringcare.plugin.models + +open class StringEntity( + var name: String, + var attributes: List, + var value: String, + val tag: String, + val index: Int, + val androidTreatment: Boolean = true +) + +data class SAttribute(val name: String, val value: String) diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/ObfuscateAssetsTask.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/ObfuscateAssetsTask.kt new file mode 100644 index 0000000..5303358 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/ObfuscateAssetsTask.kt @@ -0,0 +1,95 @@ +package dev.vyp.stringcare.plugin.tasks + +import com.google.gson.Gson +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.internal.PrintUtils +import dev.vyp.stringcare.plugin.internal.backupAssets +import dev.vyp.stringcare.plugin.internal.backupAssetsFiles +import dev.vyp.stringcare.plugin.internal.fingerPrint +import dev.vyp.stringcare.plugin.internal.getContent +import dev.vyp.stringcare.plugin.internal.locateAssetsFiles +import dev.vyp.stringcare.plugin.internal.obfuscateAssets +import dev.vyp.stringcare.plugin.internal.obfuscateFile +import dev.vyp.stringcare.plugin.internal.Stark +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class ObfuscateAssetsTask : DefaultTask() { + + @Input + var projectPath: String = "" + + @Input + var moduleName: String = "" + + @Input + var variantName: String = "" + + @Input + var applicationId: String = "" + + @Input + var skip: Boolean = false + + @Input + var debug: Boolean = false + + @Input + var mockedFingerprint: String = "" + + @Input + var srcFoldersJson: String = "[]" + + @Input + var stringFilesJson: String = "[]" + + @Input + var assetsFilesJson: String = "[]" + + private fun configuration(): StringCareConfiguration { + val gson = Gson() + return StringCareConfiguration(moduleName).apply { + this.applicationId = this@ObfuscateAssetsTask.applicationId + this.skip = this@ObfuscateAssetsTask.skip + this.debug = this@ObfuscateAssetsTask.debug + this.mockedFingerprint = this@ObfuscateAssetsTask.mockedFingerprint + @Suppress("UNCHECKED_CAST") + gson.fromJson(srcFoldersJson, MutableList::class.java)?.let { srcFolders.addAll(it as List) } + @Suppress("UNCHECKED_CAST") + gson.fromJson(stringFilesJson, MutableList::class.java)?.let { stringFiles.addAll(it as List) } + @Suppress("UNCHECKED_CAST") + gson.fromJson(assetsFilesJson, MutableList::class.java)?.let { assetsFiles.addAll(it as List) } + } + } + + @TaskAction + fun run() { + val config = configuration() + if (skip) { + PrintUtils.print(moduleName, "Skipping $variantName") + return + } + if (!Stark.isNativeLibLoaded()) { + PrintUtils.print(moduleName, "Skipping $variantName (native library not available for this architecture)") + return + } + PrintUtils.print("", "ApplicationId: $applicationId", tab = true) + fingerPrint(moduleName, variantName, config) { key -> + if (key == "none" || key.trim().isEmpty()) { + PrintUtils.print("No SHA1 key found for :$moduleName:$variantName") + return@fingerPrint + } + PrintUtils.print(moduleName, "$variantName:$key") + PrintUtils.print(moduleName, backupAssets) + backupAssetsFiles(projectPath, config) + val files = locateAssetsFiles(projectPath, config) + files.forEach { file -> + if (debug) PrintUtils.print(null, file.file.getContent()) + obfuscateFile(key, file.file, config.applicationId) + if (debug) PrintUtils.print(null, file.file.getContent()) + } + PrintUtils.print(moduleName, obfuscateAssets) + } + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/ObfuscateStringsTask.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/ObfuscateStringsTask.kt new file mode 100644 index 0000000..af17807 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/ObfuscateStringsTask.kt @@ -0,0 +1,90 @@ +package dev.vyp.stringcare.plugin.tasks + +import com.google.gson.Gson +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.internal.PrintUtils +import dev.vyp.stringcare.plugin.internal.backupResourceFiles +import dev.vyp.stringcare.plugin.internal.backupStringRes +import dev.vyp.stringcare.plugin.internal.fingerPrint +import dev.vyp.stringcare.plugin.internal.locateResourceFiles +import dev.vyp.stringcare.plugin.internal.modifyXML +import dev.vyp.stringcare.plugin.internal.obfuscateStringRes +import dev.vyp.stringcare.plugin.internal.Stark +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class ObfuscateStringsTask : DefaultTask() { + + @Input + var projectPath: String = "" + + @Input + var moduleName: String = "" + + @Input + var variantName: String = "" + + @Input + var applicationId: String = "" + + @Input + var skip: Boolean = false + + @Input + var debug: Boolean = false + + @Input + var mockedFingerprint: String = "" + + @Input + var srcFoldersJson: String = "[]" + + @Input + var stringFilesJson: String = "[]" + + @Input + var assetsFilesJson: String = "[]" + + private fun configuration(): StringCareConfiguration { + val gson = Gson() + return StringCareConfiguration(moduleName).apply { + this.applicationId = this@ObfuscateStringsTask.applicationId + this.skip = this@ObfuscateStringsTask.skip + this.debug = this@ObfuscateStringsTask.debug + this.mockedFingerprint = this@ObfuscateStringsTask.mockedFingerprint + @Suppress("UNCHECKED_CAST") + gson.fromJson(srcFoldersJson, MutableList::class.java)?.let { srcFolders.addAll(it as List) } + @Suppress("UNCHECKED_CAST") + gson.fromJson(stringFilesJson, MutableList::class.java)?.let { stringFiles.addAll(it as List) } + @Suppress("UNCHECKED_CAST") + gson.fromJson(assetsFilesJson, MutableList::class.java)?.let { assetsFiles.addAll(it as List) } + } + } + + @TaskAction + fun run() { + val config = configuration() + if (skip) { + PrintUtils.print(moduleName, "Skipping $variantName") + return + } + if (!Stark.isNativeLibLoaded()) { + PrintUtils.print(moduleName, "Skipping $variantName (native library not available for this architecture)") + return + } + PrintUtils.print("", "ApplicationId: $applicationId", tab = true) + fingerPrint(moduleName, variantName, config) { key -> + if (key == "none" || key.trim().isEmpty()) { + PrintUtils.print("No SHA1 key found for :$moduleName:$variantName") + return@fingerPrint + } + PrintUtils.print(moduleName, "$variantName:$key") + PrintUtils.print(moduleName, backupStringRes) + backupResourceFiles(projectPath, config) + val files = locateResourceFiles(projectPath, config) + files.forEach { modifyXML(it.file, key, config) } + } + PrintUtils.print(moduleName, obfuscateStringRes) + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/RestoreAssetsTask.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/RestoreAssetsTask.kt new file mode 100644 index 0000000..db64d38 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/RestoreAssetsTask.kt @@ -0,0 +1,27 @@ +package dev.vyp.stringcare.plugin.tasks + +import dev.vyp.stringcare.plugin.internal.PrintUtils +import dev.vyp.stringcare.plugin.internal.restoreAssets +import dev.vyp.stringcare.plugin.internal.restoreAssetsFiles +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class RestoreAssetsTask : DefaultTask() { + + @Input + var projectPath: String = "" + + @Input + var moduleName: String = "" + + @Input + var skip: Boolean = false + + @TaskAction + fun run() { + if (skip) return + PrintUtils.print(moduleName, restoreAssets) + restoreAssetsFiles(projectPath, moduleName) + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/RestoreStringsTask.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/RestoreStringsTask.kt new file mode 100644 index 0000000..e48198b --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/RestoreStringsTask.kt @@ -0,0 +1,27 @@ +package dev.vyp.stringcare.plugin.tasks + +import dev.vyp.stringcare.plugin.internal.PrintUtils +import dev.vyp.stringcare.plugin.internal.restoreResourceFiles +import dev.vyp.stringcare.plugin.internal.restoreStringRes +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class RestoreStringsTask : DefaultTask() { + + @Input + var projectPath: String = "" + + @Input + var moduleName: String = "" + + @Input + var skip: Boolean = false + + @TaskAction + fun run() { + if (skip) return + PrintUtils.print(moduleName, restoreStringRes) + restoreResourceFiles(projectPath, moduleName) + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/SCPreview.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/SCPreview.kt new file mode 100644 index 0000000..666873a --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/SCPreview.kt @@ -0,0 +1,96 @@ +package dev.vyp.stringcare.plugin.tasks + +import com.google.gson.Gson +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.StringCarePlugin +import dev.vyp.stringcare.plugin.internal.getContent +import dev.vyp.stringcare.plugin.internal.locateResourceFiles +import dev.vyp.stringcare.plugin.internal.parseXML +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import java.io.File + +open class SCPreview : DefaultTask() { + + @Input + var module = String() + + @Input + var assetsFiles = String() + + @Input + var stringFiles = String() + + @Input + var srcFolders = String() + + @Input + var debug = false + + @Input + var skip = false + + @Input + var applicationId = "" + + @Input + var mockedFingerprint = "" + + @Input + var variantName = "debug" + + @TaskAction + fun greet() { + val gson = Gson() + println("== REPORT ======================================") + val task = this + println("\t== $variantName ======================================") + val configuration = StringCareConfiguration(module).apply { + @Suppress("UNCHECKED_CAST") + val lSrcFolders = gson.fromJson(task.srcFolders, MutableList::class.java) + if (task.srcFolders.isNotEmpty()) { + srcFolders.addAll(lSrcFolders as MutableList) + } + @Suppress("UNCHECKED_CAST") + val lStringFiles = gson.fromJson(task.stringFiles, MutableList::class.java) + if (task.stringFiles.isNotEmpty()) { + stringFiles.addAll(lStringFiles as MutableList) + } + @Suppress("UNCHECKED_CAST") + val lAssetsFiles = gson.fromJson(task.assetsFiles, MutableList::class.java) + if (task.assetsFiles.isNotEmpty()) { + assetsFiles.addAll(lAssetsFiles as MutableList) + } + if (task.mockedFingerprint.isNotEmpty()) { + mockedFingerprint = task.mockedFingerprint + } + applicationId = task.applicationId + skip = task.skip + debug = task.debug + } + val files = locateResourceFiles(StringCarePlugin.absoluteProjectPath, configuration) + println("\tLocated files(${files.size}) for obfuscating") + println("\tConfig (${gson.toJson(configuration)})") + files.forEach { file -> + println("\t- ${file.file.name}") + println("\t\t${file.module}${File.separator}${file.sourceFolder}${File.separator}") + val entities = parseXML(file.file) + println("\tpath: ${file.file.absolutePath}") + println("") + println("\t============================") + entities.forEach { entity -> + entity.attributes.forEach { attribute -> + println("\t\"${attribute.name}\": \"${attribute.value}\"") + } + println("\t\"value\": \"${entity.value}\"") + println("\t============================") + } + println("") + println("\t=== content ================") + println("" + file.file.getContent()) + println("\t============================") + } + println("== END REPORT ==================================") + } +} diff --git a/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/SCTestObfuscation.kt b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/SCTestObfuscation.kt new file mode 100644 index 0000000..3ea11f2 --- /dev/null +++ b/plugin/src/main/kotlin/dev/vyp/stringcare/plugin/tasks/SCTestObfuscation.kt @@ -0,0 +1,114 @@ +package dev.vyp.stringcare.plugin.tasks + +import com.google.gson.Gson +import dev.vyp.stringcare.plugin.StringCareConfiguration +import dev.vyp.stringcare.plugin.StringCarePlugin +import dev.vyp.stringcare.plugin.internal.backupResourceFiles +import dev.vyp.stringcare.plugin.internal.extractFingerprint +import dev.vyp.stringcare.plugin.internal.getContent +import dev.vyp.stringcare.plugin.internal.modifyXML +import dev.vyp.stringcare.plugin.internal.parseXML +import dev.vyp.stringcare.plugin.internal.restoreResourceFiles +import dev.vyp.stringcare.plugin.internal.runCommand +import dev.vyp.stringcare.plugin.internal.signingReportTask +import dev.vyp.stringcare.plugin.internal.Stark +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class SCTestObfuscation : DefaultTask() { + + @Input + var module = String() + + @Input + var assetsFiles = String() + + @Input + var stringFiles = String() + + @Input + var srcFolders = String() + + @Input + var debug = false + + @Input + var skip = false + + @Input + var applicationId = "" + + @Input + var mockedFingerprint = "" + + @Input + var variantName = "debug" + + @TaskAction + fun greet() { + println("== TEST OBFUSCATION ======================================") + var key = "" + val gson = Gson() + + val task = this + val configuration = StringCareConfiguration(module).apply { + @Suppress("UNCHECKED_CAST") + val lSrcFolders = gson.fromJson(task.srcFolders, MutableList::class.java) + if (task.srcFolders.isNotEmpty()) { + srcFolders.addAll(lSrcFolders as MutableList) + } + @Suppress("UNCHECKED_CAST") + val lStringFiles = gson.fromJson(task.stringFiles, MutableList::class.java) + if (task.stringFiles.isNotEmpty()) { + stringFiles.addAll(lStringFiles as MutableList) + } + @Suppress("UNCHECKED_CAST") + val lAssetsFiles = gson.fromJson(task.assetsFiles, MutableList::class.java) + if (task.assetsFiles.isNotEmpty()) { + assetsFiles.addAll(lAssetsFiles as MutableList) + } + if (task.mockedFingerprint.isNotEmpty()) { + mockedFingerprint = task.mockedFingerprint + } + applicationId = task.applicationId + skip = task.skip + debug = task.debug + } + println("\t== $variantName ======================================") + if (configuration.skip) { + println("\tSkipping (skip=true)") + return + } + if (!Stark.isNativeLibLoaded()) { + println("\tSkipping (native library not available for this architecture)") + return + } + signingReportTask().runCommand { _, result -> + key = result.extractFingerprint(module, variantName, configuration) + } + val filesToObfuscate = backupResourceFiles(StringCarePlugin.absoluteProjectPath, configuration) + filesToObfuscate.forEach { file -> + val originalEntities = parseXML(file.file) + println("\t============================") + println("\tpath: ${file.file.absolutePath}") + originalEntities.forEach { entity -> + entity.attributes.forEach { attribute -> + println("\"${attribute.name}\": \"${attribute.value}\"") + } + println("\"\tvalue\": \"${entity.value}\"") + println("\t============================") + } + println("") + println("\t=== content ================") + println(file.file.getContent()) + println("\t============================") + modifyXML(file.file, key, configuration) + println("\t=== content obfuscated ================") + println(file.file.getContent()) + println("\t============================") + } + restoreResourceFiles(StringCarePlugin.absoluteProjectPath, module) + println("== END OBFUSCATION ==================================") + } +} diff --git a/plugin/src/main/resources/META-INF/gradle-plugins/dev.vyp.stringcare.plugin.properties b/plugin/src/main/resources/META-INF/gradle-plugins/dev.vyp.stringcare.plugin.properties new file mode 100644 index 0000000..c730a30 --- /dev/null +++ b/plugin/src/main/resources/META-INF/gradle-plugins/dev.vyp.stringcare.plugin.properties @@ -0,0 +1 @@ +implementation-class=dev.vyp.stringcare.plugin.StringCarePlugin diff --git a/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/AssetsTest.kt b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/AssetsTest.kt new file mode 100644 index 0000000..53f562e --- /dev/null +++ b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/AssetsTest.kt @@ -0,0 +1,133 @@ +package dev.vyp.stringcare.plugin + +import dev.vyp.stringcare.plugin.internal.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.File + +class AssetsTest { + + // private val logger by logger() + + private val configuration = defaultConfig().apply { + debug = true + applicationId = "com.stringcare.sample" + stringFiles.add("strings_extra.xml") + srcFolders.add("src/other_source") + } + + @BeforeEach + fun setup() { + librarySetupTask.runCommand() + } + + @Test + fun `01 - (PLUGIN) locate assets files for default configuration`() { + val temp = tempPath() + prepareTask(temp).runCommand { _, _ -> + assert(locateAssetsFiles("$temp${File.separator}$testProjectName", configuration.apply { + assetsFiles = mutableListOf("*.json") + }).isNotEmpty()) + } + StringCarePlugin.resetFolder() + } + + @Test + fun `02 - (PLUGIN) backup assets files`() { + val temp = tempPath() + prepareTask(temp).runCommand { _, _ -> + assert(backupAssetsFiles("$temp${File.separator}$testProjectName", configuration.apply { + assetsFiles = mutableListOf("*.json") + }).isNotEmpty()) + } + StringCarePlugin.resetFolder() + } + + @Test + fun `03 - (PLUGIN) restore assets files`() { + val temp = tempPath() + prepareTask(temp).runCommand { _, report -> + println(report) + assert( + restoreAssetsFiles("$temp${File.separator}$testProjectName", defaultMainModule).isEmpty() + ) + assert( + backupAssetsFiles("$temp${File.separator}$testProjectName", configuration.apply { + assetsFiles = mutableListOf("*.json") + }).isNotEmpty() + ) + assert( + restoreAssetsFiles("$temp${File.separator}$testProjectName", defaultMainModule).isNotEmpty() + ) + } + } + + @Test + fun `04 - (PLUGIN) asset obfuscation`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + val key = report.extractFingerprint(variant = "prodDebug", configuration = configuration) + println(key) + assert(key.isNotEmpty()) + val files = locateAssetsFiles( + "$temp${File.separator}$testProjectName", + configuration.apply { + assetsFiles = mutableListOf("*.json") + }) + assert(files.isNotEmpty()) + files.forEach { + println("-------------------------------------------------------") + val original = it.file.getContent() + println("original: \n $original") + obfuscateFile( + key, + it.file, + configuration.applicationId + ) + val obfuscated = it.file.getContent() + println("obfuscated: \n $obfuscated") + assert(original != obfuscated) + } + } + } + + @Test + fun `05 - (PLUGIN) asset reveal`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + val key = report.extractFingerprint(variant = "prodDebug", configuration = configuration) + println(key) + assert(key.isNotEmpty()) + val files = locateAssetsFiles( + "$temp${File.separator}$testProjectName", + configuration.apply { + assetsFiles = mutableListOf("*.json") + }) + assert(files.isNotEmpty()) + files.forEach { + println("-------------------------------------------------------") + val original = it.file.getContent() + println("original: \n $original") + obfuscateFile( + key, + it.file, + configuration.applicationId + ) + val obfuscated = it.file.getContent() + println("obfuscated: \n $obfuscated") + assert(original != obfuscated) + revealFile( + key, + it.file, + configuration.applicationId + ) + val reveal = it.file.getContent() + println("reveal: \n $reveal") + assert(original == reveal) + } + } + } + +} \ No newline at end of file diff --git a/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/PluginApplicationTest.kt b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/PluginApplicationTest.kt new file mode 100644 index 0000000..520d152 --- /dev/null +++ b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/PluginApplicationTest.kt @@ -0,0 +1,17 @@ +package dev.vyp.stringcare.plugin + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test + +class PluginApplicationTest { + + @Test + fun `plugin applies and creates stringcare extension`() { + val project: Project = ProjectBuilder.builder().build() + project.pluginManager.apply("dev.vyp.stringcare.plugin") + val extension = project.extensions.findByName("stringcare") + assertNotNull(extension) + } +} diff --git a/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/SCTest.kt b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/SCTest.kt new file mode 100644 index 0000000..503cd2b --- /dev/null +++ b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/SCTest.kt @@ -0,0 +1,302 @@ +package dev.vyp.stringcare.plugin + +import dev.vyp.stringcare.plugin.internal.* +import dev.vyp.stringcare.plugin.models.StringEntity +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import dev.vyp.stringcare.plugin.utils.modifyForTest +import java.io.File + +class SCTest { + + private val configuration = defaultConfig().apply { + debug = true + applicationId = "com.stringcare.sample" + stringFiles.add("strings_extra.xml") + srcFolders.add("src/other_source") + } + + @BeforeEach + fun setup() { + librarySetupTask.runCommand() + } + + @Test + fun `01 - (PLUGIN) terminal verification`() { + "echo $extensionName".runCommand { command, result -> + println(result) + assert(command.contains(result.normalize())) + } + } + + @Test + fun `02 - (PLUGIN) gradlew signingReport`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + assert(report.contains("SHA1") && report.contains("BUILD SUCCESSFUL")) + } + } + + @Test + fun `03 - (PLUGIN) fingerprint extraction`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + assert(report.extractFingerprint(variant = "prodDebug", configuration = configuration).split(":").size == 20) + } + } + + @Test + fun `04 - (PLUGIN) locate string files for default configuration`() { + val temp = tempPath() + prepareTask(temp).runCommand { _, _ -> + assert(locateResourceFiles("$temp${File.separator}$testProjectName", configuration).isNotEmpty()) + } + StringCarePlugin.resetFolder() + } + + @Test + fun `05 - (PLUGIN) backup string files`() { + val temp = tempPath() + prepareTask(temp).runCommand { _, _ -> + assert(backupResourceFiles("$temp${File.separator}$testProjectName", configuration).isNotEmpty()) + } + StringCarePlugin.resetFolder() + } + + @Test + fun `06 - (PLUGIN) restore string files`() { + val temp = tempPath() + prepareTask(temp).runCommand { _, report -> + println(report) + assert( + restoreResourceFiles("$temp${File.separator}$testProjectName", defaultMainModule).isEmpty() + ) + assert( + backupResourceFiles("$temp${File.separator}$testProjectName", configuration).isNotEmpty() + ) + assert( + restoreResourceFiles("$temp${File.separator}$testProjectName", defaultMainModule).isNotEmpty() + ) + } + } + + @Test + fun `07 - (PLUGIN) xml parsing`() { + val temp = tempPath() + prepareTask(temp).runCommand { _, report -> + println(report) + val files = locateResourceFiles("$temp${File.separator}$testProjectName", configuration) + files.forEach { + assert(parseXML(it.file).isNotEmpty()) + } + } + } + + @Test + fun `08 - (PLUGIN) obfuscate string values`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + val key = report.extractFingerprint(variant = "prodDebug", configuration = configuration) + println(key) + assert(key.isNotEmpty()) + val files = locateResourceFiles("$temp${File.separator}$testProjectName", configuration) + files.forEach { file -> + val entities = parseXML(file.file) + entities.forEach { entity -> + val obfuscated = obfuscateStringEntity( + key, + entity, + configuration.applicationId + ) + assert(obfuscated.value != entity.value) + } + } + } + } + + @Test + fun `09 - (PLUGIN) obfuscate and reveal string values`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + val key = report.extractFingerprint(variant = "prodDebug", configuration = configuration) + println(key) + assert(key.isNotEmpty()) + val files = locateResourceFiles("$temp${File.separator}$testProjectName", configuration) + files.forEach { file -> + val entities = parseXML(file.file) + entities.forEach { entity -> + val obfuscated = obfuscateStringEntity( + key, + entity, + configuration.applicationId + ) + assert(obfuscated.value != entity.value) + + val original = revealStringEntity( + key, + obfuscated, + configuration.applicationId + ) + + assert( + original.value == when (entity.androidTreatment) { + true -> entity.value.androidTreatment() + else -> entity.value + } + ) + + } + } + } + } + + @Test + fun `10 - (PLUGIN) obfuscate xml`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + val files = locateResourceFiles("$temp${File.separator}$testProjectName", configuration) + files.forEach { file -> + val entities = parseXML(file.file) + assert(entities.isNotEmpty()) + modifyXML(file.file, report.extractFingerprint(configuration = configuration), configuration) + } + val filesObfuscated = locateResourceFiles("$temp${File.separator}$testProjectName", configuration) + filesObfuscated.forEach { file -> + val entities = parseXML(file.file) + assert(entities.isEmpty()) + } + } + } + + @Test + fun `11 - (PLUGIN) obfuscate, restore and compare xml values with originals`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + val files = backupResourceFiles("$temp${File.separator}$testProjectName", configuration) + assert(files.isNotEmpty()) + files.forEach { file -> + val entities = parseXML(file.file) + assert(entities.isNotEmpty()) + modifyXML(file.file, report.extractFingerprint(configuration = configuration), configuration) + } + val filesObfuscated = locateResourceFiles("$temp${File.separator}$testProjectName", configuration) + filesObfuscated.forEach { file -> + val entities = parseXML(file.file) + assert(entities.isEmpty()) + } + + val restoredFiles = restoreResourceFiles("$temp${File.separator}$testProjectName", defaultMainModule) + assert(restoredFiles.isNotEmpty()) + + val originalEntities = mutableListOf() + files.forEach { file -> + originalEntities.addAll(parseXML(file.file)) + } + assert(originalEntities.isNotEmpty()) + + val restoredEntities = mutableListOf() + restoredFiles.forEach { file -> + restoredEntities.addAll(parseXML(file)) + } + assert(restoredEntities.isNotEmpty()) + + originalEntities.forEach { entity -> + val eq = restoredEntities.find { + it.name == entity.name + } + eq?.let { + assert(entity.name == it.name) + assert(entity.value == it.value) + } + } + } + } + + @Test + fun `12 - (ANDROID COMPILATION) obfuscate xml and build (not real workflow)`() { + val temp = tempPath() + signingReportTask(temp).runCommand { _, report -> + println(report) + val files = locateResourceFiles("$temp${File.separator}$testProjectName", configuration) + files.forEach { file -> + val entities = parseXML(file.file) + assert(entities.isNotEmpty()) + modifyXML( + file.file, + report.extractFingerprint(configuration = configuration), + configuration + ) + } + val filesObfuscated = locateResourceFiles( + "$temp${File.separator}$testProjectName", + configuration + ) + filesObfuscated.forEach { file -> + val entities = parseXML(file.file) + assert(entities.isEmpty()) + } + } + } + + @Test + fun `13 - (PLUGIN COMPILATION) plugin with no test`() { + pluginBuildTask().runCommand { _, report -> + assert(report.contains("BUILD SUCCESSFUL")) + println(report) + } + } + + @Test + fun `14 - (ANDROID COMPILATION) plugin running on Android`() { + pluginBuildTask().runCommand { _, report -> + assert(report.contains("BUILD SUCCESSFUL")) + } + val temp = tempPath() + prepareTask(temp).runCommand { _, _ -> + modifyForTest(temp, testProjectName) + buildTask("$temp${File.separator}$testProjectName").runCommand { _, androidReport -> + assert(androidReport.contains("BUILD SUCCESSFUL")) + println(androidReport) + } + } + } + + @Test + fun `15 - (GRADLE TASK) stringcarePreview`() { + pluginBuildTask().runCommand { _, report -> + assert(report.contains("BUILD SUCCESSFUL")) + } + val temp = tempPath() + prepareTask(temp).runCommand { _, _ -> + modifyForTest(temp, testProjectName) + basicGradleTask("$temp${File.separator}$testProjectName").runCommand { _, androidReport -> + assert(androidReport.contains("END REPORT")) + println(androidReport) + } + + } + } + + @Test + fun `16 - (GRADLE TASK) stringcareTestObfuscate`() { + pluginBuildTask().runCommand { _, report -> + assert(report.contains("BUILD SUCCESSFUL")) + } + val temp = tempPath() + prepareTask(temp).runCommand { _, _ -> + modifyForTest(temp, testProjectName) + obfuscationTestGradleTask("$temp${File.separator}$testProjectName").runCommand { _, androidReport -> + assert(androidReport.contains("END OBFUSCATION")) + println(androidReport) + } + + } + } + +} \ No newline at end of file diff --git a/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/XmlProcessorTest.kt b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/XmlProcessorTest.kt new file mode 100644 index 0000000..90f975e --- /dev/null +++ b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/XmlProcessorTest.kt @@ -0,0 +1,52 @@ +package dev.vyp.stringcare.plugin + +import dev.vyp.stringcare.plugin.internal.parseXML +import dev.vyp.stringcare.plugin.models.StringEntity +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.io.File + +class XmlProcessorTest { + + @Test + fun `parseXML extracts StringEntity with hidden true`() { + val xml = """ + + + Hello + + + """.trimIndent() + val file = File.createTempFile("strings", ".xml") + file.writeText(xml) + try { + val entities = parseXML(file) + assertEquals(1, entities.size) + val entity = entities[0] + assertEquals("secret", entity.name) + assertEquals("sensitive", entity.value) + assertTrue(entity.androidTreatment) + } finally { + file.delete() + } + } + + @Test + fun `parseXML returns empty for no hidden strings`() { + val xml = """ + + + Hello + + """.trimIndent() + val file = File.createTempFile("strings", ".xml") + file.writeText(xml) + try { + val entities = parseXML(file) + assertTrue(entities.isEmpty()) + } finally { + file.delete() + } + } +} diff --git a/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/utils/Helper.kt b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/utils/Helper.kt new file mode 100644 index 0000000..59a62dd --- /dev/null +++ b/plugin/src/test/kotlin/dev/vyp/stringcare/plugin/utils/Helper.kt @@ -0,0 +1,22 @@ +package dev.vyp.stringcare.plugin.utils + +import dev.vyp.stringcare.plugin.internal.Os +import dev.vyp.stringcare.plugin.internal.getContent +import dev.vyp.stringcare.plugin.internal.getOs +import dev.vyp.stringcare.plugin.internal.version +import java.io.File +import java.io.FileWriter + +fun modifyForTest(directory: String, projectPath: String) { + val current = File(".") + val file = File("$directory${File.separator}$projectPath${File.separator}build.gradle") + val content = file.getContent().replace( + "classpath \"io.github.stringcare:plugin:\$stringcare_version\"", + when (getOs()) { + Os.WINDOWS -> "\nclasspath files(\"${current.absolutePath.replace("\\", "\\\\")}${File.separator}${File.separator}build${File.separator}${File.separator}libs${File.separator}${File.separator}plugin-$version.jar\")\n" + Os.OSX, Os.LINUX -> "\nclasspath files(\"${current.absolutePath}${File.separator}build${File.separator}libs${File.separator}plugin-$version.jar\")\n" + } + ) + FileWriter(file.absolutePath).use { it.write(content) } + +} \ No newline at end of file diff --git a/plugin/src/test/resources/junit-platform.properties b/plugin/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..e6d55f8 --- /dev/null +++ b/plugin/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100755 index 85b59a4..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app', ':library' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..44e15a2 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + includeBuild("plugin") +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "stringcare-android" +include(":app", ":library")