├── .dockerignore ├── .gitattributes ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feat.yml ├── renovate.json └── workflows │ ├── build.yml │ ├── release.yml │ ├── update-graphql-schema.yml │ └── update-inno-dependencies.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.md ├── README.md ├── assets ├── CodeDependencies.iss ├── banner.svg ├── demo.gif ├── github.graphql ├── gpl-3.0.rst ├── installer.iss ├── logo.ico └── logo.svg ├── build.rs ├── rustfmt.toml └── src ├── commands ├── analyse.rs ├── cleanup.rs ├── complete.rs ├── list_versions.rs ├── mod.rs ├── new_version.rs ├── remove_dead_versions.rs ├── remove_version.rs ├── show_version.rs ├── submit.rs ├── sync_fork.rs ├── token │ ├── commands.rs │ ├── mod.rs │ ├── remove.rs │ └── update.rs ├── update_version.rs └── utils │ └── mod.rs ├── credential.rs ├── download ├── downloader.rs ├── file.rs └── mod.rs ├── download_file.rs ├── editor.rs ├── file_analyser.rs ├── github ├── github_client.rs ├── graphql │ ├── create_commit.rs │ ├── create_pull_request.rs │ ├── create_ref.rs │ ├── get_all_values.rs │ ├── get_branches.rs │ ├── get_current_user_login.rs │ ├── get_directory_content.rs │ ├── get_directory_content_with_text.rs │ ├── get_existing_pull_request.rs │ ├── get_file_content.rs │ ├── get_repository_info.rs │ ├── github_schema.rs │ ├── merge_upstream.rs │ ├── mod.rs │ ├── types.rs │ └── update_refs.rs ├── mod.rs ├── rest │ ├── get_tree.rs │ └── mod.rs └── utils │ ├── mod.rs │ ├── package_path.rs │ └── pull_request.rs ├── installers ├── burn │ ├── manifest.rs │ ├── mod.rs │ └── wix_burn_stub.rs ├── inno │ ├── compression.rs │ ├── encoding.rs │ ├── entry │ │ ├── component.rs │ │ ├── condition.rs │ │ ├── directory.rs │ │ ├── file.rs │ │ ├── icon.rs │ │ ├── ini.rs │ │ ├── language.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── permission.rs │ │ ├── registry.rs │ │ ├── task.rs │ │ └── type.rs │ ├── enum_value.rs │ ├── flag_reader.rs │ ├── header │ │ ├── architecture.rs │ │ ├── enums.rs │ │ ├── flags.rs │ │ └── mod.rs │ ├── loader.rs │ ├── mod.rs │ ├── read │ │ ├── block.rs │ │ ├── chunk.rs │ │ ├── crc32.rs │ │ ├── decoder.rs │ │ └── mod.rs │ ├── version.rs │ ├── windows_version.rs │ └── wizard.rs ├── mod.rs ├── msi │ └── mod.rs ├── msix_family │ ├── bundle.rs │ ├── mod.rs │ └── utils.rs ├── nsis │ ├── entry │ │ ├── creation_disposition.rs │ │ ├── exec_flag.rs │ │ ├── generic_access_rights.rs │ │ ├── mod.rs │ │ ├── push_pop.rs │ │ ├── seek_from.rs │ │ ├── show_window.rs │ │ └── window_message.rs │ ├── file_system │ │ ├── item.rs │ │ └── mod.rs │ ├── first_header │ │ ├── flags.rs │ │ ├── mod.rs │ │ └── signature.rs │ ├── header │ │ ├── block.rs │ │ ├── compression.rs │ │ ├── decoder.rs │ │ ├── flags.rs │ │ └── mod.rs │ ├── language │ │ ├── mod.rs │ │ └── table.rs │ ├── mod.rs │ ├── registry.rs │ ├── section │ │ ├── flags.rs │ │ └── mod.rs │ ├── state.rs │ ├── strings │ │ ├── code.rs │ │ ├── mod.rs │ │ ├── predefined.rs │ │ ├── shell.rs │ │ └── var.rs │ └── version.rs ├── possible_installers.rs ├── utils │ ├── lzma_stream_header.rs │ ├── mod.rs │ └── registry.rs └── zip.rs ├── main.rs ├── manifests ├── manifest.rs ├── mod.rs └── url.rs ├── match_installers.rs ├── prompts ├── list.rs ├── mod.rs └── text.rs ├── terminal ├── hyperlink.rs └── mod.rs ├── traits ├── mod.rs ├── name.rs └── path.rs └── update_state.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | .gitattributes 3 | .gitigore 4 | *.md -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @russellbanks 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: russellbanks 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 'Bug Report' 2 | description: File a bug report 3 | title: '[Bug]: ' 4 | labels: [bug, help wanted] 5 | assignees: russellbanks 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: checkboxes 12 | attributes: 13 | label: Is there an existing issue for this? 14 | description: Please search to see if an issue already exists for the bug you noticed. 15 | options: 16 | - label: I have searched the existing issues 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: What happened? 21 | description: Also tell us, what did you expect to happen? 22 | placeholder: Tell us what you see! 23 | validations: 24 | required: true 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Review open issues 4 | url: https://github.com/russellbanks/Komac/issues 5 | about: Please ensure you have gone through open issues. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feat.yml: -------------------------------------------------------------------------------- 1 | name: 'Feature Request' 2 | description: Request a new feature 3 | title: '[Feature/Idea]: ' 4 | labels: [feat] 5 | assignees: russellbanks 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to request a new feature! 11 | - type: textarea 12 | attributes: 13 | label: What would you like to see changed/added? 14 | description: Try to give some examples to make it really clear! 15 | placeholder: Tell us what you would like to see! Something new and amazing! 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":maintainLockFilesWeekly", 6 | ":semanticCommitsDisabled" 7 | ], 8 | "rangeStrategy": "bump" 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/workflows/build.yml 7 | - .github/workflows/release.yml 8 | - assets/** 9 | - src/** 10 | - Cargo.toml 11 | - Cargo.lock 12 | - build.rs 13 | pull_request: 14 | paths: 15 | - .github/workflows/build.yml 16 | - .github/workflows/release.yml 17 | - assets/** 18 | - src/** 19 | - Cargo.toml 20 | - Cargo.lock 21 | - build.rs 22 | 23 | env: 24 | CARGO_TERM_COLOR: always 25 | 26 | jobs: 27 | build_and_test: 28 | name: Rust project - latest 29 | strategy: 30 | matrix: 31 | os: [ ubuntu-latest, windows-latest, macos-latest ] 32 | runs-on: ${{ matrix.os }} 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Install Rust 39 | uses: moonrepo/setup-rust@v1 40 | with: 41 | cache: false 42 | 43 | - name: Check 44 | run: cargo check 45 | 46 | - name: Test 47 | run: cargo test 48 | 49 | - name: Publish dry run 50 | run: cargo publish --dry-run 51 | 52 | msrv: 53 | name: MSRV 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout repository 57 | uses: actions/checkout@v4 58 | 59 | - name: Install Rust 60 | uses: moonrepo/setup-rust@v1 61 | with: 62 | bins: cargo-msrv 63 | 64 | - name: Verify MSRV 65 | id: verify 66 | run: cargo msrv verify 67 | 68 | - name: Find actual MSRV 69 | if: steps.verify.outcome == 'failure' 70 | run: cargo msrv find 71 | -------------------------------------------------------------------------------- /.github/workflows/update-graphql-schema.yml: -------------------------------------------------------------------------------- 1 | name: Update local copy of GraphQL schema 2 | 3 | on: 4 | schedule: 5 | - cron: '0 9 * * *' 6 | 7 | jobs: 8 | update-schema: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Download latest GitHub GraphQL Schema 19 | run: curl -L https://docs.github.com/public/fpt/schema.docs.graphql -o assets/github.graphql 20 | 21 | - name: Create Pull Request 22 | uses: peter-evans/create-pull-request@v7 23 | with: 24 | commit-message: "Update GitHub GraphQL Schema" 25 | branch: update-github-graphql-schema 26 | title: "Update GitHub GraphQL Schema" 27 | body: "This is an automated pull request to update the local GitHub GraphQL schema" 28 | -------------------------------------------------------------------------------- /.github/workflows/update-inno-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Update local copy of Inno Setup Dependency Installer script 2 | 3 | on: 4 | schedule: 5 | - cron: '0 9 1 * *' 6 | 7 | jobs: 8 | update-schema: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Download latest Inno Setup Dependency Installer script 19 | run: curl -L https://github.com/DomGries/InnoDependencyInstaller/raw/HEAD/CodeDependencies.iss -o assets/CodeDependencies.iss 20 | 21 | - name: Create Pull Request 22 | uses: peter-evans/create-pull-request@v7 23 | with: 24 | commit-message: "Update Inno Setup Dependency Installer script" 25 | branch: update-inno-dependencies-script 26 | title: "Update Inno Setup Dependency Installer script" 27 | body: "This is an automated pull request to update the Inno Setup Dependency Installer script from [DomGries/InnoDependencyInstaller](https://github.com/DomGries/InnoDependencyInstaller)" 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 4 | 5 | * If you have suggestions for adding or removing projects, feel free to [open an issue](https://github.com/russellbanks/Komac/issues/new) to discuss it, or directly create a pull request after you edit the *README.md* file with necessary changes. 6 | * Please make sure you check your spelling and grammar. 7 | * Create individual PR for each suggestion. 8 | * Please also read through the [Code of Conduct](./CODE_OF_CONDUCT.md) before posting your first idea as well. 9 | 10 | ### Creating a Pull Request 11 | 12 | 1. Fork the Project 13 | 2. Create your Feature Branch (`git checkout -b feat/new-feature`) 14 | 3. Commit your Changes (`git commit -m 'Add some feature'`) 15 | 4. Push to the Branch (`git push origin feat/new-feature`) 16 | 5. Open a Pull Request 17 | 18 | ### Testing your changes 19 | 20 | Using Docker is the easiest way to to test your code before submitting a pull request. 21 | 22 | > [!NOTE] 23 | > When using the Docker container on Windows, the WSL engine does not support the default collection for keys or tokens. This means that when testing inside the container GitHub tokens will not be stored, even when `komac token update` is used. 24 | > 25 | > This is a [known issue](https://github.com/hwchen/keyring-rs/blob/47c8daf3e6178a2282ae3e8670d1ea7fa736b8cb/src/secret_service.rs#L73-L77) which is documented in the keyring crate. 26 | > 27 | > As a workaround, you can set the `GITHUB_TOKEN` environment variable from within the container, in the `docker run` command, or in the Dockerfile itself 28 | 29 | 1. Ensure you have docker installed and the docker engine is running. 30 | 2. Run `docker build ./ --tag komac_dev:latest`. 31 | 3. Wait for the build to complete. 32 | 4. Start the container using `docker run -it komac_dev bash`. 33 | 5. Test out any commands. Use the `exit` command to quit the container -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "komac" 3 | version = "2.12.0" 4 | authors = ["Russell Banks"] 5 | edition = "2024" 6 | rust-version = "1.85" 7 | description = "A manifest creator for winget-pkgs" 8 | license = "GPL-3.0-or-later" 9 | repository = "https://github.com/russellbanks/Komac" 10 | readme = "README.md" 11 | documentation = "https://github.com/russellbanks/Komac/blob/main/README.md" 12 | categories = ["command-line-utilities", "development-tools"] 13 | keywords = ["winget", "winget-pkgs", "winget-cli", "windows"] 14 | build = "build.rs" 15 | include = ["**/*.rs", "assets/github.graphql", "assets/logo.ico"] 16 | 17 | [[bin]] 18 | name = "komac" 19 | path = "src/main.rs" 20 | 21 | [profile.release] 22 | codegen-units = 1 23 | lto = true 24 | strip = true 25 | 26 | [dependencies] 27 | anstream = "0.6.18" 28 | base64ct = { version = "1.8.0", features = ["std"] } 29 | bit-set = "0.8.0" 30 | bitflags = "2.9.1" 31 | bon = "3.6.3" 32 | byteorder = "1.5.0" 33 | bytes = "1.10.1" 34 | bzip2 = "0.5.2" 35 | cab = "0.6.0" 36 | camino = { version = "1.1.10", features = ["serde1"] } 37 | chrono = { version = "0.4.41", features = ["serde"] } 38 | clap = { version = "4.5.39", features = ["derive", "cargo", "env"] } 39 | clap_complete = "4.5.52" 40 | codepage = "0.1.2" 41 | color-eyre = { version = "0.6.5", default-features = false } 42 | compact_str = "0.9.0" 43 | const_format = { version = "0.2.34", features = ["derive"] } 44 | crc32fast = "1.4.2" 45 | crossbeam-channel = "0.5.15" 46 | crossterm = "0.29.0" 47 | cynic = { version = "3.11.0", features = ["http-reqwest"] } 48 | derive-new = "0.7.0" 49 | derive_more = { version = "2.0.1", features = ["as_ref", "debug", "deref", "deref_mut", "display", "from_str", "into", "into_iterator"] } 50 | encoding_rs = "0.8.35" 51 | flate2 = "1.1.1" 52 | futures = "0.3.31" 53 | futures-util = "0.3.31" 54 | html2text = "0.15.1" 55 | indexmap = "2.9.0" 56 | indextree = "4.7.4" 57 | indicatif = "0.17.11" 58 | inquire = "0.7.5" 59 | itertools = "0.14.0" 60 | keyring = { version = "3.6.2", features = ["apple-native", "crypto-openssl", "sync-secret-service", "vendored", "windows-native"] } 61 | liblzma = { version = "0.4.1", features = ["static"] } 62 | memchr = "2.7.4" 63 | memmap2 = "0.9.5" 64 | msi = "0.8.0" 65 | nt-time = { version = "0.11.1", features = ["chrono"] } 66 | num_cpus = "1.17.0" 67 | open = "5.3.2" 68 | ordinal = "0.4.0" 69 | owo-colors = "4.2.1" 70 | protobuf = "3.7.2" 71 | quick-xml = { version = "0.37.5", features = ["serialize"] } 72 | rand = "0.9.1" 73 | ratatui = "0.29.0" 74 | regex = "1.11.1" 75 | reqwest = { version = "0.12.19", features = ["native-tls-vendored", "stream"] } 76 | serde = { version = "1.0.219", features = ["derive"] } 77 | serde_json = "1.0.140" 78 | serde_yaml = "0.9.34" 79 | sha2 = "0.10.9" 80 | strsim = "0.11.1" 81 | strum = { version = "0.27.1", features = ["derive"] } 82 | supports-hyperlinks = "3.1.0" 83 | tempfile = "3.20.0" 84 | thiserror = "2.0.12" 85 | tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros", "fs", "parking_lot"] } 86 | tracing = { version = "0.1.41", features = ["release_max_level_warn"] } 87 | tracing-indicatif = "0.3.9" 88 | tracing-subscriber = "0.3.19" 89 | tree-sitter-highlight = "0.25.6" 90 | tree-sitter-yaml = "0.7.1" 91 | tui-textarea = { version = "0.7.0", features = ["search"] } 92 | url = { version = "2.5.4", features = ["serde"] } 93 | uuid = { version = "1.17.0", features = ["v4"] } 94 | walkdir = "2.5.0" 95 | winget-types = { version = "0.3.0", features = ["serde", "std", "chrono"] } 96 | yara-x = { version = "0.15.0", default-features = false, features = ["pe-module"] } 97 | zerocopy = { version = "0.8.25", features = ["derive", "std"] } 98 | zip = { version = "4.0.0", default-features = false, features = ["deflate"] } 99 | 100 | [build-dependencies] 101 | cynic-codegen = { version = "3.11.0", features = ["rkyv"] } 102 | windows_exe_info = { version = "0.5.2", features = ["manifest"] } 103 | 104 | [dev-dependencies] 105 | indoc = "2.0.6" 106 | rstest = "0.25.0" 107 | 108 | [package.metadata.generate-rpm] 109 | assets = [ 110 | { source = "target/release/komac", dest = "/usr/bin/komac", mode = "755" }, 111 | ] 112 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim as build 2 | 3 | # Copy source code into the build container 4 | WORKDIR /usr/src 5 | COPY ./ /usr/src 6 | 7 | # Install apt packages required for building the package dependencies 8 | RUN apt-get update \ 9 | && apt-get install -y --no-install-recommends --no-install-suggests \ 10 | libssl-dev \ 11 | perl \ 12 | make 13 | 14 | # Build Komac from the source code 15 | RUN cargo build --release 16 | 17 | # Create a new container for 18 | FROM debian:bookworm-slim as release 19 | RUN apt-get update \ 20 | && apt-get install -y --no-install-recommends --no-install-suggests \ 21 | ca-certificates \ 22 | && rm -rf \ 23 | /var/lib/apt/lists/* \ 24 | /tmp/* \ 25 | /var/tmp/* 26 | 27 | COPY --from=build /usr/src/target/release/komac /usr/local/bin/ 28 | WORKDIR /root -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbanks/Komac/5e13baafd65c5985d3603d6201c8a872d38d0610/assets/demo.gif -------------------------------------------------------------------------------- /assets/installer.iss: -------------------------------------------------------------------------------- 1 | #define AppName "Komac" 2 | #define Version GetFileProductVersion(InputExecutable) 3 | #define Publisher "Russell Banks" 4 | #define URL "https://github.com/russellbanks/Komac" 5 | #define ExeName GetFileOriginalFilename(InputExecutable) 6 | 7 | #if Pos("x64", Architecture) > 0 8 | #define ArchAllowed "x64compatible and not arm64" 9 | #else 10 | #define ArchAllowed Architecture 11 | #endif 12 | 13 | #include "CodeDependencies.iss" 14 | 15 | [Setup] 16 | AppId={{776938BF-CF8E-488B-A3DF-8048BC64F2CD} 17 | AppName={#AppName} 18 | AppVersion={#Version} 19 | AppPublisher={#Publisher} 20 | AppPublisherURL={#URL} 21 | AppSupportURL={#URL} 22 | AppUpdatesURL={#URL} 23 | DefaultDirName={autopf}\{#AppName} 24 | DisableDirPage=yes 25 | DefaultGroupName={#AppName} 26 | DisableProgramGroupPage=yes 27 | LicenseFile=gpl-3.0.rst 28 | PrivilegesRequired=lowest 29 | PrivilegesRequiredOverridesAllowed=dialog 30 | OutputBaseFilename={#AppName}Setup-{#Version}-{#Architecture} 31 | SetupIconFile=logo.ico 32 | UninstallDisplayName={#AppName} ({#Architecture}) 33 | WizardStyle=modern 34 | ChangesEnvironment=yes 35 | ArchitecturesAllowed={#ArchAllowed} 36 | ArchitecturesInstallIn64BitMode={#ArchAllowed} 37 | 38 | [Languages] 39 | Name: "english"; MessagesFile: "compiler:Default.isl" 40 | 41 | [Files] 42 | Source: "{#InputExecutable}"; DestDir: "{app}\bin"; DestName: "{#ExeName}" 43 | 44 | [Code] 45 | function InitializeSetup: Boolean; 46 | begin 47 | Dependency_AddVC2015To2022; 48 | Result := True; 49 | end; 50 | 51 | procedure EnvAddPath(Path: string); 52 | var 53 | Paths: string; 54 | RootKey: Integer; 55 | EnvironmentKey: string; 56 | begin 57 | if IsAdminInstallMode() then 58 | begin 59 | EnvironmentKey := 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; 60 | RootKey := HKEY_LOCAL_MACHINE; 61 | end 62 | else 63 | begin 64 | EnvironmentKey := 'Environment'; 65 | RootKey := HKEY_CURRENT_USER; 66 | end; 67 | 68 | { Retrieve current path (use empty string if entry not exists) } 69 | if not RegQueryStringValue(RootKey, EnvironmentKey, 'Path', Paths) 70 | then Paths := ''; 71 | 72 | { Skip if string already found in path } 73 | if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit; 74 | 75 | { App string to the end of the path variable } 76 | Paths := Paths + ';'+ Path +';' 77 | 78 | { Overwrite (or create if missing) path environment variable } 79 | if RegWriteStringValue(RootKey, EnvironmentKey, 'Path', Paths) 80 | then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths])) 81 | else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths])); 82 | end; 83 | 84 | 85 | procedure EnvRemovePath(Path: string); 86 | var 87 | Paths: string; 88 | P: Integer; 89 | RootKey: Integer; 90 | EnvironmentKey: string; 91 | begin 92 | if Pos(ExpandConstant('{commonpf}'), ExpandConstant('{app}')) = 1 then 93 | begin 94 | EnvironmentKey := 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; 95 | RootKey := HKEY_LOCAL_MACHINE; 96 | end 97 | else 98 | begin 99 | EnvironmentKey := 'Environment'; 100 | RootKey := HKEY_CURRENT_USER; 101 | end; 102 | 103 | { Skip if registry entry not exists } 104 | if not RegQueryStringValue(RootKey, EnvironmentKey, 'Path', Paths) then 105 | exit; 106 | 107 | { Skip if string not found in path } 108 | P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); 109 | if P = 0 then exit; 110 | 111 | { Update path variable } 112 | Delete(Paths, P - 1, Length(Path) + 1); 113 | 114 | { Overwrite path environment variable } 115 | if RegWriteStringValue(RootKey, EnvironmentKey, 'Path', Paths) 116 | then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths])) 117 | else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths])); 118 | end; 119 | 120 | procedure CurStepChanged(CurStep: TSetupStep); 121 | begin 122 | if CurStep = ssPostInstall 123 | then EnvAddPath(ExpandConstant('{app}') +'\bin'); 124 | end; 125 | 126 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 127 | begin 128 | if CurUninstallStep = usPostUninstall 129 | then EnvRemovePath(ExpandConstant('{app}') +'\bin'); 130 | end; 131 | -------------------------------------------------------------------------------- /assets/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbanks/Komac/5e13baafd65c5985d3603d6201c8a872d38d0610/assets/logo.ico -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate windows_exe_info; 2 | 3 | use cynic_codegen::registration::SchemaRegistration; 4 | 5 | fn main() { 6 | cynic_codegen::register_schema("github") 7 | .from_sdl_file("assets/github.graphql") 8 | .and_then(SchemaRegistration::as_default) 9 | .unwrap(); 10 | windows_exe_info::icon::icon_ico("assets/logo.ico"); 11 | windows_exe_info::versioninfo::link_cargo_env(); 12 | } 13 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Native" 2 | imports_granularity = "Crate" 3 | group_imports = "StdExternalCrate" 4 | unstable_features = true 5 | -------------------------------------------------------------------------------- /src/commands/analyse.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use anstream::stdout; 4 | use camino::{Utf8Path, Utf8PathBuf}; 5 | use clap::Parser; 6 | use color_eyre::{Result, eyre::ensure}; 7 | use memmap2::Mmap; 8 | use sha2::{Digest, Sha256}; 9 | use winget_types::Sha256String; 10 | 11 | use crate::{file_analyser::FileAnalyser, manifests::print_manifest}; 12 | 13 | /// Analyses a file and outputs information about it 14 | #[derive(Parser)] 15 | pub struct Analyse { 16 | #[arg(value_parser = is_valid_file, value_hint = clap::ValueHint::FilePath)] 17 | file_path: Utf8PathBuf, 18 | 19 | #[cfg(not(debug_assertions))] 20 | /// Hash the file and include it in the `InstallerSha256` field 21 | #[arg(long = "hash", alias = "sha256", overrides_with = "hash")] 22 | _no_hash: bool, 23 | 24 | #[cfg(not(debug_assertions))] 25 | /// Skip hashing the file 26 | #[arg(long = "no-hash", alias = "no-sha256", action = clap::ArgAction::SetFalse)] 27 | hash: bool, 28 | 29 | #[cfg(debug_assertions)] 30 | /// Hash the file and include it in the `InstallerSha256` field 31 | #[arg(long, alias = "sha256", overrides_with = "_no_hash")] 32 | hash: bool, 33 | 34 | #[cfg(debug_assertions)] 35 | /// Skip hashing the file 36 | #[arg(long = "no-hash", alias = "no-sha256")] 37 | _no_hash: bool, 38 | } 39 | 40 | impl Analyse { 41 | pub fn run(self) -> Result<()> { 42 | let file = File::open(&self.file_path)?; 43 | let mmap = unsafe { Mmap::map(&file) }?; 44 | let file_name = self 45 | .file_path 46 | .file_name() 47 | .unwrap_or_else(|| self.file_path.as_str()); 48 | let mut analyser = FileAnalyser::new(&mmap, file_name)?; 49 | if self.hash { 50 | let sha_256 = Sha256String::from_digest(&Sha256::digest(&mmap)); 51 | for installer in &mut analyser.installers { 52 | installer.sha_256 = sha_256.clone(); 53 | } 54 | } 55 | let yaml = match analyser.installers.as_slice() { 56 | [installer] => serde_yaml::to_string(installer)?, 57 | installers => serde_yaml::to_string(installers)?, 58 | }; 59 | let mut lock = stdout().lock(); 60 | print_manifest(&mut lock, &yaml); 61 | Ok(()) 62 | } 63 | } 64 | 65 | fn is_valid_file(path: &str) -> Result { 66 | let path = Utf8Path::new(path); 67 | ensure!(path.exists(), "{path} does not exist"); 68 | ensure!(path.is_file(), "{path} is not a file"); 69 | Ok(path.to_path_buf()) 70 | } 71 | -------------------------------------------------------------------------------- /src/commands/cleanup.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use anstream::println; 4 | use bitflags::bitflags; 5 | use clap::Parser; 6 | use color_eyre::Result; 7 | use futures_util::TryFutureExt; 8 | use indicatif::ProgressBar; 9 | use inquire::MultiSelect; 10 | use owo_colors::OwoColorize; 11 | 12 | use crate::{ 13 | commands::utils::SPINNER_TICK_RATE, credential::handle_token, github::github_client::GitHub, 14 | prompts::handle_inquire_error, 15 | }; 16 | 17 | /// Finds branches from the fork of winget-pkgs that have had a merged or closed pull request to 18 | /// microsoft/winget-pkgs from them, prompting for which ones to delete 19 | #[derive(Parser)] 20 | #[clap(visible_alias = "clean")] 21 | pub struct Cleanup { 22 | /// Only delete merged branches 23 | #[arg(long)] 24 | only_merged: bool, 25 | 26 | /// Only delete closed branches 27 | #[arg(long)] 28 | only_closed: bool, 29 | 30 | /// Automatically delete all relevant branches 31 | #[arg(short, long, env = "CI")] 32 | all: bool, 33 | 34 | /// GitHub personal access token with the `public_repo` scope 35 | #[arg(short, long, env = "GITHUB_TOKEN")] 36 | token: Option, 37 | } 38 | 39 | impl Cleanup { 40 | pub async fn run(self) -> Result<()> { 41 | let token = handle_token(self.token.as_deref()).await?; 42 | let github = GitHub::new(&token)?; 43 | 44 | let merge_state = MergeState::from((self.only_merged, self.only_closed)); 45 | 46 | let pb = ProgressBar::new_spinner().with_message(format!( 47 | "Retrieving branches that have a {merge_state} pull request associated with them" 48 | )); 49 | pb.enable_steady_tick(SPINNER_TICK_RATE); 50 | 51 | // Get all fork branches with an associated pull request to microsoft/winget-pkgs 52 | let (pr_branch_map, repository_id) = github 53 | .get_username() 54 | .and_then(|username| github.get_branches(username, merge_state)) 55 | .await?; 56 | 57 | pb.finish_and_clear(); 58 | 59 | // Exit if there are no branches to delete 60 | if pr_branch_map.is_empty() { 61 | println!( 62 | "There are no {} pull requests with branches that can be deleted", 63 | merge_state.blue() 64 | ); 65 | return Ok(()); 66 | } 67 | 68 | let chosen_pr_branches = if self.all { 69 | pr_branch_map.keys().collect() 70 | } else { 71 | // Show a multi-selection prompt for which branches to delete, with all options pre-selected 72 | MultiSelect::new( 73 | "Please select branches to delete", 74 | pr_branch_map.keys().collect(), 75 | ) 76 | .with_all_selected_by_default() 77 | .with_page_size(10) 78 | .prompt() 79 | .map_err(handle_inquire_error)? 80 | }; 81 | 82 | if chosen_pr_branches.is_empty() { 83 | println!("No branches have been deleted"); 84 | return Ok(()); 85 | } 86 | 87 | // Get branch names from chosen pull requests 88 | let branches_to_delete = chosen_pr_branches 89 | .into_iter() 90 | .filter_map(|pull_request| pr_branch_map.get(pull_request).map(String::as_str)) 91 | .collect::>(); 92 | 93 | let branch_label = match branches_to_delete.len() { 94 | 1 => "branch", 95 | _ => "branches", 96 | }; 97 | 98 | pb.reset(); 99 | pb.set_message(format!( 100 | "Deleting {} selected {branch_label}", 101 | branches_to_delete.len(), 102 | )); 103 | pb.enable_steady_tick(SPINNER_TICK_RATE); 104 | 105 | github 106 | .delete_branches(&repository_id, &branches_to_delete) 107 | .await?; 108 | 109 | pb.finish_and_clear(); 110 | 111 | println!( 112 | "{} deleted {} selected {branch_label}", 113 | "Successfully".green(), 114 | branches_to_delete.len().blue(), 115 | ); 116 | 117 | Ok(()) 118 | } 119 | } 120 | 121 | // Using bitflags instead of an enum to allow combining multiple states (MERGED, CLOSED) 122 | bitflags! { 123 | #[derive(Copy, Clone, PartialEq, Eq)] 124 | pub struct MergeState: u8 { 125 | const MERGED = 1 << 0; 126 | const CLOSED = 1 << 1; 127 | } 128 | } 129 | 130 | impl Display for MergeState { 131 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 132 | write!( 133 | f, 134 | "{}", 135 | match *self { 136 | Self::MERGED => "merged", 137 | Self::CLOSED => "closed", 138 | _ => "merged or closed", 139 | } 140 | ) 141 | } 142 | } 143 | 144 | impl From<(bool, bool)> for MergeState { 145 | fn from((only_merged, only_closed): (bool, bool)) -> Self { 146 | match (only_merged, only_closed) { 147 | (true, false) => Self::MERGED, 148 | (false, true) => Self::CLOSED, 149 | _ => Self::all(), 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/commands/complete.rs: -------------------------------------------------------------------------------- 1 | use anstream::stdout; 2 | use clap::{CommandFactory, Parser}; 3 | use clap_complete::{Shell, generate}; 4 | use color_eyre::{Result, Section, eyre::eyre}; 5 | 6 | use crate::Cli; 7 | 8 | /// Outputs an autocompletion script for the given shell. Example usage: 9 | /// 10 | /// Bash: echo "source <(komac complete bash)" >> ~/.bashrc 11 | /// Elvish: echo "eval (komac complete elvish | slurp)" >> ~/.elvish/rc.elv 12 | /// Fish: echo "source (komac complete fish | psub)" >> ~/.config/fish/config.fish 13 | /// Powershell: echo "komac complete powershell | Out-String | Invoke-Expression" >> $PROFILE 14 | /// Zsh: echo "source <(komac complete zsh)" >> ~/.zshrc 15 | #[derive(Parser)] 16 | #[clap(visible_alias = "autocomplete", verbatim_doc_comment)] 17 | pub struct Complete { 18 | /// Specifies the shell for which to generate the completion script. 19 | /// 20 | /// If not provided, the shell will be inferred based on the current environment. 21 | #[arg()] 22 | shell: Option, 23 | } 24 | 25 | impl Complete { 26 | pub fn run(self) -> Result<()> { 27 | let Some(shell) = self.shell.or_else(Shell::from_env) else { 28 | return Err( 29 | eyre!("Unable to determine the current shell from the environment") 30 | .suggestion("Specify shell explicitly"), 31 | ); 32 | }; 33 | 34 | let mut command = Cli::command(); 35 | let command_name = command.get_name().to_owned(); 36 | generate(shell, &mut command, command_name, &mut stdout()); 37 | 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/list_versions.rs: -------------------------------------------------------------------------------- 1 | use std::{io, io::Write}; 2 | 3 | use clap::{Args, Parser}; 4 | use color_eyre::Result; 5 | use winget_types::PackageIdentifier; 6 | 7 | use crate::{credential::handle_token, github::github_client::GitHub}; 8 | 9 | /// Lists all versions for a given package 10 | #[derive(Parser)] 11 | #[clap(visible_alias = "list")] 12 | pub struct ListVersions { 13 | #[arg()] 14 | package_identifier: PackageIdentifier, 15 | 16 | #[command(flatten)] 17 | output_type: OutputType, 18 | 19 | /// GitHub personal access token with the `public_repo` scope 20 | #[arg(short, long, env = "GITHUB_TOKEN")] 21 | token: Option, 22 | } 23 | 24 | #[derive(Args)] 25 | #[group(multiple = false)] 26 | struct OutputType { 27 | /// Output the versions as JSON 28 | #[arg(long)] 29 | json: bool, 30 | 31 | /// Output the versions as prettified JSON 32 | #[arg(long)] 33 | pretty_json: bool, 34 | 35 | /// Output the versions as YAML 36 | #[arg(long)] 37 | yaml: bool, 38 | } 39 | 40 | impl ListVersions { 41 | pub async fn run(self) -> Result<()> { 42 | let token = handle_token(self.token.as_deref()).await?; 43 | let github = GitHub::new(&token)?; 44 | 45 | let versions = github.get_versions(&self.package_identifier).await?; 46 | 47 | let mut stdout_lock = io::stdout().lock(); 48 | match ( 49 | self.output_type.json, 50 | self.output_type.pretty_json, 51 | self.output_type.yaml, 52 | ) { 53 | (true, _, _) => serde_json::to_writer(stdout_lock, &versions)?, 54 | (_, true, _) => serde_json::to_writer_pretty(stdout_lock, &versions)?, 55 | (_, _, true) => serde_yaml::to_writer(stdout_lock, &versions)?, 56 | _ => { 57 | for version in versions { 58 | writeln!(stdout_lock, "{version}")?; 59 | } 60 | } 61 | } 62 | 63 | Ok(()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod analyse; 2 | pub mod cleanup; 3 | pub mod complete; 4 | pub mod list_versions; 5 | pub mod new_version; 6 | pub mod remove_dead_versions; 7 | pub mod remove_version; 8 | pub mod show_version; 9 | pub mod submit; 10 | pub mod sync_fork; 11 | pub mod token; 12 | pub mod update_version; 13 | pub mod utils; 14 | -------------------------------------------------------------------------------- /src/commands/remove_version.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU32; 2 | 3 | use anstream::println; 4 | use clap::Parser; 5 | use color_eyre::eyre::{Result, bail}; 6 | use futures_util::TryFutureExt; 7 | use inquire::{ 8 | Text, 9 | validator::{MaxLengthValidator, MinLengthValidator}, 10 | }; 11 | use owo_colors::OwoColorize; 12 | use tokio::try_join; 13 | use winget_types::{PackageIdentifier, PackageVersion}; 14 | 15 | use crate::{ 16 | credential::handle_token, 17 | github::github_client::{GitHub, WINGET_PKGS_FULL_NAME}, 18 | prompts::{handle_inquire_error, text::confirm_prompt}, 19 | }; 20 | 21 | /// Remove a version from winget-pkgs 22 | /// 23 | /// To remove a package, all versions of that package must be removed 24 | #[derive(Parser)] 25 | pub struct RemoveVersion { 26 | /// The package's unique identifier 27 | #[arg()] 28 | package_identifier: PackageIdentifier, 29 | 30 | /// The package's version 31 | #[arg(short = 'v', long = "version")] 32 | package_version: PackageVersion, 33 | 34 | #[arg(short = 'r', long = "reason")] 35 | deletion_reason: Option, 36 | 37 | /// List of issues that removing this version would resolve 38 | #[arg(long)] 39 | resolves: Option>, 40 | 41 | #[arg(short, long)] 42 | submit: bool, 43 | 44 | /// Don't show the package removal warning 45 | #[arg(long)] 46 | no_warning: bool, 47 | 48 | /// Open pull request link automatically 49 | #[arg(long, env = "OPEN_PR")] 50 | open_pr: bool, 51 | 52 | /// GitHub personal access token with the `public_repo` scope 53 | #[arg(short, long, env = "GITHUB_TOKEN")] 54 | token: Option, 55 | } 56 | 57 | impl RemoveVersion { 58 | const MIN_REASON_LENGTH: usize = 4; 59 | const MAX_REASON_LENGTH: usize = 1000; 60 | 61 | pub async fn run(self) -> Result<()> { 62 | let token = handle_token(self.token.as_deref()).await?; 63 | if !self.no_warning { 64 | println!( 65 | "{}", 66 | "Packages should only be removed when necessary".yellow() 67 | ); 68 | } 69 | let github = GitHub::new(&token)?; 70 | 71 | let (fork, winget_pkgs, versions) = try_join!( 72 | github 73 | .get_username() 74 | .and_then(|current_user| github.get_winget_pkgs().owner(current_user).send()), 75 | github.get_winget_pkgs().send(), 76 | github.get_versions(&self.package_identifier) 77 | )?; 78 | 79 | if !versions.contains(&self.package_version) { 80 | bail!( 81 | "{} version {} does not exist in {WINGET_PKGS_FULL_NAME}", 82 | self.package_identifier, 83 | self.package_version, 84 | ); 85 | } 86 | 87 | let latest_version = versions.last().unwrap_or_else(|| unreachable!()); 88 | println!( 89 | "Latest version of {}: {latest_version}", 90 | &self.package_identifier 91 | ); 92 | let deletion_reason = match self.deletion_reason { 93 | Some(reason) => reason, 94 | None => Text::new(&format!( 95 | "Give a reason for removing {} version {}", 96 | &self.package_identifier, &self.package_version 97 | )) 98 | .with_validator(MinLengthValidator::new(Self::MIN_REASON_LENGTH)) 99 | .with_validator(MaxLengthValidator::new(Self::MAX_REASON_LENGTH)) 100 | .prompt() 101 | .map_err(handle_inquire_error)?, 102 | }; 103 | let should_remove_manifest = self.submit 104 | || confirm_prompt(&format!( 105 | "Would you like to make a pull request to remove {} {}?", 106 | self.package_identifier, self.package_version 107 | ))?; 108 | 109 | if !should_remove_manifest { 110 | return Ok(()); 111 | } 112 | 113 | let pull_request_url = github 114 | .remove_version() 115 | .identifier(&self.package_identifier) 116 | .version(&self.package_version) 117 | .reason(deletion_reason) 118 | .fork(&fork) 119 | .winget_pkgs(&winget_pkgs) 120 | .maybe_issue_resolves(self.resolves) 121 | .send() 122 | .await?; 123 | 124 | if self.open_pr { 125 | open::that(pull_request_url.as_str())?; 126 | } 127 | 128 | Ok(()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/commands/show_version.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use color_eyre::Result; 3 | use winget_types::{PackageIdentifier, PackageVersion}; 4 | 5 | use crate::{credential::handle_token, github::github_client::GitHub, manifests::print_changes}; 6 | 7 | /// Output the manifests for a given package and version 8 | #[expect(clippy::struct_excessive_bools)] 9 | #[derive(Parser)] 10 | pub struct ShowVersion { 11 | /// The package's unique identifier 12 | #[arg()] 13 | package_identifier: PackageIdentifier, 14 | 15 | /// The package's version 16 | #[arg(short = 'v', long = "version")] 17 | package_version: Option, 18 | 19 | /// Switch to display the installer manifest 20 | #[arg(short, long)] 21 | installer_manifest: bool, 22 | 23 | /// Switch to display the default locale manifest 24 | #[arg(short, long = "defaultlocale-manifest")] 25 | default_locale_manifest: bool, 26 | 27 | /// Switch to display all locale manifests 28 | #[arg(short, long)] 29 | locale_manifests: bool, 30 | 31 | /// Switch to display the version manifest 32 | #[arg(long)] 33 | version_manifest: bool, 34 | 35 | /// GitHub personal access token with the `public_repo` scope 36 | #[arg(short, long, env = "GITHUB_TOKEN")] 37 | token: Option, 38 | } 39 | 40 | impl ShowVersion { 41 | pub async fn run(self) -> Result<()> { 42 | let token = handle_token(self.token.as_deref()).await?; 43 | let github = GitHub::new(&token)?; 44 | 45 | // Get a list of all versions for the given package 46 | let mut versions = github.get_versions(&self.package_identifier).await?; 47 | 48 | // Get the manifests for the latest or specified version 49 | let manifests = github 50 | .get_manifests( 51 | &self.package_identifier, 52 | &self 53 | .package_version 54 | .unwrap_or_else(|| versions.pop_last().unwrap_or_else(|| unreachable!())), 55 | ) 56 | .await?; 57 | 58 | let all = matches!( 59 | ( 60 | self.installer_manifest, 61 | self.default_locale_manifest, 62 | self.locale_manifests, 63 | self.version_manifest 64 | ), 65 | (false, false, false, false) 66 | ); 67 | 68 | let mut contents = Vec::new(); 69 | if all || self.installer_manifest { 70 | contents.push(serde_yaml::to_string(&manifests.installer)?); 71 | } 72 | if all || self.default_locale_manifest { 73 | contents.push(serde_yaml::to_string(&manifests.default_locale)?); 74 | } 75 | if all || self.locale_manifests { 76 | contents.extend( 77 | manifests 78 | .locales 79 | .into_iter() 80 | .flat_map(|locale_manifest| serde_yaml::to_string(&locale_manifest)), 81 | ); 82 | } 83 | if all || self.version_manifest { 84 | contents.push(serde_yaml::to_string(&manifests.version)?); 85 | } 86 | 87 | print_changes(contents.iter().map(String::as_str)); 88 | 89 | Ok(()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/commands/sync_fork.rs: -------------------------------------------------------------------------------- 1 | use anstream::println; 2 | use clap::Parser; 3 | use color_eyre::Result; 4 | use futures_util::TryFutureExt; 5 | use indicatif::ProgressBar; 6 | use owo_colors::OwoColorize; 7 | use tokio::try_join; 8 | 9 | use crate::{ 10 | commands::utils::SPINNER_TICK_RATE, credential::handle_token, github::github_client::GitHub, 11 | terminal::Hyperlinkable, 12 | }; 13 | 14 | /// Merges changes from microsoft/winget-pkgs into the fork repository 15 | #[derive(Parser)] 16 | #[clap(visible_aliases = ["sync", "merge-upstream"])] 17 | pub struct SyncFork { 18 | /// Merges changes even if the fork's default branch is not fast-forward. This is not 19 | /// recommended as you should instead have a clean default branch that has not diverged from the 20 | /// upstream default branch 21 | #[arg(short, long)] 22 | force: bool, 23 | 24 | /// GitHub personal access token with the `public_repo` scope 25 | #[arg(short, long, env = "GITHUB_TOKEN")] 26 | token: Option, 27 | } 28 | 29 | impl SyncFork { 30 | pub async fn run(self) -> Result<()> { 31 | let token = handle_token(self.token.as_deref()).await?; 32 | let github = GitHub::new(&token)?; 33 | 34 | // Fetch repository data from both upstream and fork repositories asynchronously 35 | let (winget_pkgs, fork) = try_join!( 36 | github.get_winget_pkgs().send(), 37 | github 38 | .get_username() 39 | .and_then(|username| github.get_winget_pkgs().owner(username).send()), 40 | )?; 41 | 42 | // Check whether the fork is already up-to-date with upstream by their latest commit OID's 43 | if winget_pkgs.default_branch_oid == fork.default_branch_oid { 44 | println!( 45 | "{} is already {} with {}", 46 | fork.full_name.hyperlink(&fork.url).blue(), 47 | "up-to-date".green(), 48 | winget_pkgs.full_name.hyperlink(&winget_pkgs.url).blue() 49 | ); 50 | return Ok(()); 51 | } 52 | 53 | // Calculate how many commits upstream is ahead of fork 54 | let new_commits_count = winget_pkgs.commit_count - fork.commit_count; 55 | let commit_label = match new_commits_count { 56 | 1 => "commit", 57 | _ => "commits", 58 | }; 59 | 60 | // Show an indeterminate progress bar while upstream changes are being merged 61 | let pb = ProgressBar::new_spinner().with_message(format!( 62 | "Merging {new_commits_count} upstream {commit_label} from {} into {}", 63 | winget_pkgs.full_name.blue(), 64 | fork.full_name.blue(), 65 | )); 66 | pb.enable_steady_tick(SPINNER_TICK_RATE); 67 | 68 | github 69 | .merge_upstream( 70 | &fork.default_branch_ref_id, 71 | winget_pkgs.default_branch_oid, 72 | self.force, 73 | ) 74 | .await?; 75 | 76 | pb.finish_and_clear(); 77 | 78 | println!( 79 | "{} merged {new_commits_count} upstream {commit_label} from {} into {}", 80 | "Successfully".green(), 81 | winget_pkgs.full_name.hyperlink(winget_pkgs.url).blue(), 82 | fork.full_name.hyperlink(fork.url).blue() 83 | ); 84 | 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/commands/token/commands.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, Subcommand}; 2 | 3 | use crate::commands::token::{remove::RemoveToken, update::UpdateToken}; 4 | 5 | #[derive(Args)] 6 | pub struct TokenArgs { 7 | #[command(subcommand)] 8 | pub command: TokenCommands, 9 | } 10 | #[derive(Subcommand)] 11 | pub enum TokenCommands { 12 | Update(UpdateToken), 13 | Remove(RemoveToken), 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/token/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | pub mod remove; 3 | pub mod update; 4 | -------------------------------------------------------------------------------- /src/commands/token/remove.rs: -------------------------------------------------------------------------------- 1 | use anstream::println; 2 | use clap::Parser; 3 | use color_eyre::eyre::Result; 4 | use owo_colors::OwoColorize; 5 | 6 | use crate::{credential::get_komac_credential, prompts::text::confirm_prompt}; 7 | 8 | /// Remove the stored token 9 | #[derive(Parser)] 10 | #[clap(visible_alias = "delete")] 11 | pub struct RemoveToken { 12 | /// Skip the confirmation prompt to delete the token 13 | #[arg(short = 'y', long = "yes")] 14 | skip_prompt: bool, 15 | } 16 | 17 | impl RemoveToken { 18 | pub fn run(self) -> Result<()> { 19 | let credential = get_komac_credential()?; 20 | 21 | if matches!( 22 | credential.get_password().err(), 23 | Some(keyring::Error::NoEntry) 24 | ) { 25 | println!("No token stored is currently stored in the platform's secure storage"); 26 | } 27 | 28 | let confirm = if self.skip_prompt { 29 | true 30 | } else { 31 | confirm_prompt("Would you like to remove the currently stored token?")? 32 | }; 33 | 34 | if confirm { 35 | credential.delete_credential()?; 36 | println!( 37 | "{} deleted the stored token from the platform's secure storage", 38 | "Successfully".green() 39 | ); 40 | } else { 41 | println!("{}", "No token was deleted".cyan()); 42 | } 43 | 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/token/update.rs: -------------------------------------------------------------------------------- 1 | use anstream::println; 2 | use clap::Parser; 3 | use color_eyre::eyre::Result; 4 | use owo_colors::OwoColorize; 5 | use reqwest::Client; 6 | 7 | use crate::credential::{get_default_headers, get_komac_credential, token_prompt, validate_token}; 8 | 9 | /// Update the stored token 10 | #[derive(Parser)] 11 | #[clap(visible_aliases = ["new", "add"])] 12 | pub struct UpdateToken { 13 | /// The new token to store 14 | #[arg(short, long)] 15 | token: Option, 16 | } 17 | 18 | impl UpdateToken { 19 | pub async fn run(self) -> Result<()> { 20 | let credential = get_komac_credential()?; 21 | 22 | let client = Client::builder() 23 | .default_headers(get_default_headers(None)) 24 | .build()?; 25 | 26 | let token = match self.token { 27 | Some(token) => validate_token(&client, &token).await.map(|()| token)?, 28 | None => token_prompt(client, Some("Please enter the new token to set"))?, 29 | }; 30 | 31 | if credential.set_password(&token).is_ok() { 32 | println!( 33 | "{} stored token in platform's secure storage", 34 | "Successfully".green() 35 | ); 36 | } 37 | 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{env, time::Duration}; 2 | 3 | use anstream::println; 4 | use camino::Utf8Path; 5 | use chrono::Local; 6 | use color_eyre::Result; 7 | use derive_more::Display; 8 | use futures_util::{StreamExt, TryStreamExt, stream}; 9 | use inquire::{Select, error::InquireResult}; 10 | use owo_colors::OwoColorize; 11 | use strum::{EnumIter, IntoEnumIterator}; 12 | use tokio::{fs, fs::File, io::AsyncWriteExt}; 13 | use winget_types::{PackageIdentifier, PackageVersion}; 14 | 15 | use crate::{ 16 | editor::Editor, 17 | github::graphql::get_existing_pull_request::PullRequest, 18 | manifests::print_changes, 19 | prompts::{handle_inquire_error, text::confirm_prompt}, 20 | }; 21 | 22 | pub const SPINNER_TICK_RATE: Duration = Duration::from_millis(50); 23 | 24 | pub const SPINNER_SLOW_TICK_RATE: Duration = Duration::from_millis(100); 25 | 26 | pub fn prompt_existing_pull_request( 27 | identifier: &PackageIdentifier, 28 | version: &PackageVersion, 29 | pull_request: &PullRequest, 30 | ) -> InquireResult { 31 | let created_at = pull_request.created_at.with_timezone(&Local); 32 | println!( 33 | "There is already {} pull request for {identifier} {version} that was created on {} at {}", 34 | pull_request.state, 35 | created_at.date_naive(), 36 | created_at.time() 37 | ); 38 | println!("{}", pull_request.url.blue()); 39 | if env::var("CI").is_ok_and(|ci| ci.parse() == Ok(true)) { 40 | // Exit instead of proceeding in CI environments 41 | Ok(false) 42 | } else { 43 | confirm_prompt("Would you like to proceed?") 44 | } 45 | } 46 | 47 | pub fn prompt_submit_option( 48 | changes: &mut [(String, String)], 49 | submit: bool, 50 | identifier: &PackageIdentifier, 51 | version: &PackageVersion, 52 | dry_run: bool, 53 | ) -> Result { 54 | let mut submit_option; 55 | loop { 56 | print_changes(changes.iter().map(|(_, content)| content.as_str())); 57 | 58 | submit_option = if dry_run { 59 | SubmitOption::Exit 60 | } else if submit { 61 | SubmitOption::Submit 62 | } else { 63 | Select::new( 64 | &format!("What would you like to do with {identifier} {version}?"), 65 | SubmitOption::iter().collect(), 66 | ) 67 | .prompt() 68 | .map_err(handle_inquire_error)? 69 | }; 70 | 71 | if submit_option == SubmitOption::Edit { 72 | Editor::new(changes).run()?; 73 | } else { 74 | break; 75 | } 76 | } 77 | Ok(submit_option) 78 | } 79 | 80 | #[derive(Display, EnumIter, Eq, PartialEq)] 81 | pub enum SubmitOption { 82 | Submit, 83 | Edit, 84 | Exit, 85 | } 86 | 87 | pub async fn write_changes_to_dir(changes: &[(String, String)], output: &Utf8Path) -> Result<()> { 88 | fs::create_dir_all(output).await?; 89 | stream::iter(changes.iter()) 90 | .map(|(path, content)| async move { 91 | if let Some(file_name) = Utf8Path::new(path).file_name() { 92 | let mut file = File::create(output.join(file_name)).await?; 93 | file.write_all(content.as_bytes()).await?; 94 | } 95 | Ok::<(), color_eyre::eyre::Error>(()) 96 | }) 97 | .buffer_unordered(2) 98 | .try_collect() 99 | .await 100 | } 101 | -------------------------------------------------------------------------------- /src/credential.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use color_eyre::eyre::{Result, bail}; 4 | use inquire::{Password, error::InquireResult, validator::Validation}; 5 | use keyring::Entry; 6 | use reqwest::{ 7 | Client, StatusCode, 8 | header::{AUTHORIZATION, DNT, HeaderMap, HeaderValue, USER_AGENT}, 9 | }; 10 | use tokio::runtime::Handle; 11 | 12 | use crate::prompts::handle_inquire_error; 13 | 14 | const SERVICE: &str = "komac"; 15 | const USERNAME: &str = "github-access-token"; 16 | const GITHUB_API_ENDPOINT: &str = "https://api.github.com/octocat"; 17 | 18 | pub fn get_komac_credential() -> keyring::Result { 19 | Entry::new(SERVICE, USERNAME) 20 | } 21 | 22 | pub async fn handle_token(token: Option<&str>) -> Result> { 23 | let client = Client::builder() 24 | .default_headers(get_default_headers(None)) 25 | .build()?; 26 | 27 | if let Some(token) = token { 28 | return validate_token(&client, token) 29 | .await 30 | .map(|()| Cow::Borrowed(token)); 31 | } 32 | 33 | let credential_entry = get_komac_credential()?; 34 | 35 | if let Ok(stored_token) = credential_entry.get_password() { 36 | validate_token(&client, &stored_token) 37 | .await 38 | .map(|()| Cow::Owned(stored_token)) 39 | } else { 40 | let token = token_prompt(client, None)?; 41 | if credential_entry.set_password(&token).is_ok() { 42 | println!("Successfully stored token in platform's secure storage"); 43 | } 44 | Ok(Cow::Owned(token)) 45 | } 46 | } 47 | 48 | pub fn token_prompt(client: Client, prompt: Option<&str>) -> InquireResult { 49 | tokio::task::block_in_place(|| { 50 | let rt = Handle::current(); 51 | let validator = 52 | move |input: &str| match rt.block_on(async { validate_token(&client, input).await }) { 53 | Ok(()) => Ok(Validation::Valid), 54 | Err(err) => Ok(Validation::Invalid(err.into())), 55 | }; 56 | Password::new(prompt.unwrap_or("Enter a GitHub token")) 57 | .with_validator(validator) 58 | .without_confirmation() 59 | .prompt() 60 | .map_err(handle_inquire_error) 61 | }) 62 | } 63 | 64 | pub async fn validate_token(client: &Client, token: &str) -> Result<()> { 65 | match client 66 | .get(GITHUB_API_ENDPOINT) 67 | .bearer_auth(token) 68 | .send() 69 | .await 70 | { 71 | Ok(response) => match response.status() { 72 | StatusCode::UNAUTHORIZED => bail!("GitHub token is invalid"), 73 | _ => Ok(()), 74 | }, 75 | Err(error) => { 76 | if error.is_connect() { 77 | bail!("Failed to connect to GitHub. Please check your internet connection."); 78 | } 79 | Err(error.into()) 80 | } 81 | } 82 | } 83 | 84 | const MICROSOFT_DELIVERY_OPTIMIZATION: HeaderValue = 85 | HeaderValue::from_static("Microsoft-Delivery-Optimization/10.1"); 86 | const SEC_GPC: &str = "Sec-GPC"; 87 | 88 | pub fn get_default_headers(github_token: Option<&str>) -> HeaderMap { 89 | let mut default_headers = HeaderMap::new(); 90 | default_headers.insert(USER_AGENT, MICROSOFT_DELIVERY_OPTIMIZATION); 91 | default_headers.insert(DNT, HeaderValue::from(1)); 92 | default_headers.insert(SEC_GPC, HeaderValue::from(1)); 93 | if let Some(token) = github_token { 94 | if let Ok(bearer_auth) = HeaderValue::from_str(&format!("Bearer {token}")) { 95 | default_headers.insert(AUTHORIZATION, bearer_auth); 96 | } 97 | } 98 | default_headers 99 | } 100 | -------------------------------------------------------------------------------- /src/download/file.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use chrono::NaiveDate; 4 | use memmap2::Mmap; 5 | use winget_types::Sha256String; 6 | 7 | use crate::manifests::Url; 8 | 9 | pub struct DownloadedFile { 10 | // As the downloaded file is a temporary file, it's stored here so that the reference stays 11 | // alive and the file does not get deleted. This is necessary because the memory map needs the 12 | // file to remain present. 13 | #[expect(dead_code)] 14 | pub file: File, 15 | pub url: Url, 16 | pub mmap: Mmap, 17 | pub sha_256: Sha256String, 18 | pub file_name: String, 19 | pub last_modified: Option, 20 | } 21 | -------------------------------------------------------------------------------- /src/download_file.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, mem}; 2 | 3 | use color_eyre::eyre::Result; 4 | use futures_util::{StreamExt, TryStreamExt, stream}; 5 | use winget_types::{installer::Architecture, url::DecodedUrl}; 6 | 7 | use crate::{download::DownloadedFile, file_analyser::FileAnalyser}; 8 | 9 | pub async fn process_files( 10 | files: &mut [DownloadedFile], 11 | ) -> Result> { 12 | stream::iter(files.iter_mut().map( 13 | |DownloadedFile { 14 | url, 15 | mmap, 16 | sha_256, 17 | file_name, 18 | last_modified, 19 | .. 20 | }| async move { 21 | let mut file_analyser = FileAnalyser::new(mmap, file_name)?; 22 | let architecture = url 23 | .override_architecture() 24 | .or_else(|| Architecture::from_url(url.as_str())); 25 | for installer in &mut file_analyser.installers { 26 | if let Some(architecture) = architecture { 27 | installer.architecture = architecture; 28 | } 29 | installer.url = url.inner().clone(); 30 | installer.sha_256 = sha_256.clone(); 31 | installer.release_date = *last_modified; 32 | } 33 | file_analyser.file_name = mem::take(file_name); 34 | Ok((mem::take(url.inner_mut()), file_analyser)) 35 | }, 36 | )) 37 | .buffer_unordered(num_cpus::get()) 38 | .try_collect::>() 39 | .await 40 | } 41 | -------------------------------------------------------------------------------- /src/github/graphql/create_commit.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use derive_new::new; 4 | use url::Url; 5 | 6 | use crate::github::graphql::{ 7 | github_schema::github_schema as schema, 8 | types::{Base64String, GitObjectId}, 9 | }; 10 | 11 | #[derive(cynic::QueryVariables)] 12 | pub struct CreateCommitVariables<'a> { 13 | pub input: CreateCommitOnBranchInput<'a>, 14 | } 15 | 16 | #[derive(cynic::QueryFragment)] 17 | #[cynic(graphql_type = "Mutation", variables = "CreateCommitVariables")] 18 | pub struct CreateCommit { 19 | #[arguments(input: $input)] 20 | pub create_commit_on_branch: Option, 21 | } 22 | 23 | /// 24 | #[derive(cynic::QueryFragment)] 25 | pub struct CreateCommitOnBranchPayload { 26 | pub commit: Option, 27 | } 28 | 29 | /// 30 | #[derive(cynic::QueryFragment)] 31 | pub struct Commit { 32 | pub url: Url, 33 | } 34 | 35 | /// 36 | #[derive(cynic::InputObject)] 37 | pub struct CreateCommitOnBranchInput<'a> { 38 | pub branch: CommittableBranch<'a>, 39 | pub expected_head_oid: GitObjectId, 40 | #[cynic(skip_serializing_if = "Option::is_none")] 41 | pub file_changes: Option>, 42 | pub message: CommitMessage<'a>, 43 | } 44 | 45 | /// 46 | #[derive(cynic::InputObject)] 47 | pub struct FileChanges<'a> { 48 | #[cynic(skip_serializing_if = "Option::is_none")] 49 | pub additions: Option>>, 50 | #[cynic(skip_serializing_if = "Option::is_none")] 51 | pub deletions: Option>>, 52 | } 53 | 54 | /// 55 | #[derive(cynic::InputObject, new)] 56 | pub struct FileDeletion<'path> { 57 | #[new(into)] 58 | pub path: Cow<'path, str>, 59 | } 60 | 61 | /// 62 | #[derive(cynic::InputObject, new)] 63 | pub struct FileAddition<'path> { 64 | pub contents: Base64String, 65 | #[new(into)] 66 | pub path: Cow<'path, str>, 67 | } 68 | 69 | /// 70 | #[derive(cynic::InputObject)] 71 | pub struct CommittableBranch<'a> { 72 | pub id: &'a cynic::Id, 73 | } 74 | 75 | /// 76 | #[derive(cynic::InputObject)] 77 | pub struct CommitMessage<'a> { 78 | #[cynic(skip_serializing_if = "Option::is_none")] 79 | pub body: Option<&'a str>, 80 | pub headline: &'a str, 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use cynic::{Id, MutationBuilder}; 86 | use indoc::indoc; 87 | 88 | use crate::github::graphql::{ 89 | create_commit::{ 90 | CommitMessage, CommittableBranch, CreateCommit, CreateCommitOnBranchInput, 91 | CreateCommitVariables, 92 | }, 93 | types::GitObjectId, 94 | }; 95 | 96 | #[test] 97 | fn create_commit_output() { 98 | const CREATE_COMMIT_MUTATION: &str = indoc! {" 99 | mutation CreateCommit($input: CreateCommitOnBranchInput!) { 100 | createCommitOnBranch(input: $input) { 101 | commit { 102 | url 103 | } 104 | } 105 | } 106 | "}; 107 | 108 | let id = Id::new(""); 109 | let operation = CreateCommit::build(CreateCommitVariables { 110 | input: CreateCommitOnBranchInput { 111 | branch: CommittableBranch { id: &id }, 112 | expected_head_oid: GitObjectId::new(""), 113 | file_changes: None, 114 | message: CommitMessage { 115 | body: None, 116 | headline: "", 117 | }, 118 | }, 119 | }); 120 | 121 | assert_eq!(operation.query, CREATE_COMMIT_MUTATION); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/github/graphql/create_pull_request.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | use crate::github::graphql::github_schema::github_schema as schema; 4 | 5 | #[derive(cynic::QueryVariables)] 6 | pub struct CreatePullRequestVariables<'a> { 7 | pub input: CreatePullRequestInput<'a>, 8 | } 9 | 10 | #[derive(cynic::QueryFragment)] 11 | #[cynic(graphql_type = "Mutation", variables = "CreatePullRequestVariables")] 12 | pub struct CreatePullRequest { 13 | #[arguments(input: $input)] 14 | pub create_pull_request: Option, 15 | } 16 | 17 | /// 18 | #[derive(cynic::QueryFragment)] 19 | pub struct CreatePullRequestPayload { 20 | pub pull_request: Option, 21 | } 22 | 23 | /// 24 | #[derive(cynic::QueryFragment)] 25 | pub struct PullRequest { 26 | pub url: Url, 27 | } 28 | 29 | /// 30 | #[derive(cynic::InputObject)] 31 | pub struct CreatePullRequestInput<'a> { 32 | pub base_ref_name: &'a str, 33 | #[cynic(skip_serializing_if = "Option::is_none")] 34 | pub body: Option<&'a str>, 35 | #[cynic(skip_serializing_if = "Option::is_none")] 36 | pub draft: Option, 37 | pub head_ref_name: &'a str, 38 | #[cynic(skip_serializing_if = "Option::is_none")] 39 | pub head_repository_id: Option<&'a cynic::Id>, 40 | #[cynic(skip_serializing_if = "Option::is_none")] 41 | pub maintainer_can_modify: Option, 42 | pub repository_id: &'a cynic::Id, 43 | pub title: &'a str, 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use cynic::{Id, MutationBuilder}; 49 | use indoc::indoc; 50 | 51 | use crate::github::graphql::create_pull_request::{ 52 | CreatePullRequest, CreatePullRequestInput, CreatePullRequestVariables, 53 | }; 54 | 55 | #[test] 56 | fn create_commit_output() { 57 | const CREATE_PULL_REQUEST_MUTATION: &str = indoc! {" 58 | mutation CreatePullRequest($input: CreatePullRequestInput!) { 59 | createPullRequest(input: $input) { 60 | pullRequest { 61 | url 62 | } 63 | } 64 | } 65 | "}; 66 | 67 | let id = Id::new(""); 68 | let operation = CreatePullRequest::build(CreatePullRequestVariables { 69 | input: CreatePullRequestInput { 70 | base_ref_name: "", 71 | body: None, 72 | draft: None, 73 | head_ref_name: "", 74 | head_repository_id: None, 75 | maintainer_can_modify: None, 76 | repository_id: &id, 77 | title: "", 78 | }, 79 | }); 80 | 81 | assert_eq!(operation.query, CREATE_PULL_REQUEST_MUTATION); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/github/graphql/create_ref.rs: -------------------------------------------------------------------------------- 1 | use crate::github::graphql::{github_schema::github_schema as schema, types::GitObjectId}; 2 | 3 | /// 4 | #[derive(cynic::QueryVariables)] 5 | pub struct CreateRefVariables<'a> { 6 | pub name: &'a str, 7 | pub oid: GitObjectId, 8 | pub repository_id: &'a cynic::Id, 9 | } 10 | 11 | #[derive(cynic::QueryFragment)] 12 | #[cynic(graphql_type = "Mutation", variables = "CreateRefVariables")] 13 | pub struct CreateRef { 14 | #[arguments(input: { name: $name, oid: $oid, repositoryId: $repository_id })] 15 | pub create_ref: Option, 16 | } 17 | 18 | /// 19 | #[derive(cynic::QueryFragment)] 20 | pub struct CreateRefPayload { 21 | #[cynic(rename = "ref")] 22 | pub ref_: Option, 23 | } 24 | 25 | /// 26 | #[derive(cynic::QueryFragment)] 27 | pub struct Ref { 28 | pub id: cynic::Id, 29 | pub name: String, 30 | pub target: Option, 31 | } 32 | 33 | /// 34 | #[derive(cynic::QueryFragment)] 35 | pub struct GitObject { 36 | pub oid: GitObjectId, 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use cynic::{Id, MutationBuilder}; 42 | use indoc::indoc; 43 | 44 | use crate::github::graphql::{ 45 | create_ref::{CreateRef, CreateRefVariables}, 46 | types::GitObjectId, 47 | }; 48 | 49 | #[test] 50 | fn create_ref_output() { 51 | const CREATE_REF_MUTATION: &str = indoc! {" 52 | mutation CreateRef($name: String!, $oid: GitObjectID!, $repositoryId: ID!) { 53 | createRef(input: {name: $name, oid: $oid, repositoryId: $repositoryId}) { 54 | ref { 55 | id 56 | name 57 | target { 58 | oid 59 | } 60 | } 61 | } 62 | } 63 | "}; 64 | 65 | let id = Id::new(""); 66 | let operation = CreateRef::build(CreateRefVariables { 67 | name: "", 68 | oid: GitObjectId::new(""), 69 | repository_id: &id, 70 | }); 71 | 72 | assert_eq!(operation.query, CREATE_REF_MUTATION); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/github/graphql/get_all_values.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | use crate::github::graphql::{github_schema::github_schema as schema, types::Html}; 4 | 5 | #[derive(cynic::QueryVariables)] 6 | pub struct GetAllValuesVariables<'a> { 7 | pub owner: &'a str, 8 | pub name: &'a str, 9 | pub tag_name: &'a str, 10 | } 11 | 12 | #[derive(cynic::QueryFragment)] 13 | pub struct Tree { 14 | #[cynic(flatten)] 15 | pub entries: Vec, 16 | } 17 | 18 | #[derive(cynic::QueryFragment)] 19 | pub struct TreeEntry { 20 | pub name: String, 21 | #[cynic(rename = "type")] 22 | pub type_: String, 23 | } 24 | 25 | #[derive(cynic::QueryFragment)] 26 | #[cynic(graphql_type = "Query", variables = "GetAllValuesVariables")] 27 | pub struct GetAllValues { 28 | #[arguments(owner: $owner, name: $name)] 29 | pub repository: Option, 30 | } 31 | 32 | /// 33 | #[derive(cynic::QueryFragment)] 34 | #[cynic(variables = "GetAllValuesVariables")] 35 | pub struct Repository { 36 | pub has_issues_enabled: bool, 37 | pub license_info: Option, 38 | pub owner: RepositoryOwner, 39 | #[arguments(expression: "HEAD:")] 40 | pub object: Option, 41 | #[arguments(tagName: $tag_name)] 42 | pub release: Option, 43 | #[cynic(rename = "repositoryTopics")] 44 | #[arguments(first: 16)] 45 | pub topics: RepositoryTopicConnection, 46 | pub url: Url, 47 | } 48 | 49 | /// 50 | #[derive(cynic::QueryFragment)] 51 | pub struct RepositoryTopicConnection { 52 | #[cynic(flatten)] 53 | pub nodes: Vec, 54 | } 55 | 56 | /// 57 | #[derive(cynic::QueryFragment)] 58 | pub struct RepositoryTopic { 59 | pub topic: Topic, 60 | } 61 | 62 | /// 63 | #[derive(cynic::QueryFragment)] 64 | pub struct Topic { 65 | pub name: String, 66 | } 67 | 68 | /// 69 | #[derive(cynic::QueryFragment)] 70 | pub struct Release { 71 | #[cynic(rename = "descriptionHTML")] 72 | pub description_html: Option, 73 | pub url: Url, 74 | } 75 | 76 | /// 77 | #[derive(cynic::QueryFragment)] 78 | pub struct RepositoryOwner { 79 | pub url: Url, 80 | } 81 | 82 | /// 83 | #[derive(cynic::QueryFragment)] 84 | pub struct License { 85 | pub key: String, 86 | #[cynic(rename = "pseudoLicense")] 87 | pub is_pseudo: bool, 88 | pub spdx_id: Option, 89 | } 90 | 91 | #[derive(cynic::InlineFragments)] 92 | #[cynic(graphql_type = "GitObject")] 93 | pub enum GetAllValuesGitObject { 94 | Tree(Tree), 95 | #[cynic(fallback)] 96 | Unknown, 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use cynic::QueryBuilder; 102 | use indoc::indoc; 103 | 104 | use crate::github::{ 105 | github_client::{MICROSOFT, WINGET_PKGS}, 106 | graphql::get_all_values::{GetAllValues, GetAllValuesVariables}, 107 | }; 108 | 109 | #[test] 110 | fn get_all_values_output() { 111 | const GET_ALL_VALUES_QUERY: &str = indoc! {r#" 112 | query GetAllValues($owner: String!, $name: String!, $tagName: String!) { 113 | repository(owner: $owner, name: $name) { 114 | hasIssuesEnabled 115 | licenseInfo { 116 | key 117 | pseudoLicense 118 | spdxId 119 | } 120 | owner { 121 | url 122 | } 123 | object(expression: "HEAD:") { 124 | __typename 125 | ... on Tree { 126 | entries { 127 | name 128 | type 129 | } 130 | } 131 | } 132 | release(tagName: $tagName) { 133 | descriptionHTML 134 | url 135 | } 136 | repositoryTopics(first: 16) { 137 | nodes { 138 | topic { 139 | name 140 | } 141 | } 142 | } 143 | url 144 | } 145 | } 146 | "#}; 147 | 148 | let operation = GetAllValues::build(GetAllValuesVariables { 149 | owner: MICROSOFT, 150 | name: WINGET_PKGS, 151 | tag_name: "", 152 | }); 153 | 154 | assert_eq!(operation.query, GET_ALL_VALUES_QUERY); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/github/graphql/get_branches.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use url::Url; 4 | 5 | use crate::github::graphql::github_schema::github_schema as schema; 6 | 7 | #[derive(cynic::QueryVariables)] 8 | pub struct GetBranchesVariables<'a> { 9 | pub owner: &'a str, 10 | pub name: &'a str, 11 | pub cursor: Option<&'a str>, 12 | } 13 | 14 | #[derive(cynic::QueryFragment)] 15 | #[cynic(graphql_type = "Query", variables = "GetBranchesVariables")] 16 | pub struct GetBranches { 17 | #[arguments(owner: $owner, name: $name)] 18 | pub repository: Option, 19 | } 20 | 21 | /// 22 | #[derive(cynic::QueryFragment)] 23 | #[cynic(variables = "GetBranchesVariables")] 24 | pub struct Repository { 25 | pub id: cynic::Id, 26 | pub default_branch_ref: Option, 27 | #[arguments(first: 100, after: $cursor, refPrefix: "refs/heads/")] 28 | pub refs: Option, 29 | } 30 | 31 | /// 32 | #[derive(cynic::QueryFragment)] 33 | pub struct RefConnection { 34 | #[cynic(rename = "nodes", flatten)] 35 | pub branches: Vec, 36 | pub page_info: PageInfo, 37 | } 38 | 39 | #[derive(cynic::QueryFragment)] 40 | pub struct PageInfo { 41 | pub end_cursor: Option, 42 | pub has_next_page: bool, 43 | } 44 | 45 | /// 46 | #[derive(cynic::QueryFragment, Hash, PartialEq, Eq)] 47 | #[cynic(graphql_type = "Ref")] 48 | pub struct PullRequestBranchRef { 49 | pub name: String, 50 | #[arguments(first: 5)] 51 | pub associated_pull_requests: PullRequestConnection, 52 | } 53 | 54 | /// 55 | #[derive(cynic::QueryFragment)] 56 | #[cynic(graphql_type = "Ref")] 57 | pub struct DefaultBranchRef { 58 | pub name: String, 59 | } 60 | 61 | /// 62 | #[derive(cynic::QueryFragment, Hash, PartialEq, Eq)] 63 | pub struct PullRequestConnection { 64 | #[cynic(rename = "nodes", flatten)] 65 | pub pull_requests: Vec, 66 | } 67 | 68 | /// 69 | #[derive(cynic::QueryFragment, Hash, PartialEq, Eq)] 70 | pub struct PullRequest { 71 | pub title: String, 72 | pub url: Url, 73 | pub state: PullRequestState, 74 | pub repository: PullRequestRepository, 75 | } 76 | 77 | impl Display for PullRequest { 78 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 79 | write!(f, "{}", self.title) 80 | } 81 | } 82 | 83 | /// 84 | #[derive(cynic::QueryFragment, Hash, PartialEq, Eq)] 85 | #[cynic(graphql_type = "Repository")] 86 | pub struct PullRequestRepository { 87 | pub name_with_owner: String, 88 | } 89 | 90 | /// 91 | #[derive(cynic::Enum, Clone, Copy, Hash, PartialEq, Eq)] 92 | pub enum PullRequestState { 93 | Closed, 94 | Merged, 95 | Open, 96 | } 97 | 98 | impl Display for PullRequestState { 99 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 100 | write!( 101 | f, 102 | "{}", 103 | match self { 104 | Self::Merged => "a merged", 105 | Self::Open => "an open", 106 | Self::Closed => "a closed", 107 | } 108 | ) 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use cynic::QueryBuilder; 115 | use indoc::indoc; 116 | 117 | use crate::github::{ 118 | github_client::{MICROSOFT, WINGET_PKGS}, 119 | graphql::get_branches::{GetBranches, GetBranchesVariables}, 120 | }; 121 | 122 | #[test] 123 | fn get_branches_query_output() { 124 | const GET_BRANCHES_QUERY: &str = indoc! {r#" 125 | query GetBranches($owner: String!, $name: String!, $cursor: String) { 126 | repository(owner: $owner, name: $name) { 127 | id 128 | defaultBranchRef { 129 | name 130 | } 131 | refs(first: 100, after: $cursor, refPrefix: "refs/heads/") { 132 | nodes { 133 | name 134 | associatedPullRequests(first: 5) { 135 | nodes { 136 | title 137 | url 138 | state 139 | repository { 140 | nameWithOwner 141 | } 142 | } 143 | } 144 | } 145 | pageInfo { 146 | endCursor 147 | hasNextPage 148 | } 149 | } 150 | } 151 | } 152 | "#}; 153 | 154 | let operation = GetBranches::build(GetBranchesVariables { 155 | owner: MICROSOFT, 156 | name: WINGET_PKGS, 157 | cursor: None, 158 | }); 159 | 160 | assert_eq!(operation.query, GET_BRANCHES_QUERY); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/github/graphql/get_current_user_login.rs: -------------------------------------------------------------------------------- 1 | use crate::github::graphql::github_schema::github_schema as schema; 2 | 3 | /// 4 | #[derive(cynic::QueryFragment)] 5 | #[cynic(graphql_type = "Query")] 6 | pub struct GetCurrentUserLogin { 7 | pub viewer: User, 8 | } 9 | 10 | /// 11 | #[derive(cynic::QueryFragment)] 12 | pub struct User { 13 | pub login: String, 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use cynic::QueryBuilder; 19 | use indoc::indoc; 20 | 21 | use crate::github::graphql::get_current_user_login::GetCurrentUserLogin; 22 | 23 | #[test] 24 | fn get_current_user_login_output() { 25 | const GET_CURRENT_USER_LOGIN_QUERY: &str = indoc! {r#" 26 | query GetCurrentUserLogin { 27 | viewer { 28 | login 29 | } 30 | } 31 | "#}; 32 | 33 | let operation = GetCurrentUserLogin::build(()); 34 | 35 | assert_eq!(operation.query, GET_CURRENT_USER_LOGIN_QUERY); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/github/graphql/get_directory_content.rs: -------------------------------------------------------------------------------- 1 | use crate::github::graphql::github_schema::github_schema as schema; 2 | 3 | #[derive(cynic::QueryVariables)] 4 | pub struct GetDirectoryContentVariables<'a> { 5 | pub owner: &'a str, 6 | pub name: &'a str, 7 | pub expression: &'a str, 8 | } 9 | 10 | #[derive(cynic::QueryFragment)] 11 | pub struct Tree { 12 | #[cynic(flatten)] 13 | pub entries: Vec, 14 | } 15 | 16 | #[derive(cynic::QueryFragment)] 17 | pub struct TreeEntry { 18 | pub path: Option, 19 | } 20 | 21 | #[derive(cynic::QueryFragment)] 22 | #[cynic(graphql_type = "Query", variables = "GetDirectoryContentVariables")] 23 | pub struct GetDirectoryContent { 24 | #[arguments(owner: $owner, name: $name)] 25 | pub repository: Option, 26 | } 27 | 28 | #[derive(cynic::QueryFragment)] 29 | #[cynic(variables = "GetDirectoryContentVariables")] 30 | pub struct Repository { 31 | #[arguments(expression: $expression)] 32 | pub object: Option, 33 | } 34 | 35 | #[derive(cynic::InlineFragments)] 36 | #[cynic(graphql_type = "GitObject")] 37 | pub enum TreeGitObject { 38 | Tree(Tree), 39 | #[cynic(fallback)] 40 | Unknown, 41 | } 42 | 43 | impl TreeGitObject { 44 | pub fn into_entries(self) -> Option> { 45 | match self { 46 | Self::Tree(tree) => Some(tree.entries), 47 | Self::Unknown => None, 48 | } 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use cynic::QueryBuilder; 55 | use indoc::indoc; 56 | 57 | use crate::github::{ 58 | github_client::{MICROSOFT, WINGET_PKGS}, 59 | graphql::get_directory_content::{GetDirectoryContent, GetDirectoryContentVariables}, 60 | }; 61 | 62 | #[test] 63 | fn get_directory_content_output() { 64 | const GET_DIRECTORY_CONTENT_QUERY: &str = indoc! {r#" 65 | query GetDirectoryContent($owner: String!, $name: String!, $expression: String!) { 66 | repository(owner: $owner, name: $name) { 67 | object(expression: $expression) { 68 | __typename 69 | ... on Tree { 70 | entries { 71 | path 72 | } 73 | } 74 | } 75 | } 76 | } 77 | "#}; 78 | 79 | let operation = GetDirectoryContent::build(GetDirectoryContentVariables { 80 | owner: MICROSOFT, 81 | name: WINGET_PKGS, 82 | expression: "", 83 | }); 84 | 85 | assert_eq!(operation.query, GET_DIRECTORY_CONTENT_QUERY); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/github/graphql/get_directory_content_with_text.rs: -------------------------------------------------------------------------------- 1 | use crate::github::graphql::{ 2 | get_directory_content::GetDirectoryContentVariablesFields, 3 | github_schema::github_schema as schema, 4 | }; 5 | 6 | #[derive(cynic::QueryFragment)] 7 | pub struct Tree { 8 | #[cynic(flatten)] 9 | pub entries: Vec, 10 | } 11 | 12 | #[derive(cynic::QueryFragment)] 13 | pub struct TreeEntry { 14 | pub name: String, 15 | pub object: Option, 16 | } 17 | 18 | #[derive(cynic::QueryFragment)] 19 | #[cynic(graphql_type = "Query", variables = "GetDirectoryContentVariables")] 20 | pub struct GetDirectoryContentWithText { 21 | #[arguments(owner: $owner, name: $name)] 22 | pub repository: Option, 23 | } 24 | 25 | #[derive(cynic::QueryFragment)] 26 | #[cynic(variables = "GetDirectoryContentVariables")] 27 | pub struct Repository { 28 | #[arguments(expression: $expression)] 29 | pub object: Option, 30 | } 31 | 32 | #[derive(cynic::QueryFragment)] 33 | pub struct Blob { 34 | pub text: Option, 35 | } 36 | 37 | #[derive(cynic::InlineFragments)] 38 | #[cynic(graphql_type = "GitObject")] 39 | pub enum BlobObject { 40 | Blob(Blob), 41 | #[cynic(fallback)] 42 | Unknown, 43 | } 44 | 45 | impl BlobObject { 46 | pub fn into_blob_text(self) -> Option { 47 | match self { 48 | Self::Blob(blob) => blob.text, 49 | Self::Unknown => None, 50 | } 51 | } 52 | } 53 | 54 | #[derive(cynic::InlineFragments)] 55 | #[cynic(graphql_type = "GitObject")] 56 | pub enum TreeObject { 57 | Tree(Tree), 58 | #[cynic(fallback)] 59 | Unknown, 60 | } 61 | 62 | impl TreeObject { 63 | pub fn into_tree_entries(self) -> Option> { 64 | match self { 65 | Self::Tree(tree) => Some(tree.entries), 66 | Self::Unknown => None, 67 | } 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use cynic::QueryBuilder; 74 | use indoc::indoc; 75 | 76 | use crate::github::{ 77 | github_client::{MICROSOFT, WINGET_PKGS}, 78 | graphql::{ 79 | get_directory_content::GetDirectoryContentVariables, 80 | get_directory_content_with_text::GetDirectoryContentWithText, 81 | }, 82 | }; 83 | 84 | #[test] 85 | fn get_directory_content_with_text_output() { 86 | const GET_DIRECTORY_CONTENT_WITH_TEXT_QUERY: &str = indoc! {r#" 87 | query GetDirectoryContentWithText($owner: String!, $name: String!, $expression: String!) { 88 | repository(owner: $owner, name: $name) { 89 | object(expression: $expression) { 90 | __typename 91 | ... on Tree { 92 | entries { 93 | name 94 | object { 95 | __typename 96 | ... on Blob { 97 | text 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | "#}; 106 | 107 | let operation = GetDirectoryContentWithText::build(GetDirectoryContentVariables { 108 | owner: MICROSOFT, 109 | name: WINGET_PKGS, 110 | expression: "", 111 | }); 112 | 113 | assert_eq!(operation.query, GET_DIRECTORY_CONTENT_WITH_TEXT_QUERY); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/github/graphql/get_existing_pull_request.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use url::Url; 3 | 4 | use crate::github::graphql::{ 5 | get_branches::PullRequestState, github_schema::github_schema as schema, 6 | }; 7 | 8 | #[derive(cynic::QueryVariables)] 9 | pub struct GetExistingPullRequestVariables<'a> { 10 | pub query: &'a str, 11 | } 12 | 13 | #[derive(cynic::QueryFragment)] 14 | #[cynic(graphql_type = "Query", variables = "GetExistingPullRequestVariables")] 15 | pub struct GetExistingPullRequest { 16 | #[arguments(first: 1, type: ISSUE, query: $query)] 17 | pub search: SearchResultItemConnection, 18 | } 19 | 20 | #[derive(cynic::QueryFragment)] 21 | pub struct SearchResultItemConnection { 22 | #[cynic(flatten)] 23 | pub edges: Vec, 24 | } 25 | 26 | #[derive(cynic::QueryFragment)] 27 | pub struct SearchResultItemEdge { 28 | pub node: Option, 29 | } 30 | 31 | #[derive(cynic::QueryFragment)] 32 | pub struct PullRequest { 33 | pub url: Url, 34 | pub state: PullRequestState, 35 | pub created_at: DateTime, 36 | } 37 | 38 | #[derive(cynic::InlineFragments)] 39 | pub enum SearchResultItem { 40 | PullRequest(PullRequest), 41 | #[cynic(fallback)] 42 | Unknown, 43 | } 44 | 45 | impl SearchResultItem { 46 | pub fn into_pull_request(self) -> Option { 47 | match self { 48 | Self::PullRequest(pull_request) => Some(pull_request), 49 | Self::Unknown => None, 50 | } 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use cynic::QueryBuilder; 57 | use indoc::indoc; 58 | 59 | use crate::github::graphql::get_existing_pull_request::{ 60 | GetExistingPullRequest, GetExistingPullRequestVariables, 61 | }; 62 | 63 | #[test] 64 | fn get_existing_pull_request_output() { 65 | const GET_EXISTING_PULL_REQUEST_QUERY: &str = indoc! {r#" 66 | query GetExistingPullRequest($query: String!) { 67 | search(first: 1, type: ISSUE, query: $query) { 68 | edges { 69 | node { 70 | __typename 71 | ... on PullRequest { 72 | url 73 | state 74 | createdAt 75 | } 76 | } 77 | } 78 | } 79 | } 80 | "#}; 81 | 82 | let operation = 83 | GetExistingPullRequest::build(GetExistingPullRequestVariables { query: "" }); 84 | 85 | assert_eq!(operation.query, GET_EXISTING_PULL_REQUEST_QUERY); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/github/graphql/get_file_content.rs: -------------------------------------------------------------------------------- 1 | use crate::github::graphql::{ 2 | get_directory_content::GetDirectoryContentVariablesFields, 3 | get_directory_content_with_text::BlobObject, github_schema::github_schema as schema, 4 | }; 5 | 6 | #[derive(cynic::QueryFragment)] 7 | #[cynic(graphql_type = "Query", variables = "GetDirectoryContentVariables")] 8 | pub struct GetFileContent { 9 | #[arguments(owner: $owner, name: $name)] 10 | pub repository: Option, 11 | } 12 | 13 | #[derive(cynic::QueryFragment)] 14 | #[cynic(variables = "GetDirectoryContentVariables")] 15 | pub struct Repository { 16 | #[arguments(expression: $expression)] 17 | pub object: Option, 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use cynic::QueryBuilder; 23 | use indoc::indoc; 24 | 25 | use crate::github::{ 26 | github_client::{MICROSOFT, WINGET_PKGS}, 27 | graphql::{ 28 | get_directory_content::GetDirectoryContentVariables, get_file_content::GetFileContent, 29 | }, 30 | }; 31 | 32 | #[test] 33 | fn get_file_content_output() { 34 | const GET_FILE_CONTENT_QUERY: &str = indoc! {r#" 35 | query GetFileContent($owner: String!, $name: String!, $expression: String!) { 36 | repository(owner: $owner, name: $name) { 37 | object(expression: $expression) { 38 | __typename 39 | ... on Blob { 40 | text 41 | } 42 | } 43 | } 44 | } 45 | "#}; 46 | 47 | let operation = GetFileContent::build(GetDirectoryContentVariables { 48 | owner: MICROSOFT, 49 | name: WINGET_PKGS, 50 | expression: "", 51 | }); 52 | 53 | assert_eq!(operation.query, GET_FILE_CONTENT_QUERY); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/github/graphql/get_repository_info.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | use crate::github::graphql::{github_schema::github_schema as schema, types::GitObjectId}; 4 | 5 | #[derive(cynic::QueryVariables)] 6 | pub struct RepositoryVariables<'a> { 7 | pub owner: &'a str, 8 | pub name: &'a str, 9 | } 10 | 11 | #[derive(cynic::QueryFragment)] 12 | #[cynic(graphql_type = "Query", variables = "RepositoryVariables")] 13 | pub struct GetRepositoryInfo { 14 | #[arguments(owner: $owner, name: $name)] 15 | pub repository: Option, 16 | } 17 | 18 | #[derive(cynic::QueryFragment)] 19 | pub struct Repository { 20 | pub id: cynic::Id, 21 | pub owner: RepositoryOwner, 22 | pub name_with_owner: String, 23 | pub url: Url, 24 | pub default_branch_ref: Option, 25 | } 26 | 27 | #[derive(cynic::QueryFragment)] 28 | pub struct Ref { 29 | pub name: String, 30 | pub id: cynic::Id, 31 | pub target: Option, 32 | } 33 | 34 | #[derive(cynic::QueryFragment)] 35 | pub struct RepositoryOwner { 36 | pub login: String, 37 | } 38 | 39 | #[derive(cynic::QueryFragment)] 40 | pub struct Commit { 41 | pub oid: GitObjectId, 42 | pub history: CommitHistoryConnection, 43 | } 44 | 45 | #[derive(cynic::QueryFragment)] 46 | pub struct CommitHistoryConnection { 47 | pub total_count: i32, 48 | } 49 | 50 | #[derive(cynic::InlineFragments)] 51 | #[cynic(graphql_type = "GitObject")] 52 | pub enum TargetGitObject { 53 | Commit(Commit), 54 | #[cynic(fallback)] 55 | Unknown, 56 | } 57 | 58 | impl TargetGitObject { 59 | pub fn into_commit(self) -> Option { 60 | match self { 61 | Self::Commit(commit) => Some(commit), 62 | Self::Unknown => None, 63 | } 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use cynic::QueryBuilder; 70 | use indoc::indoc; 71 | 72 | use crate::github::{ 73 | github_client::{MICROSOFT, WINGET_PKGS}, 74 | graphql::get_repository_info::{GetRepositoryInfo, RepositoryVariables}, 75 | }; 76 | 77 | #[test] 78 | fn get_repository_info_output() { 79 | const GET_REPOSITORY_INFO_QUERY: &str = indoc! {r#" 80 | query GetRepositoryInfo($owner: String!, $name: String!) { 81 | repository(owner: $owner, name: $name) { 82 | id 83 | owner { 84 | login 85 | } 86 | nameWithOwner 87 | url 88 | defaultBranchRef { 89 | name 90 | id 91 | target { 92 | __typename 93 | ... on Commit { 94 | oid 95 | history { 96 | totalCount 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | "#}; 104 | 105 | let operation = GetRepositoryInfo::build(RepositoryVariables { 106 | owner: MICROSOFT, 107 | name: WINGET_PKGS, 108 | }); 109 | 110 | assert_eq!(operation.query, GET_REPOSITORY_INFO_QUERY); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/github/graphql/github_schema.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use cynic::impl_scalar; 3 | use url::Url; 4 | 5 | #[cynic::schema("github")] 6 | pub mod github_schema {} 7 | 8 | impl_scalar!(Url, github_schema::URI); 9 | impl_scalar!(DateTime, github_schema::DateTime); 10 | -------------------------------------------------------------------------------- /src/github/graphql/merge_upstream.rs: -------------------------------------------------------------------------------- 1 | use crate::github::graphql::{github_schema::github_schema as schema, types::GitObjectId}; 2 | 3 | #[derive(cynic::QueryVariables)] 4 | pub struct MergeUpstreamVariables<'id> { 5 | pub branch_ref_id: &'id cynic::Id, 6 | pub upstream_target_oid: GitObjectId, 7 | pub force: bool, 8 | } 9 | 10 | #[derive(cynic::QueryFragment)] 11 | #[cynic(graphql_type = "Mutation", variables = "MergeUpstreamVariables")] 12 | pub struct MergeUpstream { 13 | #[expect(dead_code)] 14 | #[arguments(input: { oid: $upstream_target_oid, refId: $branch_ref_id, force: $force })] 15 | pub update_ref: Option, 16 | } 17 | 18 | #[derive(cynic::QueryFragment)] 19 | pub struct UpdateRefPayload { 20 | #[expect(dead_code)] 21 | pub client_mutation_id: Option, 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use cynic::{Id, MutationBuilder}; 27 | use indoc::indoc; 28 | 29 | use crate::github::graphql::{ 30 | merge_upstream::{MergeUpstream, MergeUpstreamVariables}, 31 | types::GitObjectId, 32 | }; 33 | 34 | #[test] 35 | fn merge_upstream_output() { 36 | const MERGE_UPSTREAM_MUTATION: &str = indoc! {" 37 | mutation MergeUpstream($branchRefId: ID!, $upstreamTargetOid: GitObjectID!, $force: Boolean!) { 38 | updateRef(input: {oid: $upstreamTargetOid, refId: $branchRefId, force: $force}) { 39 | clientMutationId 40 | } 41 | } 42 | "}; 43 | 44 | let id = Id::new(""); 45 | let operation = MergeUpstream::build(MergeUpstreamVariables { 46 | branch_ref_id: &id, 47 | upstream_target_oid: GitObjectId::new(""), 48 | force: false, 49 | }); 50 | 51 | assert_eq!(operation.query, MERGE_UPSTREAM_MUTATION); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/github/graphql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_commit; 2 | pub mod create_pull_request; 3 | pub mod create_ref; 4 | pub mod get_all_values; 5 | pub mod get_branches; 6 | pub mod get_current_user_login; 7 | pub mod get_directory_content; 8 | pub mod get_directory_content_with_text; 9 | pub mod get_existing_pull_request; 10 | pub mod get_file_content; 11 | pub mod get_repository_info; 12 | pub mod github_schema; 13 | pub mod merge_upstream; 14 | pub mod types; 15 | pub mod update_refs; 16 | -------------------------------------------------------------------------------- /src/github/graphql/types.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Deref; 2 | use derive_new::new; 3 | 4 | use crate::github::graphql::github_schema::github_schema as schema; 5 | 6 | /// 7 | #[derive(cynic::Scalar, new)] 8 | pub struct Base64String(#[new(into)] String); 9 | 10 | /// 11 | #[derive(cynic::Scalar, PartialEq, Eq, Clone, new)] 12 | #[cynic(graphql_type = "GitObjectID")] 13 | pub struct GitObjectId(#[new(into)] String); 14 | 15 | /// 16 | #[derive(cynic::Scalar, new)] 17 | #[cynic(graphql_type = "GitRefname")] 18 | pub struct GitRefName(#[new(into)] String); 19 | 20 | /// 21 | #[derive(cynic::Scalar, Deref)] 22 | #[cynic(graphql_type = "HTML")] 23 | pub struct Html(String); 24 | -------------------------------------------------------------------------------- /src/github/graphql/update_refs.rs: -------------------------------------------------------------------------------- 1 | use crate::github::graphql::{ 2 | github_schema::github_schema as schema, 3 | types::{GitObjectId, GitRefName}, 4 | }; 5 | 6 | /// 7 | #[derive(cynic::QueryVariables)] 8 | pub struct UpdateRefsVariables<'id> { 9 | pub ref_updates: Vec, 10 | pub repository_id: &'id cynic::Id, 11 | } 12 | 13 | #[derive(cynic::QueryFragment)] 14 | #[cynic(graphql_type = "Mutation", variables = "UpdateRefsVariables")] 15 | pub struct UpdateRefs { 16 | #[expect(dead_code)] 17 | #[arguments(input: { refUpdates: $ref_updates, repositoryId: $repository_id })] 18 | pub update_refs: Option, 19 | } 20 | 21 | #[derive(cynic::QueryFragment)] 22 | pub struct UpdateRefsPayload { 23 | #[expect(dead_code)] 24 | pub client_mutation_id: Option, 25 | } 26 | 27 | /// 28 | #[derive(cynic::InputObject)] 29 | pub struct RefUpdate { 30 | pub after_oid: GitObjectId, 31 | #[cynic(skip_serializing_if = "Option::is_none")] 32 | pub before_oid: Option, 33 | #[cynic(skip_serializing_if = "Option::is_none")] 34 | pub force: Option, 35 | pub name: GitRefName, 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use cynic::{Id, MutationBuilder}; 41 | use indoc::indoc; 42 | 43 | use crate::github::graphql::update_refs::{UpdateRefs, UpdateRefsVariables}; 44 | 45 | #[test] 46 | fn create_ref_output() { 47 | const UPDATE_REFS_MUTATION: &str = indoc! {" 48 | mutation UpdateRefs($refUpdates: [RefUpdate!]!, $repositoryId: ID!) { 49 | updateRefs(input: {refUpdates: $refUpdates, repositoryId: $repositoryId}) { 50 | clientMutationId 51 | } 52 | } 53 | "}; 54 | 55 | let id = Id::new(""); 56 | let operation = UpdateRefs::build(UpdateRefsVariables { 57 | repository_id: &id, 58 | ref_updates: vec![], 59 | }); 60 | 61 | assert_eq!(operation.query, UPDATE_REFS_MUTATION); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/github/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod github_client; 2 | pub mod graphql; 3 | mod rest; 4 | pub mod utils; 5 | -------------------------------------------------------------------------------- /src/github/rest/get_tree.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub struct GitTree { 5 | pub sha: String, 6 | pub url: String, 7 | pub truncated: bool, 8 | pub tree: Vec, 9 | } 10 | 11 | #[derive(Serialize, Deserialize)] 12 | pub struct TreeObject { 13 | pub path: String, 14 | pub mode: String, 15 | pub r#type: String, 16 | pub sha: String, 17 | pub size: Option, 18 | pub url: String, 19 | } 20 | -------------------------------------------------------------------------------- /src/github/rest/mod.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::HeaderValue; 2 | 3 | pub mod get_tree; 4 | 5 | pub const GITHUB_JSON_MIME: HeaderValue = HeaderValue::from_static("application/vnd.github+json"); 6 | -------------------------------------------------------------------------------- /src/github/utils/package_path.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Write}; 2 | 3 | use winget_types::{ManifestTypeWithLocale, PackageIdentifier, PackageVersion}; 4 | 5 | use super::{INSTALLER_PART, LOCALE_PART, YAML_EXTENSION}; 6 | 7 | #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 8 | #[repr(transparent)] 9 | pub struct PackagePath(String); 10 | 11 | impl PackagePath { 12 | pub fn new( 13 | identifier: &PackageIdentifier, 14 | version: Option<&PackageVersion>, 15 | manifest_type: Option<&ManifestTypeWithLocale>, 16 | ) -> Self { 17 | let first_character = identifier.as_str().chars().next().map_or_else( 18 | || unreachable!("Package identifiers cannot be empty"), 19 | |mut first| { 20 | first.make_ascii_lowercase(); 21 | first 22 | }, 23 | ); 24 | 25 | // manifests/p 26 | let mut result = format!("manifests/{first_character}"); 27 | 28 | // manifests/p/Package/Identifier 29 | for part in identifier.as_str().split('.') { 30 | let _ = write!(result, "/{part}"); 31 | } 32 | 33 | // manifests/p/Package/Identifier/1.2.3 34 | if let Some(version) = version { 35 | let _ = write!(result, "/{version}"); 36 | 37 | // The full manifest file path should only be included if a version was passed in too 38 | if let Some(manifest_type) = manifest_type { 39 | let _ = write!(result, "/{identifier}"); 40 | if matches!(manifest_type, ManifestTypeWithLocale::Installer) { 41 | // manifests/p/Package/Identifier/1.2.3/Package.Identifier.installer.yaml 42 | result.push_str(INSTALLER_PART); 43 | } else if let ManifestTypeWithLocale::Locale(tag) = manifest_type { 44 | let _ = write!(result, "{LOCALE_PART}{tag}"); 45 | } 46 | result.push_str(YAML_EXTENSION); 47 | } 48 | } 49 | 50 | Self(result) 51 | } 52 | 53 | pub fn as_str(&self) -> &str { 54 | self.0.as_str() 55 | } 56 | } 57 | 58 | impl Display for PackagePath { 59 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 60 | self.0.fmt(f) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use rstest::rstest; 67 | use winget_types::{ManifestTypeWithLocale, PackageIdentifier, icu_locid::langid}; 68 | 69 | use super::PackagePath; 70 | 71 | #[rstest] 72 | #[case("Package.Identifier", None, None, "manifests/p/Package/Identifier")] 73 | #[case( 74 | "Package.Identifier", 75 | Some("1.2.3"), 76 | None, 77 | "manifests/p/Package/Identifier/1.2.3" 78 | )] 79 | #[case( 80 | "Package.Identifier", 81 | Some("1.2.3"), 82 | Some(ManifestTypeWithLocale::Installer), 83 | "manifests/p/Package/Identifier/1.2.3/Package.Identifier.installer.yaml" 84 | )] 85 | #[case( 86 | "Package.Identifier", 87 | Some("1.2.3"), 88 | Some(ManifestTypeWithLocale::Locale(langid!("en-US"))), 89 | "manifests/p/Package/Identifier/1.2.3/Package.Identifier.locale.en-US.yaml" 90 | )] 91 | #[case( 92 | "Package.Identifier", 93 | Some("1.2.3"), 94 | Some(ManifestTypeWithLocale::Locale(langid!("zh-CN"))), 95 | "manifests/p/Package/Identifier/1.2.3/Package.Identifier.locale.zh-CN.yaml" 96 | )] 97 | #[case( 98 | "Package.Identifier", 99 | Some("1.2.3"), 100 | Some(ManifestTypeWithLocale::Version), 101 | "manifests/p/Package/Identifier/1.2.3/Package.Identifier.yaml" 102 | )] 103 | fn package_paths( 104 | #[case] identifier: &str, 105 | #[case] version: Option<&str>, 106 | #[case] manifest_type: Option, 107 | #[case] expected: &str, 108 | ) { 109 | let identifier = identifier.parse::().unwrap(); 110 | let version = version.and_then(|version| version.parse().ok()); 111 | assert_eq!( 112 | PackagePath::new(&identifier, version.as_ref(), manifest_type.as_ref()).as_str(), 113 | expected 114 | ) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/github/utils/pull_request.rs: -------------------------------------------------------------------------------- 1 | use bon::builder; 2 | use color_eyre::Result; 3 | use winget_types::PackageIdentifier; 4 | 5 | use crate::{ 6 | github::utils::PackagePath, 7 | manifests::{Manifests, build_manifest_string}, 8 | }; 9 | 10 | #[builder(finish_fn = create)] 11 | pub fn pr_changes( 12 | package_identifier: &PackageIdentifier, 13 | manifests: &Manifests, 14 | package_path: &PackagePath, 15 | created_with: Option<&str>, 16 | ) -> Result> { 17 | let mut path_content_map = vec![ 18 | ( 19 | format!("{package_path}/{package_identifier}.installer.yaml"), 20 | build_manifest_string(&manifests.installer, created_with)?, 21 | ), 22 | ( 23 | format!( 24 | "{}/{}.locale.{}.yaml", 25 | package_path, package_identifier, manifests.version.default_locale 26 | ), 27 | build_manifest_string(&manifests.default_locale, created_with)?, 28 | ), 29 | ]; 30 | for locale_manifest in &manifests.locales { 31 | path_content_map.push(( 32 | format!( 33 | "{package_path}/{package_identifier}.locale.{}.yaml", 34 | locale_manifest.package_locale 35 | ), 36 | build_manifest_string(locale_manifest, created_with)?, 37 | )); 38 | } 39 | path_content_map.push(( 40 | format!("{package_path}/{package_identifier}.yaml"), 41 | build_manifest_string(&manifests.version, created_with)?, 42 | )); 43 | Ok(path_content_map) 44 | } 45 | -------------------------------------------------------------------------------- /src/installers/burn/wix_burn_stub.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use zerocopy::{Immutable, KnownLayout, TryFromBytes, little_endian::U32}; 4 | 5 | #[expect(dead_code)] 6 | #[derive(Debug, TryFromBytes, KnownLayout, Immutable)] 7 | #[repr(u32)] 8 | enum WixBurnStubMagic { 9 | F14300 = 0x00F1_4300_u32.to_le(), 10 | } 11 | 12 | /// 13 | #[derive(Debug, TryFromBytes, KnownLayout, Immutable)] 14 | #[repr(C)] 15 | pub struct WixBurnStub { 16 | magic: WixBurnStubMagic, 17 | version: U32, 18 | guid: uuid::Bytes, 19 | stub_size: U32, 20 | original_checksum: U32, 21 | original_signature_offset: U32, 22 | original_signature_size: U32, 23 | container_format: U32, 24 | container_count: U32, 25 | bootstrapper_application_container_size: U32, 26 | // (512 (minimum section size) - 52 (size of above data)) / 4 (size of DWORD) 27 | attached_container_sizes: [U32; 115], 28 | } 29 | 30 | impl WixBurnStub { 31 | pub const fn ux_container_slice_range(&self) -> Range { 32 | let stub_size = self.stub_size.get() as usize; 33 | stub_size..stub_size + self.bootstrapper_application_container_size.get() as usize 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use crate::installers::burn::wix_burn_stub::WixBurnStub; 40 | 41 | #[test] 42 | fn wix_burn_stub_size() { 43 | const MINIMUM_PE_SECTION_SIZE: usize = 512; 44 | 45 | assert_eq!(size_of::(), MINIMUM_PE_SECTION_SIZE) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/installers/inno/compression.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | #[derive(Debug)] 4 | pub enum Compression { 5 | Stored(u32), 6 | Zlib(u32), 7 | LZMA1(u32), 8 | } 9 | 10 | impl Deref for Compression { 11 | type Target = u32; 12 | 13 | fn deref(&self) -> &Self::Target { 14 | match self { 15 | Self::Stored(size) | Self::Zlib(size) | Self::LZMA1(size) => size, 16 | } 17 | } 18 | } 19 | 20 | impl DerefMut for Compression { 21 | fn deref_mut(&mut self) -> &mut Self::Target { 22 | match self { 23 | Self::Stored(size) | Self::Zlib(size) | Self::LZMA1(size) => size, 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/installers/inno/encoding.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use byteorder::{LE, ReadBytesExt}; 4 | use encoding_rs::Encoding; 5 | 6 | #[derive(Debug, Default)] 7 | pub struct InnoValue(Vec); 8 | 9 | impl InnoValue { 10 | pub fn new_raw(reader: &mut R) -> Result>> { 11 | let length = reader.read_u32::()?; 12 | if length == 0 { 13 | return Ok(None); 14 | } 15 | let mut buf = vec![0; length as usize]; 16 | reader.read_exact(&mut buf)?; 17 | Ok(Some(buf)) 18 | } 19 | 20 | pub fn new_encoded(reader: &mut R) -> Result> { 21 | Self::new_raw(reader).map(|opt_bytes| opt_bytes.map(Self)) 22 | } 23 | 24 | pub fn new_string( 25 | reader: &mut R, 26 | codepage: &'static Encoding, 27 | ) -> Result> { 28 | Self::new_encoded(reader) 29 | .map(|opt_value| opt_value.map(|value| value.into_string(codepage))) 30 | } 31 | 32 | pub fn new_sized_string( 33 | reader: &mut R, 34 | length: u32, 35 | codepage: &'static Encoding, 36 | ) -> Result> { 37 | if length == 0 { 38 | return Ok(None); 39 | } 40 | let mut buf = vec![0; length as usize]; 41 | reader.read_exact(&mut buf)?; 42 | Ok(Some(codepage.decode(&buf).0.into_owned())) 43 | } 44 | 45 | pub fn into_string(self, codepage: &'static Encoding) -> String { 46 | codepage.decode(&self.0).0.into_owned() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/installers/inno/entry/component.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bitflags::bitflags; 4 | use byteorder::{LE, ReadBytesExt}; 5 | use encoding_rs::Encoding; 6 | 7 | use crate::installers::inno::{ 8 | encoding::InnoValue, version::InnoVersion, windows_version::WindowsVersionRange, 9 | }; 10 | 11 | #[expect(dead_code)] 12 | #[derive(Debug, Default)] 13 | pub struct Component { 14 | name: Option, 15 | description: Option, 16 | types: Option, 17 | languages: Option, 18 | check: Option, 19 | extra_disk_space_required: u64, 20 | level: u32, 21 | used: bool, 22 | flags: ComponentFlags, 23 | size: u64, 24 | } 25 | 26 | impl Component { 27 | pub fn from_reader( 28 | reader: &mut R, 29 | codepage: &'static Encoding, 30 | version: &InnoVersion, 31 | ) -> Result { 32 | let mut component = Self { 33 | name: InnoValue::new_string(reader, codepage)?, 34 | description: InnoValue::new_string(reader, codepage)?, 35 | types: InnoValue::new_string(reader, codepage)?, 36 | ..Self::default() 37 | }; 38 | 39 | if *version >= (4, 0, 1) { 40 | component.languages = InnoValue::new_string(reader, codepage)?; 41 | } 42 | 43 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (1, 3, 24)) { 44 | component.check = InnoValue::new_string(reader, codepage)?; 45 | } 46 | 47 | if *version >= (4, 0, 0) { 48 | component.extra_disk_space_required = reader.read_u64::()?; 49 | } else { 50 | component.extra_disk_space_required = u64::from(reader.read_u32::()?); 51 | } 52 | 53 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (3, 0, 3)) { 54 | component.level = reader.read_u32::()?; 55 | } 56 | 57 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (3, 0, 4)) { 58 | component.used = reader.read_u8()? != 0; 59 | } else { 60 | component.used = true; 61 | } 62 | 63 | WindowsVersionRange::from_reader(reader, version)?; 64 | 65 | component.flags = ComponentFlags::from_bits_retain(reader.read_u8()?); 66 | 67 | if *version >= (4, 0, 0) { 68 | component.size = reader.read_u64::()?; 69 | } else if *version >= (2, 0, 0) || (version.is_isx() && *version >= (1, 3, 24)) { 70 | component.size = u64::from(reader.read_u32::()?); 71 | } 72 | 73 | Ok(component) 74 | } 75 | } 76 | 77 | bitflags! { 78 | #[derive(Debug, Default)] 79 | pub struct ComponentFlags: u8 { 80 | const FIXED = 1 << 0; 81 | const RESTART = 1 << 1; 82 | const DISABLE_NO_UNINSTALL_WARNING = 1 << 2; 83 | const EXCLUSIVE = 1 << 3; 84 | const DONT_INHERIT_CHECK = 1 << 4; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/installers/inno/entry/condition.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use encoding_rs::Encoding; 4 | 5 | use crate::installers::inno::{encoding::InnoValue, version::InnoVersion}; 6 | 7 | #[derive(Debug, Default)] 8 | pub struct Condition { 9 | components: Option, 10 | tasks: Option, 11 | languages: Option, 12 | check: Option, 13 | after_install: Option, 14 | before_install: Option, 15 | } 16 | 17 | impl Condition { 18 | pub fn from_reader( 19 | reader: &mut R, 20 | codepage: &'static Encoding, 21 | version: &InnoVersion, 22 | ) -> Result { 23 | let mut condition = Self::default(); 24 | 25 | if *version >= (2, 0, 0) || (version.is_isx() && *version >= (1, 3, 8)) { 26 | condition.components = InnoValue::new_string(reader, codepage)?; 27 | } 28 | 29 | if *version >= (2, 0, 0) || (version.is_isx() && *version >= (1, 3, 17)) { 30 | condition.tasks = InnoValue::new_string(reader, codepage)?; 31 | } 32 | 33 | if *version >= (4, 0, 1) { 34 | condition.languages = InnoValue::new_string(reader, codepage)?; 35 | } 36 | 37 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (1, 3, 24)) { 38 | condition.check = InnoValue::new_string(reader, codepage)?; 39 | } 40 | 41 | if *version >= (4, 1, 0) { 42 | condition.after_install = InnoValue::new_string(reader, codepage)?; 43 | condition.before_install = InnoValue::new_string(reader, codepage)?; 44 | } 45 | 46 | Ok(condition) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/installers/inno/entry/directory.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bitflags::bitflags; 4 | use byteorder::{LE, ReadBytesExt}; 5 | use encoding_rs::Encoding; 6 | 7 | use crate::installers::inno::{ 8 | encoding::InnoValue, entry::condition::Condition, version::InnoVersion, 9 | windows_version::WindowsVersionRange, 10 | }; 11 | 12 | #[expect(dead_code)] 13 | #[derive(Debug, Default)] 14 | pub struct Directory { 15 | name: Option, 16 | permissions: Option, 17 | attributes: u32, 18 | /// Index into the permission entry list 19 | permission: i16, 20 | flags: DirectoryFlags, 21 | } 22 | 23 | impl Directory { 24 | pub fn from_reader( 25 | reader: &mut R, 26 | codepage: &'static Encoding, 27 | version: &InnoVersion, 28 | ) -> Result { 29 | if *version < (1, 3, 0) { 30 | let _uncompressed_size = reader.read_u32::()?; 31 | } 32 | 33 | let mut directory = Self { 34 | name: InnoValue::new_string(reader, codepage)?, 35 | permission: -1, 36 | ..Self::default() 37 | }; 38 | 39 | Condition::from_reader(reader, codepage, version)?; 40 | 41 | if *version >= (4, 0, 11) && *version < (4, 1, 0) { 42 | directory.permissions = InnoValue::new_string(reader, codepage)?; 43 | } 44 | 45 | if *version >= (2, 0, 11) { 46 | directory.attributes = reader.read_u32::()?; 47 | } 48 | 49 | WindowsVersionRange::from_reader(reader, version)?; 50 | 51 | if *version >= (4, 1, 0) { 52 | directory.permission = reader.read_i16::()?; 53 | } 54 | 55 | directory.flags = DirectoryFlags::from_bits_retain(reader.read_u8()?); 56 | 57 | Ok(directory) 58 | } 59 | } 60 | 61 | bitflags! { 62 | #[derive(Debug, Default)] 63 | pub struct DirectoryFlags: u8 { 64 | const NEVER_UNINSTALL = 1 << 0; 65 | const DELETE_AFTER_INSTALL = 1 << 1; 66 | const ALWAYS_UNINSTALL = 1 << 2; 67 | const SET_NTFS_COMPRESSION = 1 << 3; 68 | const UNSET_NTFS_COMPRESSION = 1 << 4; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/installers/inno/entry/icon.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bitflags::bitflags; 4 | use byteorder::{LE, ReadBytesExt}; 5 | use encoding_rs::Encoding; 6 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 7 | 8 | use crate::installers::inno::{ 9 | encoding::InnoValue, entry::condition::Condition, enum_value::enum_value::enum_value, 10 | flag_reader::read_flags::read_flags, version::InnoVersion, 11 | windows_version::WindowsVersionRange, 12 | }; 13 | 14 | #[expect(dead_code)] 15 | #[derive(Debug, Default)] 16 | pub struct Icon { 17 | name: Option, 18 | filename: Option, 19 | parameters: Option, 20 | working_directory: Option, 21 | file: Option, 22 | comment: Option, 23 | app_user_model_id: Option, 24 | app_user_model_toast_activator_clsid: String, 25 | index: i32, 26 | show_command: i32, 27 | close_on_exit: CloseSetting, 28 | hotkey: u16, 29 | flags: IconFlags, 30 | } 31 | 32 | impl Icon { 33 | pub fn from_reader( 34 | reader: &mut R, 35 | codepage: &'static Encoding, 36 | version: &InnoVersion, 37 | ) -> Result { 38 | if *version < (1, 3, 0) { 39 | let _uncompressed_size = reader.read_u32::()?; 40 | } 41 | 42 | let mut icon = Self { 43 | name: InnoValue::new_string(reader, codepage)?, 44 | filename: InnoValue::new_string(reader, codepage)?, 45 | parameters: InnoValue::new_string(reader, codepage)?, 46 | working_directory: InnoValue::new_string(reader, codepage)?, 47 | file: InnoValue::new_string(reader, codepage)?, 48 | comment: InnoValue::new_string(reader, codepage)?, 49 | ..Self::default() 50 | }; 51 | 52 | Condition::from_reader(reader, codepage, version)?; 53 | 54 | if *version >= (5, 3, 5) { 55 | icon.app_user_model_id = InnoValue::new_string(reader, codepage)?; 56 | } 57 | 58 | if *version >= (6, 1, 0) { 59 | let mut buf = [0; 16]; 60 | reader.read_exact(&mut buf)?; 61 | icon.app_user_model_toast_activator_clsid = codepage.decode(&buf).0.into_owned(); 62 | } 63 | 64 | WindowsVersionRange::from_reader(reader, version)?; 65 | 66 | icon.index = reader.read_i32::()?; 67 | 68 | icon.show_command = if *version >= (1, 3, 24) { 69 | reader.read_i32::()? 70 | } else { 71 | 1 72 | }; 73 | 74 | if *version >= (1, 3, 15) { 75 | icon.close_on_exit = enum_value!(reader, CloseSetting)?; 76 | } 77 | 78 | if *version >= (2, 0, 7) { 79 | icon.hotkey = reader.read_u16::()?; 80 | } 81 | 82 | icon.flags = read_flags!(reader, 83 | IconFlags::NEVER_UNINSTALL, 84 | if *version < (1, 3, 26) => IconFlags::RUN_MINIMIZED, 85 | [IconFlags::CREATE_ONLY_IF_FILE_EXISTS, IconFlags::USE_APP_PATHS], 86 | if *version >= (5, 0, 3) && *version < (6, 3, 0) => IconFlags::FOLDER_SHORTCUT, 87 | if *version >= (5, 4, 2) => IconFlags::EXCLUDE_FROM_SHOW_IN_NEW_INSTALL, 88 | if *version >= (5, 5, 0) => IconFlags::PREVENT_PINNING, 89 | if *version >= (6, 1, 0) => IconFlags::HAS_APP_USER_MODEL_TOAST_ACTIVATOR_CLSID 90 | )?; 91 | 92 | Ok(icon) 93 | } 94 | } 95 | 96 | #[expect(dead_code)] 97 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 98 | #[repr(u8)] 99 | enum CloseSetting { 100 | #[default] 101 | NoSetting, 102 | CloseOnExit, 103 | DontCloseOnExit, 104 | } 105 | 106 | bitflags! { 107 | #[derive(Debug, Default)] 108 | pub struct IconFlags: u8 { 109 | const NEVER_UNINSTALL = 1 << 0; 110 | const CREATE_ONLY_IF_FILE_EXISTS = 1 << 1; 111 | const USE_APP_PATHS = 1 << 2; 112 | const FOLDER_SHORTCUT = 1 << 3; 113 | const EXCLUDE_FROM_SHOW_IN_NEW_INSTALL = 1 << 4; 114 | const PREVENT_PINNING = 1 << 5; 115 | const HAS_APP_USER_MODEL_TOAST_ACTIVATOR_CLSID = 1 << 6; 116 | const RUN_MINIMIZED = 1 << 7; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/installers/inno/entry/ini.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bitflags::bitflags; 4 | use byteorder::{LE, ReadBytesExt}; 5 | use encoding_rs::Encoding; 6 | 7 | use crate::installers::inno::{ 8 | encoding::InnoValue, entry::condition::Condition, version::InnoVersion, 9 | windows_version::WindowsVersionRange, 10 | }; 11 | 12 | #[expect(dead_code)] 13 | #[derive(Debug, Default)] 14 | pub struct Ini { 15 | file: String, 16 | section: Option, 17 | key: Option, 18 | value: Option, 19 | flags: IniFlags, 20 | } 21 | 22 | impl Ini { 23 | const DEFAULT_FILE: &'static str = "{windows}/WIN.INI"; 24 | 25 | pub fn from_reader( 26 | reader: &mut R, 27 | codepage: &'static Encoding, 28 | version: &InnoVersion, 29 | ) -> Result { 30 | if *version < (1, 3, 0) { 31 | let _uncompressed_size = reader.read_u32::()?; 32 | } 33 | 34 | let mut ini = Self { 35 | file: InnoValue::new_string(reader, codepage)? 36 | .unwrap_or_else(|| Self::DEFAULT_FILE.to_string()), 37 | section: InnoValue::new_string(reader, codepage)?, 38 | key: InnoValue::new_string(reader, codepage)?, 39 | value: InnoValue::new_string(reader, codepage)?, 40 | ..Self::default() 41 | }; 42 | 43 | Condition::from_reader(reader, codepage, version)?; 44 | 45 | WindowsVersionRange::from_reader(reader, version)?; 46 | 47 | ini.flags = IniFlags::from_bits_retain(reader.read_u8()?); 48 | 49 | Ok(ini) 50 | } 51 | } 52 | 53 | bitflags! { 54 | #[derive(Debug, Default)] 55 | pub struct IniFlags: u8 { 56 | const CREATE_KEY_IF_DOESNT_EXIST = 1 << 0; 57 | const UNINSTALL_DELETE_ENTRY = 1 << 1; 58 | const UNINSTALL_DELETE_ENTIRE_SECTION = 1 << 2; 59 | const UNINSTALL_DELETE_SECTION_IF_EMPTY = 1 << 3; 60 | const HAS_VALUE = 1 << 4; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/installers/inno/entry/language.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use byteorder::{LE, ReadBytesExt}; 4 | use encoding_rs::{Encoding, UTF_16LE, WINDOWS_1252}; 5 | 6 | use crate::installers::inno::{encoding::InnoValue, version::InnoVersion}; 7 | 8 | #[derive(Debug)] 9 | pub struct Language { 10 | internal_name: Option, 11 | name: Option, 12 | dialog_font: Option, 13 | title_font: Option, 14 | welcome_font: Option, 15 | copyright_font: Option, 16 | data: Option, 17 | license_text: Option, 18 | info_before: Option, 19 | info_after: Option, 20 | pub id: u32, 21 | pub codepage: &'static Encoding, 22 | dialog_font_size: u32, 23 | dialog_font_standard_height: u32, 24 | title_font_size: u32, 25 | welcome_font_size: u32, 26 | copyright_font_size: u32, 27 | right_to_left: bool, 28 | } 29 | 30 | impl Language { 31 | pub fn from_reader( 32 | reader: &mut R, 33 | codepage: &'static Encoding, 34 | version: &InnoVersion, 35 | ) -> Result { 36 | let mut language = Self::default(); 37 | 38 | if *version >= (4, 0, 0) { 39 | language.internal_name = InnoValue::new_string(reader, codepage)?; 40 | } 41 | 42 | language.name = InnoValue::new_string(reader, codepage)?; 43 | language.dialog_font = InnoValue::new_string(reader, codepage)?; 44 | language.title_font = InnoValue::new_string(reader, codepage)?; 45 | language.welcome_font = InnoValue::new_string(reader, codepage)?; 46 | language.copyright_font = InnoValue::new_string(reader, codepage)?; 47 | 48 | if *version >= (4, 0, 0) { 49 | language.data = InnoValue::new_string(reader, codepage)?; 50 | } 51 | 52 | if *version >= (4, 0, 1) { 53 | language.license_text = InnoValue::new_string(reader, codepage)?; 54 | language.info_before = InnoValue::new_string(reader, codepage)?; 55 | language.info_after = InnoValue::new_string(reader, codepage)?; 56 | } 57 | 58 | language.id = reader.read_u32::()?; 59 | 60 | if *version < (4, 2, 2) { 61 | language.codepage = u16::try_from(language.id) 62 | .ok() 63 | .and_then(codepage::to_encoding) 64 | .unwrap_or(WINDOWS_1252); 65 | } else if !version.is_unicode() { 66 | let codepage = reader.read_u32::()?; 67 | language.codepage = (codepage != 0) 68 | .then(|| u16::try_from(codepage).ok().and_then(codepage::to_encoding)) 69 | .flatten() 70 | .unwrap_or(WINDOWS_1252); 71 | } else { 72 | if *version < (5, 3, 0) { 73 | reader.read_u32::()?; 74 | } 75 | language.codepage = UTF_16LE; 76 | } 77 | 78 | language.dialog_font_size = reader.read_u32::()?; 79 | 80 | if *version < (4, 1, 0) { 81 | language.dialog_font_standard_height = reader.read_u32::()?; 82 | } 83 | 84 | language.title_font_size = reader.read_u32::()?; 85 | language.welcome_font_size = reader.read_u32::()?; 86 | language.copyright_font_size = reader.read_u32::()?; 87 | 88 | if *version >= (5, 2, 3) { 89 | language.right_to_left = reader.read_u8()? != 0; 90 | } 91 | 92 | Ok(language) 93 | } 94 | } 95 | 96 | impl Default for Language { 97 | fn default() -> Self { 98 | Self { 99 | internal_name: None, 100 | name: None, 101 | dialog_font: None, 102 | title_font: None, 103 | welcome_font: None, 104 | copyright_font: None, 105 | data: None, 106 | license_text: None, 107 | info_before: None, 108 | info_after: None, 109 | id: 0, 110 | codepage: WINDOWS_1252, 111 | dialog_font_size: 0, 112 | dialog_font_standard_height: 0, 113 | title_font_size: 0, 114 | welcome_font_size: 0, 115 | copyright_font_size: 0, 116 | right_to_left: false, 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/installers/inno/entry/message.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use byteorder::{LE, ReadBytesExt}; 4 | use encoding_rs::Encoding; 5 | 6 | use crate::installers::inno::{encoding::InnoValue, entry::language::Language}; 7 | 8 | #[expect(dead_code)] 9 | #[derive(Debug, Default)] 10 | pub struct Message { 11 | name: String, 12 | value: String, 13 | language_index: i32, 14 | } 15 | 16 | impl Message { 17 | pub fn from_reader( 18 | reader: &mut R, 19 | languages: &[Language], 20 | codepage: &'static Encoding, 21 | ) -> Result { 22 | let mut message = Self { 23 | name: InnoValue::new_string(reader, codepage)?.unwrap_or_default(), 24 | ..Self::default() 25 | }; 26 | 27 | let value = InnoValue::new_encoded(reader)?.unwrap_or_default(); 28 | 29 | message.language_index = reader.read_i32::()?; 30 | 31 | let mut codepage = codepage; 32 | if message.language_index >= 0 { 33 | if let Some(language) = usize::try_from(message.language_index) 34 | .ok() 35 | .and_then(|index| languages.get(index)) 36 | { 37 | codepage = language.codepage; 38 | } 39 | } 40 | 41 | message.value = value.into_string(codepage); 42 | 43 | Ok(message) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/installers/inno/entry/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod component; 2 | mod condition; 3 | pub mod directory; 4 | pub mod file; 5 | pub mod icon; 6 | pub mod ini; 7 | pub mod language; 8 | pub mod message; 9 | pub mod permission; 10 | pub mod registry; 11 | pub mod task; 12 | pub mod r#type; 13 | -------------------------------------------------------------------------------- /src/installers/inno/entry/permission.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use encoding_rs::Encoding; 4 | 5 | use crate::installers::inno::encoding::InnoValue; 6 | 7 | #[expect(dead_code)] 8 | #[derive(Debug, Default)] 9 | pub struct Permission(String); 10 | 11 | impl Permission { 12 | pub fn from_reader(reader: &mut R, codepage: &'static Encoding) -> Result { 13 | InnoValue::new_string(reader, codepage) 14 | .map(Option::unwrap_or_default) 15 | .map(Permission) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/installers/inno/entry/registry.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bitflags::bitflags; 4 | use byteorder::{LE, ReadBytesExt}; 5 | use encoding_rs::Encoding; 6 | use zerocopy::{Immutable, KnownLayout, TryFromBytes, try_transmute}; 7 | 8 | use crate::installers::{ 9 | inno::{ 10 | encoding::InnoValue, entry::condition::Condition, enum_value::enum_value::enum_value, 11 | flag_reader::read_flags::read_flags, version::InnoVersion, 12 | windows_version::WindowsVersionRange, 13 | }, 14 | utils::registry::RegRoot, 15 | }; 16 | 17 | #[expect(dead_code)] 18 | #[derive(Debug, Default)] 19 | pub struct Registry { 20 | key: Option, 21 | name: Option, 22 | value: Option, 23 | permissions: Option, 24 | reg_root: RegRoot, 25 | permission: i16, 26 | r#type: RegistryType, 27 | flags: RegistryFlags, 28 | } 29 | 30 | impl Registry { 31 | pub fn from_reader( 32 | reader: &mut R, 33 | codepage: &'static Encoding, 34 | version: &InnoVersion, 35 | ) -> Result { 36 | if *version < (1, 3, 0) { 37 | let _uncompressed_size = reader.read_u32::()?; 38 | } 39 | 40 | let mut registry = Self { 41 | key: InnoValue::new_string(reader, codepage)?, 42 | name: InnoValue::new_string(reader, codepage)?, 43 | value: InnoValue::new_string(reader, codepage)?, 44 | permission: -1, 45 | ..Self::default() 46 | }; 47 | 48 | Condition::from_reader(reader, codepage, version)?; 49 | 50 | if *version >= (4, 0, 11) && *version < (4, 1, 0) { 51 | registry.permissions = InnoValue::new_string(reader, codepage)?; 52 | } 53 | 54 | WindowsVersionRange::from_reader(reader, version)?; 55 | 56 | registry.reg_root = 57 | try_transmute!(reader.read_u32::()? | 0x8000_0000).unwrap_or_default(); 58 | 59 | if *version >= (4, 1, 0) { 60 | registry.permission = reader.read_i16::()?; 61 | } 62 | 63 | registry.r#type = enum_value!(reader, RegistryType)?; 64 | 65 | registry.flags = read_flags!(reader, 66 | [ 67 | RegistryFlags::CREATE_VALUE_IF_DOESNT_EXIST, 68 | RegistryFlags::UNINSTALL_DELETE_VALUE, 69 | RegistryFlags::UNINSTALL_CLEAR_VALUE, 70 | RegistryFlags::UNINSTALL_DELETE_ENTIRE_KEY, 71 | RegistryFlags::UNINSTALL_DELETE_ENTIRE_KEY_IF_EMPTY, 72 | ], 73 | if *version >= (1, 2, 6) => RegistryFlags::PRESERVE_STRING_TYPE, 74 | if *version >= (1, 3, 9) => [ 75 | RegistryFlags::DELETE_KEY, 76 | RegistryFlags::DELETE_VALUE 77 | ], 78 | if *version >= (1, 3, 12) => RegistryFlags::NO_ERROR, 79 | if *version >= (1, 3, 16) => RegistryFlags::DONT_CREATE_KEY, 80 | if *version >= (5, 1, 0) => [RegistryFlags::BITS_32, RegistryFlags::BITS_64] 81 | )?; 82 | 83 | Ok(registry) 84 | } 85 | } 86 | 87 | #[expect(dead_code)] 88 | #[derive(Clone, Copy, Debug, Default, TryFromBytes, KnownLayout, Immutable)] 89 | #[repr(u8)] 90 | enum RegistryType { 91 | #[default] 92 | None, 93 | String, 94 | ExpandString, 95 | DWord, 96 | Binary, 97 | MultiString, 98 | QWord, 99 | } 100 | 101 | bitflags! { 102 | #[derive(Debug, Default)] 103 | pub struct RegistryFlags: u16 { 104 | const CREATE_VALUE_IF_DOESNT_EXIST = 1 << 0; 105 | const UNINSTALL_DELETE_VALUE = 1 << 1; 106 | const UNINSTALL_CLEAR_VALUE = 1 << 2; 107 | const UNINSTALL_DELETE_ENTIRE_KEY = 1 << 3; 108 | const UNINSTALL_DELETE_ENTIRE_KEY_IF_EMPTY = 1 << 4; 109 | const PRESERVE_STRING_TYPE = 1 << 5; 110 | const DELETE_KEY = 1 << 6; 111 | const DELETE_VALUE = 1 << 7; 112 | const NO_ERROR = 1 << 8; 113 | const DONT_CREATE_KEY = 1 << 9; 114 | const BITS_32 = 1 << 10; 115 | const BITS_64 = 1 << 11; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/installers/inno/entry/task.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bitflags::bitflags; 4 | use byteorder::{LE, ReadBytesExt}; 5 | use encoding_rs::Encoding; 6 | 7 | use crate::installers::inno::{ 8 | encoding::InnoValue, flag_reader::read_flags::read_flags, version::InnoVersion, 9 | windows_version::WindowsVersionRange, 10 | }; 11 | 12 | #[expect(dead_code)] 13 | #[derive(Debug, Default)] 14 | pub struct Task { 15 | name: Option, 16 | description: Option, 17 | group_description: Option, 18 | components: Option, 19 | languages: Option, 20 | check: Option, 21 | level: u32, 22 | used: bool, 23 | flags: TaskFlags, 24 | } 25 | 26 | impl Task { 27 | pub fn from_reader( 28 | reader: &mut R, 29 | codepage: &'static Encoding, 30 | version: &InnoVersion, 31 | ) -> Result { 32 | let mut task = Self { 33 | name: InnoValue::new_string(reader, codepage)?, 34 | description: InnoValue::new_string(reader, codepage)?, 35 | group_description: InnoValue::new_string(reader, codepage)?, 36 | components: InnoValue::new_string(reader, codepage)?, 37 | ..Self::default() 38 | }; 39 | 40 | if *version >= (4, 0, 1) { 41 | task.languages = InnoValue::new_string(reader, codepage)?; 42 | } 43 | 44 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (1, 3, 24)) { 45 | task.check = InnoValue::new_string(reader, codepage)?; 46 | } 47 | 48 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (3, 0, 3)) { 49 | task.level = reader.read_u32::()?; 50 | } 51 | 52 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (3, 0, 4)) { 53 | task.used = reader.read_u8()? != 0; 54 | } else { 55 | task.used = true; 56 | } 57 | 58 | WindowsVersionRange::from_reader(reader, version)?; 59 | 60 | task.flags = read_flags!(reader, 61 | [TaskFlags::EXCLUSIVE, TaskFlags::UNCHECKED], 62 | if *version >= (2, 0, 5) => TaskFlags::RESTART, 63 | if *version >= (2, 0, 6) => TaskFlags::CHECKED_ONCE, 64 | if *version >= (4, 2, 3) => TaskFlags::DONT_INHERIT_CHECK 65 | )?; 66 | 67 | Ok(task) 68 | } 69 | } 70 | 71 | bitflags! { 72 | #[derive(Debug, Default)] 73 | pub struct TaskFlags: u8 { 74 | const EXCLUSIVE = 1 << 0; 75 | const UNCHECKED = 1 << 1; 76 | const RESTART = 1 << 2; 77 | const CHECKED_ONCE = 1 << 3; 78 | const DONT_INHERIT_CHECK = 1 << 4; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/installers/inno/entry/type.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bitflags::bitflags; 4 | use byteorder::{LE, ReadBytesExt}; 5 | use encoding_rs::Encoding; 6 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 7 | 8 | use crate::installers::inno::{ 9 | encoding::InnoValue, enum_value::enum_value::enum_value, version::InnoVersion, 10 | windows_version::WindowsVersionRange, 11 | }; 12 | 13 | #[expect(dead_code)] 14 | #[derive(Debug, Default)] 15 | pub struct Type { 16 | name: String, 17 | description: Option, 18 | languages: Option, 19 | check: Option, 20 | is_custom: bool, 21 | setup: SetupType, 22 | size: u64, 23 | } 24 | 25 | impl Type { 26 | pub fn from_reader( 27 | reader: &mut R, 28 | codepage: &'static Encoding, 29 | version: &InnoVersion, 30 | ) -> Result { 31 | let mut r#type = Self { 32 | name: InnoValue::new_string(reader, codepage)?.unwrap_or_default(), 33 | description: InnoValue::new_string(reader, codepage)?, 34 | ..Self::default() 35 | }; 36 | 37 | if *version >= (4, 0, 1) { 38 | r#type.languages = InnoValue::new_string(reader, codepage)?; 39 | } 40 | 41 | if *version >= (4, 0, 0) || (version.is_isx() && *version >= (1, 3, 24)) { 42 | r#type.check = InnoValue::new_string(reader, codepage)?; 43 | } 44 | 45 | WindowsVersionRange::from_reader(reader, version)?; 46 | 47 | let flags = TypeFlags::from_bits_retain(reader.read_u8()?); 48 | r#type.is_custom = flags.contains(TypeFlags::CUSTOM_SETUP_TYPE); 49 | 50 | if *version >= (4, 0, 3) { 51 | r#type.setup = enum_value!(reader, SetupType)?; 52 | } 53 | 54 | r#type.size = if *version >= (4, 0, 0) { 55 | reader.read_u64::()? 56 | } else { 57 | u64::from(reader.read_u32::()?) 58 | }; 59 | 60 | Ok(r#type) 61 | } 62 | } 63 | 64 | #[expect(dead_code)] 65 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 66 | #[repr(u8)] 67 | enum SetupType { 68 | #[default] 69 | User, 70 | DefaultFull, 71 | DefaultCompact, 72 | DefaultCustom, 73 | } 74 | 75 | bitflags! { 76 | #[derive(Debug, Default)] 77 | pub struct TypeFlags: u8 { 78 | const CUSTOM_SETUP_TYPE = 1 << 0; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/installers/inno/enum_value.rs: -------------------------------------------------------------------------------- 1 | pub mod enum_value { 2 | macro_rules! enum_value { 3 | ($reader:expr, $ty:ty) => {{ 4 | let mut buf = [0; size_of::<$ty>()]; 5 | $reader.read_exact(&mut buf)?; 6 | <$ty>::try_read_from_bytes(&buf).map_err(|error| { 7 | std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string()) 8 | }) 9 | }}; 10 | } 11 | 12 | pub(crate) use enum_value; 13 | } 14 | -------------------------------------------------------------------------------- /src/installers/inno/header/enums.rs: -------------------------------------------------------------------------------- 1 | use winget_types::installer::ElevationRequirement; 2 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 3 | 4 | use crate::installers::inno::header::flags::{HeaderFlags, PrivilegesRequiredOverrides}; 5 | 6 | // This file defines enums corresponding to Inno Setup's header values. Each enum is represented as 7 | // a u8 as Inno Setup stores these values in a single byte. For example, 0 = Classic, 1 = Modern. 8 | 9 | /// 10 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 11 | #[repr(u8)] 12 | pub enum InnoStyle { 13 | #[default] 14 | Classic, 15 | Modern, 16 | } 17 | 18 | /// 19 | #[expect(dead_code)] 20 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 21 | #[repr(u8)] 22 | pub enum ImageAlphaFormat { 23 | #[default] 24 | Ignored, 25 | Defined, 26 | Premultiplied, 27 | } 28 | 29 | #[expect(dead_code)] 30 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 31 | #[repr(u8)] 32 | pub enum InstallVerbosity { 33 | #[default] 34 | Normal, 35 | Silent, 36 | VerySilent, 37 | } 38 | 39 | #[expect(dead_code)] 40 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 41 | #[repr(u8)] 42 | pub enum LogMode { 43 | Append, 44 | #[default] 45 | New, 46 | Overwrite, 47 | } 48 | 49 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 50 | #[repr(u8)] 51 | pub enum AutoBool { 52 | #[default] 53 | Auto, 54 | No, 55 | Yes, 56 | } 57 | 58 | impl AutoBool { 59 | pub const fn from_header_flags(flags: &HeaderFlags, flag: HeaderFlags) -> Self { 60 | if flags.contains(flag) { 61 | Self::Yes 62 | } else { 63 | Self::No 64 | } 65 | } 66 | } 67 | 68 | #[expect(dead_code)] 69 | #[derive(Debug, Default, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 70 | #[repr(u8)] 71 | pub enum PrivilegeLevel { 72 | #[default] 73 | None, 74 | PowerUser, 75 | Admin, 76 | Lowest, 77 | } 78 | 79 | impl PrivilegeLevel { 80 | pub const fn from_header_flags(flags: &HeaderFlags) -> Self { 81 | if flags.contains(HeaderFlags::ADMIN_PRIVILEGES_REQUIRED) { 82 | Self::Admin 83 | } else { 84 | Self::None 85 | } 86 | } 87 | 88 | pub const fn to_elevation_requirement( 89 | &self, 90 | overrides: &PrivilegesRequiredOverrides, 91 | ) -> Option { 92 | match self { 93 | Self::Admin | Self::PowerUser => Some(ElevationRequirement::ElevatesSelf), 94 | _ if !overrides.is_empty() => Some(ElevationRequirement::ElevatesSelf), 95 | _ => None, 96 | } 97 | } 98 | } 99 | 100 | /// 101 | #[expect(dead_code)] 102 | #[derive(Debug, Default, TryFromBytes, KnownLayout, Immutable)] 103 | #[repr(u8)] 104 | pub enum LanguageDetection { 105 | #[default] 106 | UILanguage, 107 | LocaleLanguage, 108 | None, 109 | } 110 | 111 | impl LanguageDetection { 112 | pub const fn from_header_flags(flags: &HeaderFlags) -> Self { 113 | if flags.contains(HeaderFlags::DETECT_LANGUAGE_USING_LOCALE) { 114 | Self::LocaleLanguage 115 | } else { 116 | Self::UILanguage 117 | } 118 | } 119 | } 120 | 121 | #[expect(dead_code)] 122 | #[derive(Debug, Default, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 123 | #[repr(u8)] 124 | pub enum Compression { 125 | Stored, 126 | Zlib, 127 | BZip2, 128 | LZMA1, 129 | LZMA2, 130 | #[default] 131 | Unknown = u8::MAX, // Set to u8::MAX to avoid conflicts with future variants 132 | } 133 | 134 | impl Compression { 135 | pub const fn from_header_flags(flags: &HeaderFlags) -> Self { 136 | if flags.contains(HeaderFlags::BZIP_USED) { 137 | Self::BZip2 138 | } else { 139 | Self::Zlib 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/installers/inno/header/flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | #[derive(Debug, Default)] 5 | pub struct HeaderFlags: u128 { 6 | const DISABLE_STARTUP_PROMPT = 1 << 0; 7 | const CREATE_APP_DIR = 1 << 1; 8 | const ALLOW_NO_ICONS = 1 << 2; 9 | const ALWAYS_RESTART = 1 << 3; 10 | const ALWAYS_USE_PERSONAL_GROUP = 1 << 4; 11 | const WINDOW_VISIBLE = 1 << 5; 12 | const WINDOW_SHOW_CAPTION = 1 << 6; 13 | const WINDOW_RESIZABLE = 1 << 7; 14 | const WINDOW_START_MAXIMISED = 1 << 8; 15 | const ENABLED_DIR_DOESNT_EXIST_WARNING = 1 << 9; 16 | const PASSWORD = 1 << 10; 17 | const ALLOW_ROOT_DIRECTORY = 1 << 11; 18 | const DISABLE_FINISHED_PAGE = 1 << 12; 19 | const CHANGES_ASSOCIATIONS = 1 << 13; 20 | const USE_PREVIOUS_APP_DIR = 1 << 14; 21 | const BACK_COLOR_HORIZONTAL = 1 << 15; 22 | const USE_PREVIOUS_GROUP = 1 << 16; 23 | const UPDATE_UNINSTALL_LOG_APP_NAME = 1 << 17; 24 | const USE_PREVIOUS_SETUP_TYPE = 1 << 18; 25 | const DISABLE_READY_MEMO = 1 << 19; 26 | const ALWAYS_SHOW_COMPONENTS_LIST = 1 << 20; 27 | const FLAT_COMPONENTS_LIST = 1 << 21; 28 | const SHOW_COMPONENT_SIZES = 1 << 22; 29 | const USE_PREVIOUS_TASKS = 1 << 23; 30 | const DISABLE_READY_PAGE = 1 << 24; 31 | const ALWAYS_SHOW_DIR_ON_READY_PAGE = 1 << 25; 32 | const ALWAYS_SHOW_GROUP_ON_READY_PAGE = 1 << 26; 33 | const ALLOW_UNC_PATH = 1 << 27; 34 | const USER_INFO_PAGE = 1 << 28; 35 | const USE_PREVIOUS_USER_INFO = 1 << 29; 36 | const UNINSTALL_RESTART_COMPUTER = 1 << 30; 37 | const RESTART_IF_NEEDED_BY_RUN = 1 << 31; 38 | const SHOW_TASKS_TREE_LINES = 1 << 32; 39 | const ALLOW_CANCEL_DURING_INSTALL = 1 << 33; 40 | const WIZARD_IMAGE_STRETCH = 1 << 34; 41 | const APPEND_DEFAULT_DIR_NAME = 1 << 35; 42 | const APPEND_DEFAULT_GROUP_NAME = 1 << 36; 43 | const ENCRYPTION_USED = 1 << 37; 44 | const CHANGES_ENVIRONMENT = 1 << 38; 45 | const SETUP_LOGGING = 1 << 39; 46 | const SIGNED_UNINSTALLER = 1 << 40; 47 | const USE_PREVIOUS_LANGUAGE = 1 << 41; 48 | const DISABLE_WELCOME_PAGE = 1 << 42; 49 | const CLOSE_APPLICATIONS = 1 << 43; 50 | const RESTART_APPLICATIONS = 1 << 44; 51 | const ALLOW_NETWORK_DRIVE = 1 << 45; 52 | const FORCE_CLOSE_APPLICATIONS = 1 << 46; 53 | const APP_NAME_HAS_CONSTS = 1 << 47; 54 | const USE_PREVIOUS_PRIVILEGES = 1 << 48; 55 | const WIZARD_RESIZABLE = 1 << 49; 56 | const UNINSTALL_LOGGING = 1 << 50; 57 | // Obsolete flags 58 | const UNINSTALLABLE = 1 << 51; 59 | const DISABLE_DIR_PAGE = 1 << 52; 60 | const DISABLE_PROGRAM_GROUP_PAGE = 1 << 53; 61 | const DISABLE_APPEND_DIR = 1 << 54; 62 | const ADMIN_PRIVILEGES_REQUIRED = 1 << 55; 63 | const ALWAYS_CREATE_UNINSTALL_ICON = 1 << 56; 64 | const CREATE_UNINSTALL_REG_KEY = 1 << 57; 65 | const BZIP_USED = 1 << 58; 66 | const SHOW_LANGUAGE_DIALOG = 1 << 59; 67 | const DETECT_LANGUAGE_USING_LOCALE = 1 << 60; 68 | const DISABLE_DIR_EXISTS_WARNING = 1 << 61; 69 | const BACK_SOLID = 1 << 62; 70 | const OVERWRITE_UNINSTALL_REG_ENTRIES = 1 << 63; 71 | const SHOW_UNDISPLAYABLE_LANGUAGES = 1 << 64; 72 | } 73 | } 74 | 75 | bitflags! { 76 | /// 77 | #[derive(Debug, Default)] 78 | pub struct PrivilegesRequiredOverrides: u8 { 79 | const COMMAND_LINE = 1 << 0; 80 | const DIALOG = 1 << 1; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/installers/inno/read/block.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind, Read, Result, Take}; 2 | 3 | use byteorder::{LE, ReadBytesExt}; 4 | use flate2::read::ZlibDecoder; 5 | use liblzma::read::XzDecoder; 6 | use tracing::debug; 7 | 8 | use crate::installers::{ 9 | inno::{ 10 | InnoError, 11 | compression::Compression, 12 | read::{ 13 | chunk::{INNO_CHUNK_SIZE, InnoChunkReader}, 14 | crc32::Crc32Reader, 15 | decoder::Decoder, 16 | }, 17 | version::InnoVersion, 18 | }, 19 | utils::lzma_stream_header::LzmaStreamHeader, 20 | }; 21 | 22 | pub struct InnoBlockReader { 23 | inner: Decoder>>, 24 | } 25 | 26 | impl InnoBlockReader { 27 | pub fn get(mut inner: R, version: &InnoVersion) -> Result { 28 | let compression = Self::read_header(&mut inner, version)?; 29 | 30 | let mut chunk_reader = InnoChunkReader::new(inner.take(u64::from(*compression))); 31 | 32 | Ok(Self { 33 | inner: match compression { 34 | Compression::LZMA1(_) => { 35 | let stream = LzmaStreamHeader::from_reader(&mut chunk_reader)?; 36 | Decoder::LZMA1(XzDecoder::new_stream(chunk_reader, stream)) 37 | } 38 | Compression::Zlib(_) => Decoder::Zlib(ZlibDecoder::new(chunk_reader)), 39 | Compression::Stored(_) => Decoder::Stored(chunk_reader), 40 | }, 41 | }) 42 | } 43 | 44 | pub fn read_header(reader: &mut R, version: &InnoVersion) -> Result { 45 | let expected_crc32 = reader.read_u32::()?; 46 | 47 | let mut actual_crc32 = Crc32Reader::new(reader); 48 | 49 | let compression = if *version >= (4, 0, 9) { 50 | let size = actual_crc32.read_u32::()?; 51 | let compressed = actual_crc32.read_u8()? != 0; 52 | 53 | if compressed { 54 | if *version >= (4, 1, 6) { 55 | Compression::LZMA1(size) 56 | } else { 57 | Compression::Zlib(size) 58 | } 59 | } else { 60 | Compression::Stored(size) 61 | } 62 | } else { 63 | let compressed_size = actual_crc32.read_u32::()?; 64 | let uncompressed_size = actual_crc32.read_u32::()?; 65 | 66 | let mut stored_size = if compressed_size == u32::MAX { 67 | Compression::Stored(uncompressed_size) 68 | } else { 69 | Compression::Zlib(compressed_size) 70 | }; 71 | 72 | // Add the size of a CRC32 checksum for each 4KiB sub-block 73 | *stored_size += stored_size.div_ceil(u32::from(INNO_CHUNK_SIZE)) * 4; 74 | 75 | stored_size 76 | }; 77 | 78 | debug!(?compression); 79 | 80 | let actual_crc32 = actual_crc32.finalize(); 81 | if actual_crc32 != expected_crc32 { 82 | return Err(Error::new( 83 | ErrorKind::InvalidData, 84 | InnoError::CrcChecksumMismatch { 85 | actual: actual_crc32, 86 | expected: expected_crc32, 87 | }, 88 | )); 89 | } 90 | 91 | Ok(compression) 92 | } 93 | } 94 | 95 | impl Read for InnoBlockReader { 96 | fn read(&mut self, dest: &mut [u8]) -> Result { 97 | self.inner.read(dest) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/installers/inno/read/chunk.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::min, 3 | io::{Error, ErrorKind, Read, Result}, 4 | }; 5 | 6 | use byteorder::{LE, ReadBytesExt}; 7 | 8 | use crate::installers::inno::InnoError; 9 | 10 | pub const INNO_CHUNK_SIZE: u16 = 1 << 12; 11 | 12 | pub struct InnoChunkReader { 13 | inner: R, 14 | buffer: [u8; INNO_CHUNK_SIZE as usize], 15 | pos: usize, 16 | length: usize, 17 | } 18 | 19 | impl InnoChunkReader { 20 | pub const fn new(inner: R) -> Self { 21 | Self { 22 | inner, 23 | buffer: [0; INNO_CHUNK_SIZE as usize], 24 | pos: 0, 25 | length: 0, 26 | } 27 | } 28 | 29 | fn read_chunk(&mut self) -> Result { 30 | let Ok(block_crc32) = self.inner.read_u32::() else { 31 | return Ok(false); 32 | }; 33 | 34 | self.length = self.inner.read(&mut self.buffer)?; 35 | 36 | if self.length == 0 { 37 | return Err(Error::new( 38 | ErrorKind::UnexpectedEof, 39 | "Unexpected Inno block end", 40 | )); 41 | } 42 | 43 | let actual_crc32 = crc32fast::hash(&self.buffer[..self.length]); 44 | 45 | if actual_crc32 != block_crc32 { 46 | return Err(Error::new( 47 | ErrorKind::InvalidData, 48 | InnoError::CrcChecksumMismatch { 49 | actual: actual_crc32, 50 | expected: block_crc32, 51 | }, 52 | )); 53 | } 54 | 55 | self.pos = 0; 56 | 57 | Ok(true) 58 | } 59 | } 60 | 61 | impl Read for InnoChunkReader { 62 | fn read(&mut self, dest: &mut [u8]) -> Result { 63 | let mut total_read = 0; 64 | 65 | while total_read < dest.len() { 66 | if self.pos == self.length && !self.read_chunk()? { 67 | return Ok(total_read); 68 | } 69 | 70 | let to_copy = min(dest.len() - total_read, self.length - self.pos); 71 | 72 | dest[total_read..total_read + to_copy] 73 | .copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]); 74 | 75 | self.pos += to_copy; 76 | total_read += to_copy; 77 | } 78 | 79 | Ok(total_read) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/installers/inno/read/crc32.rs: -------------------------------------------------------------------------------- 1 | use std::{io, io::Read}; 2 | 3 | use crc32fast::Hasher; 4 | 5 | pub struct Crc32Reader { 6 | inner: R, 7 | hasher: Hasher, 8 | } 9 | 10 | impl Crc32Reader { 11 | pub fn new(inner: R) -> Self { 12 | Self { 13 | inner, 14 | hasher: Hasher::new(), 15 | } 16 | } 17 | 18 | /// Provides mutable access to the inner reader without affecting the hasher 19 | pub const fn get_mut(&mut self) -> &mut R { 20 | &mut self.inner 21 | } 22 | 23 | /// Finalize the hash state and return the computed CRC32 value 24 | pub fn finalize(self) -> u32 { 25 | self.hasher.finalize() 26 | } 27 | } 28 | 29 | impl Read for Crc32Reader { 30 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 31 | let bytes_read = self.inner.read(buf)?; 32 | self.hasher.update(&buf[..bytes_read]); 33 | Ok(bytes_read) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/installers/inno/read/decoder.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use flate2::read::ZlibDecoder; 4 | use liblzma::read::XzDecoder; 5 | 6 | pub enum Decoder { 7 | Stored(R), 8 | Zlib(ZlibDecoder), 9 | LZMA1(XzDecoder), 10 | } 11 | 12 | impl Read for Decoder { 13 | fn read(&mut self, buf: &mut [u8]) -> Result { 14 | match self { 15 | Self::Stored(reader) => reader.read(buf), 16 | Self::Zlib(reader) => reader.read(buf), 17 | Self::LZMA1(reader) => reader.read(buf), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/installers/inno/read/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block; 2 | mod chunk; 3 | pub mod crc32; 4 | mod decoder; 5 | -------------------------------------------------------------------------------- /src/installers/inno/windows_version.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use byteorder::{LE, ReadBytesExt}; 4 | 5 | use crate::installers::inno::version::InnoVersion; 6 | 7 | #[derive(Debug, Default)] 8 | struct Version { 9 | major: u8, 10 | minor: u8, 11 | build: u16, 12 | } 13 | 14 | impl Version { 15 | fn from_reader(reader: &mut R, inno_version: &InnoVersion) -> Result { 16 | let mut version = Self::default(); 17 | if *inno_version >= (1, 3, 19) { 18 | version.build = reader.read_u16::()?; 19 | } 20 | version.minor = reader.read_u8()?; 21 | version.major = reader.read_u8()?; 22 | Ok(version) 23 | } 24 | } 25 | 26 | #[derive(Debug, Default)] 27 | struct ServicePack { 28 | major: u8, 29 | minor: u8, 30 | } 31 | 32 | #[expect(dead_code)] 33 | #[derive(Debug, Default)] 34 | struct WindowsVersion { 35 | pub win_version: Version, 36 | pub nt_version: Version, 37 | pub nt_service_pack: ServicePack, 38 | } 39 | 40 | impl WindowsVersion { 41 | pub fn from_reader(reader: &mut R, version: &InnoVersion) -> Result { 42 | let mut windows_version = Self { 43 | win_version: Version::from_reader(reader, version)?, 44 | nt_version: Version::from_reader(reader, version)?, 45 | ..Self::default() 46 | }; 47 | 48 | if *version >= (1, 3, 19) { 49 | windows_version.nt_service_pack.minor = reader.read_u8()?; 50 | windows_version.nt_service_pack.major = reader.read_u8()?; 51 | } 52 | 53 | Ok(windows_version) 54 | } 55 | } 56 | 57 | #[expect(dead_code)] 58 | #[derive(Debug, Default)] 59 | pub struct WindowsVersionRange { 60 | begin: WindowsVersion, 61 | end: WindowsVersion, 62 | } 63 | 64 | impl WindowsVersionRange { 65 | pub fn from_reader(reader: &mut R, version: &InnoVersion) -> Result { 66 | Ok(Self { 67 | begin: WindowsVersion::from_reader(reader, version)?, 68 | end: WindowsVersion::from_reader(reader, version)?, 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/installers/inno/wizard.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use byteorder::{LE, ReadBytesExt}; 4 | 5 | use crate::installers::inno::{ 6 | encoding::InnoValue, 7 | header::{Header, enums::Compression, flags::HeaderFlags}, 8 | version::InnoVersion, 9 | }; 10 | 11 | #[expect(dead_code)] 12 | #[derive(Debug, Default)] 13 | pub struct Wizard { 14 | images: Vec>, 15 | small_images: Vec>, 16 | decompressor_dll: Option>, 17 | decrypt_dll: Option>, 18 | } 19 | 20 | impl Wizard { 21 | pub fn from_reader( 22 | reader: &mut R, 23 | version: &InnoVersion, 24 | header: &Header, 25 | ) -> Result { 26 | let mut wizard = Self { 27 | images: Self::read_images(reader, version)?, 28 | ..Self::default() 29 | }; 30 | 31 | if *version >= (2, 0, 0) || version.is_isx() { 32 | wizard.small_images = Self::read_images(reader, version)?; 33 | } 34 | 35 | if header.compression == Compression::BZip2 36 | || (header.compression == Compression::LZMA1 && *version == (4, 1, 5)) 37 | || (header.compression == Compression::Zlib && *version >= (4, 2, 6)) 38 | { 39 | wizard.decompressor_dll = InnoValue::new_raw(reader)?; 40 | } 41 | 42 | if header.flags.contains(HeaderFlags::ENCRYPTION_USED) { 43 | wizard.decrypt_dll = InnoValue::new_raw(reader)?; 44 | } 45 | 46 | Ok(wizard) 47 | } 48 | 49 | fn read_images(reader: &mut R, version: &InnoVersion) -> Result>> { 50 | let count = if *version >= (5, 6, 0) { 51 | reader.read_u32::()? 52 | } else { 53 | 1 54 | }; 55 | 56 | let mut images = (0..count) 57 | .filter_map(|_| InnoValue::new_raw(reader).transpose()) 58 | .collect::>>()?; 59 | 60 | if *version < (5, 6, 0) && images.first().is_some_and(Vec::is_empty) { 61 | images.clear(); 62 | } 63 | 64 | Ok(images) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/installers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod burn; 2 | pub mod inno; 3 | pub mod msi; 4 | pub mod msix_family; 5 | pub mod nsis; 6 | pub mod possible_installers; 7 | pub mod utils; 8 | pub mod zip; 9 | -------------------------------------------------------------------------------- /src/installers/msix_family/bundle.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | io::{Cursor, Read, Seek}, 4 | }; 5 | 6 | use color_eyre::eyre::Result; 7 | use itertools::Itertools; 8 | use memmap2::Mmap; 9 | use quick_xml::de::from_str; 10 | use serde::Deserialize; 11 | use winget_types::installer::{Installer, PackageFamilyName}; 12 | use zip::ZipArchive; 13 | 14 | use crate::installers::msix_family::{ 15 | Msix, 16 | utils::{hash_signature, read_manifest}, 17 | }; 18 | 19 | pub struct MsixBundle { 20 | pub installers: Vec, 21 | } 22 | 23 | const APPX_BUNDLE_MANIFEST_PATH: &str = "AppxMetadata/AppxBundleManifest.xml"; 24 | 25 | impl MsixBundle { 26 | pub fn new(reader: R) -> Result { 27 | let mut zip = ZipArchive::new(reader)?; 28 | 29 | let appx_bundle_manifest = read_manifest(&mut zip, APPX_BUNDLE_MANIFEST_PATH)?; 30 | 31 | let signature_sha_256 = hash_signature(&mut zip)?; 32 | 33 | let bundle_manifest = from_str::(&appx_bundle_manifest)?; 34 | 35 | let package_family_name = PackageFamilyName::new( 36 | &bundle_manifest.identity.name, 37 | &bundle_manifest.identity.publisher, 38 | ); 39 | 40 | Ok(Self { 41 | installers: bundle_manifest 42 | .packages 43 | .package 44 | .into_iter() 45 | .filter(|package| package.r#type == PackageType::Application) 46 | .map(|package| { 47 | let mut embedded_msix = zip.by_name(&package.file_name)?; 48 | let mut temp_file = tempfile::tempfile()?; 49 | io::copy(&mut embedded_msix, &mut temp_file)?; 50 | let map = unsafe { Mmap::map(&temp_file) }?; 51 | Msix::new(Cursor::new(map.as_ref())) 52 | }) 53 | .map_ok(|msix| Installer { 54 | signature_sha_256: Some(signature_sha_256.clone()), 55 | package_family_name: Some(package_family_name.clone()), 56 | ..msix.installer 57 | }) 58 | .collect::>>()?, 59 | }) 60 | } 61 | } 62 | 63 | /// 64 | #[derive(Deserialize)] 65 | #[serde(rename_all = "PascalCase")] 66 | struct Bundle { 67 | identity: Identity, 68 | packages: Packages, 69 | } 70 | 71 | /// 72 | #[derive(Deserialize)] 73 | struct Identity { 74 | #[serde(rename = "@Name")] 75 | name: String, 76 | #[serde(rename = "@Publisher")] 77 | publisher: String, 78 | } 79 | 80 | /// 81 | #[derive(Deserialize)] 82 | #[serde(rename_all = "PascalCase")] 83 | struct Packages { 84 | package: Vec, 85 | } 86 | 87 | /// 88 | #[derive(Deserialize)] 89 | struct Package { 90 | #[serde(default, rename = "@Type")] 91 | r#type: PackageType, 92 | #[serde(rename = "@FileName")] 93 | file_name: String, 94 | } 95 | 96 | /// 97 | #[derive(Default, Deserialize, PartialEq)] 98 | #[serde(rename_all = "lowercase")] 99 | enum PackageType { 100 | Application, 101 | #[default] 102 | Resource, 103 | } 104 | -------------------------------------------------------------------------------- /src/installers/msix_family/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | io::{Read, Seek}, 4 | }; 5 | 6 | use camino::Utf8PathBuf; 7 | use color_eyre::eyre::Result; 8 | use winget_types::{Sha256String, installer::PackageFamilyName}; 9 | use zip::ZipArchive; 10 | 11 | use crate::installers::{msix_family::APPX_SIGNATURE_P7X, utils::RELATIVE_PROGRAM_FILES_64}; 12 | 13 | pub fn read_manifest(zip: &mut ZipArchive, path: &str) -> Result { 14 | let mut appx_manifest_file = zip.by_name(path)?; 15 | let mut appx_manifest = String::with_capacity(usize::try_from(appx_manifest_file.size())?); 16 | appx_manifest_file.read_to_string(&mut appx_manifest)?; 17 | Ok(appx_manifest) 18 | } 19 | 20 | pub fn hash_signature(zip: &mut ZipArchive) -> io::Result { 21 | let signature_file = zip.by_name(APPX_SIGNATURE_P7X)?; 22 | Sha256String::hash_from_reader(signature_file) 23 | } 24 | 25 | pub fn get_install_location( 26 | name: &str, 27 | publisher: &str, 28 | version: &str, 29 | architecture: &str, 30 | resource_id: &str, 31 | ) -> Utf8PathBuf { 32 | const WINDOWS_APPS: &str = "WindowsApps"; 33 | 34 | let mut path = Utf8PathBuf::from(RELATIVE_PROGRAM_FILES_64); 35 | path.push(WINDOWS_APPS); 36 | path.push(format!( 37 | "{name}_{version}_{architecture}_{resource_id}_{}", 38 | PackageFamilyName::get_id(publisher) 39 | )); 40 | path 41 | } 42 | -------------------------------------------------------------------------------- /src/installers/nsis/entry/creation_disposition.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 4 | 5 | /// 6 | #[expect(dead_code)] 7 | #[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 8 | #[repr(u32)] 9 | pub enum CreationDisposition { 10 | CreateNew = 1, 11 | CreateAlways = 2, 12 | OpenExisting = 3, 13 | OpenAlways = 4, 14 | TruncateExisting = 5, 15 | } 16 | 17 | impl CreationDisposition { 18 | pub const fn as_str(self) -> &'static str { 19 | match self { 20 | Self::CreateNew => "CreateNew", 21 | Self::CreateAlways => "CreateAlways", 22 | Self::OpenExisting => "OpenExisting", 23 | Self::OpenAlways => "OpenAlways", 24 | Self::TruncateExisting => "TruncateExisting", 25 | } 26 | } 27 | } 28 | 29 | impl fmt::Display for CreationDisposition { 30 | #[inline] 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | self.as_str().fmt(f) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/installers/nsis/entry/exec_flag.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 4 | 5 | /// 6 | #[expect(dead_code)] 7 | #[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 8 | #[repr(u32)] 9 | pub enum ExecFlag { 10 | AutoClose = 0u32, 11 | ShellVarContext = 1u32.to_le(), 12 | Errors = 2u32.to_le(), 13 | Abort = 3u32.to_le(), 14 | Reboot = 4u32.to_le(), 15 | RebootCalled = 5u32.to_le(), 16 | CurInstType = 6u32.to_le(), 17 | PluginApiVersion = 7u32.to_le(), 18 | Silent = 8u32.to_le(), 19 | InstDirError = 9u32.to_le(), 20 | RightToLeft = 10u32.to_le(), 21 | ErrorLevel = 11u32.to_le(), 22 | RegView = 12u32.to_le(), 23 | DetailsPrint = 13u32.to_le(), 24 | } 25 | 26 | impl ExecFlag { 27 | pub const fn as_str(self) -> &'static str { 28 | match self { 29 | Self::AutoClose => "AutoClose", 30 | Self::ShellVarContext => "ShellVarContext", 31 | Self::Errors => "Errors", 32 | Self::Abort => "Abort", 33 | Self::Reboot => "Reboot", 34 | Self::RebootCalled => "RebootCalled", 35 | Self::CurInstType => "CurInstType", 36 | Self::PluginApiVersion => "PluginApiVersion", 37 | Self::Silent => "Silent", 38 | Self::InstDirError => "InstDirError", 39 | Self::RightToLeft => "RightToLeft", 40 | Self::ErrorLevel => "ErrorLevel", 41 | Self::RegView => "RegView", 42 | Self::DetailsPrint => "DetailsPrint", 43 | } 44 | } 45 | } 46 | 47 | impl fmt::Display for ExecFlag { 48 | #[inline] 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | self.as_str().fmt(f) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/installers/nsis/entry/generic_access_rights.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bitflags::bitflags; 4 | use zerocopy::{FromBytes, Immutable, KnownLayout}; 5 | 6 | #[derive( 7 | Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, FromBytes, KnownLayout, Immutable, 8 | )] 9 | #[repr(transparent)] 10 | pub struct GenericAccessRights(u32); 11 | 12 | bitflags! { 13 | impl GenericAccessRights: u32 { 14 | const GENERIC_ALL = (1u32 << 28).to_le(); 15 | const GENERIC_EXECUTE = (1u32 << 29).to_le(); 16 | const GENERIC_WRITE = (1u32 << 30).to_le(); 17 | const GENERIC_READ = (1u32 << 31).to_le(); 18 | } 19 | } 20 | 21 | impl fmt::Display for GenericAccessRights { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | bitflags::parser::to_writer(self, f) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/installers/nsis/entry/push_pop.rs: -------------------------------------------------------------------------------- 1 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 2 | 3 | #[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 4 | #[repr(u32)] 5 | pub enum PushPop { 6 | Push = 0u32, 7 | Pop = 1u32.to_le(), 8 | } 9 | -------------------------------------------------------------------------------- /src/installers/nsis/entry/seek_from.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 3 | 4 | /// 5 | #[expect(dead_code)] 6 | #[derive(Copy, Clone, Debug, Display, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 7 | #[repr(u32)] 8 | pub enum SeekFrom { 9 | Set, 10 | Current, 11 | End, 12 | } 13 | -------------------------------------------------------------------------------- /src/installers/nsis/entry/show_window.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use zerocopy::{Immutable, KnownLayout, TryFromBytes, ValidityError, try_transmute}; 4 | 5 | /// 6 | #[expect(dead_code)] 7 | #[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 8 | #[repr(u32)] 9 | pub enum ShowWindow { 10 | Hide, 11 | ShowNormal, 12 | ShowMinimized, 13 | ShowMaximized, 14 | ShowNoActivate, 15 | Show, 16 | Minimize, 17 | ShowMinNoActive, 18 | ShowNA, 19 | Restore, 20 | ShowDefault, 21 | ForceMinimize, 22 | } 23 | 24 | impl ShowWindow { 25 | pub const fn as_str(self) -> &'static str { 26 | match self { 27 | Self::Hide => "Hide", 28 | Self::ShowNormal => "ShowNormal", 29 | Self::ShowMinimized => "ShowMinimized", 30 | Self::ShowMaximized => "ShowMaximized", 31 | Self::ShowNoActivate => "ShowNoActivate", 32 | Self::Show => "Show", 33 | Self::Minimize => "Minimize", 34 | Self::ShowMinNoActive => "ShowMinNoActive", 35 | Self::ShowNA => "ShowNA", 36 | Self::Restore => "Restore", 37 | Self::ShowDefault => "ShowDefault", 38 | Self::ForceMinimize => "ForceMinimize", 39 | } 40 | } 41 | } 42 | 43 | impl fmt::Display for ShowWindow { 44 | #[inline] 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | self.as_str().fmt(f) 47 | } 48 | } 49 | 50 | impl TryFrom for ShowWindow { 51 | type Error = ValidityError; 52 | 53 | #[inline] 54 | fn try_from(value: i32) -> Result { 55 | try_transmute!(value) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/installers/nsis/entry/window_message.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | use zerocopy::{Immutable, KnownLayout, TryFromBytes, ValidityError, try_transmute}; 3 | 4 | /// A merge of [`Window Messages`] and [`Window Notifications`]. 5 | /// 6 | /// [`Window Messages`]: https://learn.microsoft.com/windows/win32/winmsg/window-messages 7 | /// [`Window Notifications`]: https://learn.microsoft.com/windows/win32/winmsg/window-notifications 8 | #[expect(dead_code)] 9 | #[derive(Copy, Clone, Debug, Display, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 10 | #[repr(u16)] 11 | pub enum WindowMessage { 12 | Null = 0x0000, 13 | Create = 0x0001, 14 | Destroy = 0x0002, 15 | Move = 0x0003, 16 | Size = 0x0005, 17 | Enable = 0x000A, 18 | SetText = 0x000C, 19 | GetText = 0x000D, 20 | GetTextLength = 0x000E, 21 | Close = 0x0010, 22 | Quit = 0x0012, 23 | QueryOpen = 0x0013, 24 | ShowWindow = 0x0018, 25 | ActivateApp = 0x001C, 26 | CancelMode = 0x001F, 27 | ChildActivate = 0x0022, 28 | GetMinMaxInfo = 0x0024, 29 | SetFont = 0x0030, 30 | GetFont = 0x0031, 31 | QueryDragIcon = 0x0037, 32 | Compacting = 0x0041, 33 | WindowPosChanging = 0x0046, 34 | WindowPosChanged = 0x0047, 35 | InputLangChangeRequest = 0x0050, 36 | InputLangChange = 0x0051, 37 | UserChanged = 0x0054, 38 | StyleChanging = 0x007C, 39 | StyleChanged = 0x007D, 40 | GetIcon = 0x007F, 41 | SetIcon = 0x0080, 42 | NCCreate = 0x0081, 43 | NCDestroy = 0x0082, 44 | NCCalcSize = 0x0083, 45 | NCActivate = 0x0086, 46 | GetHMenu = 0x01E1, 47 | Sizing = 0x0214, 48 | Moving = 0x0216, 49 | EnterSizeMove = 0x0231, 50 | ExitSizeMove = 0x0232, 51 | ThemeChanged = 0x031A, 52 | } 53 | 54 | impl TryFrom for WindowMessage { 55 | type Error = ValidityError; 56 | 57 | #[inline] 58 | fn try_from(value: u16) -> Result { 59 | try_transmute!(value) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/installers/nsis/file_system/item.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use compact_str::CompactString; 5 | 6 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 7 | pub enum Item { 8 | File { 9 | name: CompactString, 10 | modified_at: Option>, 11 | }, 12 | Directory(CompactString), 13 | } 14 | 15 | impl Item { 16 | #[inline] 17 | pub const fn new_root() -> Self { 18 | Self::Directory(CompactString::const_new("/")) 19 | } 20 | 21 | pub fn new_directory(name: T) -> Self 22 | where 23 | T: Into, 24 | { 25 | Self::Directory(name.into()) 26 | } 27 | 28 | pub fn new_file(name: T, modified_at: D) -> Self 29 | where 30 | T: Into, 31 | D: Into>>, 32 | { 33 | Self::File { 34 | name: name.into(), 35 | modified_at: modified_at.into(), 36 | } 37 | } 38 | 39 | pub fn name(&self) -> &str { 40 | match self { 41 | Self::File { name, .. } | Self::Directory(name) => name.as_str(), 42 | } 43 | } 44 | 45 | pub const fn modified_at(&self) -> Option<&DateTime> { 46 | match self { 47 | Self::File { modified_at, .. } => modified_at.as_ref(), 48 | Self::Directory(_) => None, 49 | } 50 | } 51 | 52 | #[inline] 53 | pub const fn is_file(&self) -> bool { 54 | matches!(self, Self::File { .. }) 55 | } 56 | 57 | #[inline] 58 | pub const fn is_directory(&self) -> bool { 59 | matches!(self, Self::Directory { .. }) 60 | } 61 | } 62 | 63 | impl fmt::Debug for Item { 64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 65 | match self { 66 | Self::File { 67 | name, 68 | modified_at: created_at, 69 | } => { 70 | if let Some(created_at) = created_at { 71 | f.debug_struct("File") 72 | .field("name", name) 73 | .field("modified_at", created_at) 74 | .finish() 75 | } else { 76 | f.debug_tuple("File").field(name).finish() 77 | } 78 | } 79 | Self::Directory(name) => f.debug_tuple("Directory").field(name).finish(), 80 | } 81 | } 82 | } 83 | 84 | impl fmt::Display for Item { 85 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 86 | self.name().fmt(f) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/installers/nsis/first_header/flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | use zerocopy::{FromBytes, Immutable, KnownLayout}; 3 | 4 | #[derive(Copy, Clone, Debug, FromBytes, KnownLayout, Immutable)] 5 | #[repr(transparent)] 6 | pub struct HeaderFlags(u32); 7 | 8 | bitflags! { 9 | impl HeaderFlags: u32 { 10 | const UNINSTALL = (1u32 << 0).to_le(); 11 | const SILENT = (1u32 << 1).to_le(); 12 | const NO_CRC = (1u32 << 2).to_le(); 13 | const FORCE_CRC = (1u32 << 3).to_le(); 14 | // NSISBI fork flags: 15 | const BI_LONG_OFFSET = (1u32 << 4).to_le(); 16 | const BI_EXTERNAL_FILE_SUPPORT = (1u32 << 5).to_le(); 17 | const BI_EXTERNAL_FILE = (1u32 << 6).to_le(); 18 | const BI_IS_STUB_INSTALLER = (1u32 << 7).to_le(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/installers/nsis/first_header/mod.rs: -------------------------------------------------------------------------------- 1 | mod flags; 2 | mod signature; 3 | 4 | use derive_more::Debug; 5 | use zerocopy::{Immutable, KnownLayout, TryFromBytes, little_endian::U32}; 6 | 7 | use crate::installers::nsis::first_header::{ 8 | flags::HeaderFlags, 9 | signature::{Magic, NsisSignature}, 10 | }; 11 | 12 | #[derive(Copy, Clone, Debug, TryFromBytes, KnownLayout, Immutable)] 13 | #[repr(C)] 14 | pub struct FirstHeader { 15 | flags: HeaderFlags, 16 | magic: Magic, 17 | signature: NsisSignature, 18 | #[debug("{length_of_header}")] 19 | pub length_of_header: U32, 20 | #[debug("{length_of_following_data}")] 21 | length_of_following_data: U32, 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use crate::installers::nsis::first_header::FirstHeader; 27 | 28 | #[test] 29 | fn first_header_size() { 30 | const EXPECTED_FIRST_HEADER_SIZE: usize = size_of::() * 7; 31 | 32 | assert_eq!(size_of::(), EXPECTED_FIRST_HEADER_SIZE); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/installers/nsis/first_header/signature.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Debug, Display}; 2 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 3 | 4 | #[expect(dead_code)] 5 | #[derive(Copy, Clone, Debug, TryFromBytes, KnownLayout, Immutable)] 6 | #[repr(u32)] 7 | pub enum Magic { 8 | /// Default NSIS magic bytes 9 | DeadBeef = 0xDEAD_BEEFu32.to_le(), 10 | /// Present in NSIS 1.1e..<1.30 11 | DeadBeed = 0xDEAD_BEEDu32.to_le(), 12 | } 13 | 14 | #[expect(dead_code)] 15 | #[derive(Copy, Clone, Debug, Display, TryFromBytes, KnownLayout, Immutable)] 16 | #[repr(u32)] 17 | pub enum Sig1 { 18 | #[display("Null")] 19 | Null = u32::from_le_bytes(*b"Null").to_le(), 20 | #[display("nsis")] 21 | Nsis = u32::from_le_bytes(*b"nsis").to_le(), 22 | } 23 | 24 | #[expect(dead_code)] 25 | #[derive(Copy, Clone, Debug, Display, TryFromBytes, KnownLayout, Immutable)] 26 | #[repr(u32)] 27 | pub enum Sig2 { 28 | #[display("Soft")] 29 | SoftU = u32::from_le_bytes(*b"Soft").to_le(), 30 | #[display("soft")] 31 | SoftL = u32::from_le_bytes(*b"soft").to_le(), 32 | #[display("inst")] 33 | Inst = u32::from_le_bytes(*b"inst").to_le(), 34 | } 35 | 36 | #[expect(dead_code)] 37 | #[derive(Copy, Clone, Debug, Display, TryFromBytes, KnownLayout, Immutable)] 38 | #[repr(u32)] 39 | pub enum Sig3 { 40 | #[display("Inst")] 41 | Inst = u32::from_le_bytes(*b"Inst").to_le(), 42 | #[display("all\0")] 43 | All0 = u32::from_le_bytes(*b"all\0").to_le(), 44 | } 45 | 46 | #[derive(Copy, Clone, Debug, TryFromBytes, KnownLayout, Immutable)] 47 | #[debug("{_0}{_1}{_2}")] 48 | #[repr(C)] 49 | pub struct NsisSignature(Sig1, Sig2, Sig3); 50 | -------------------------------------------------------------------------------- /src/installers/nsis/header/block.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, io::Cursor, ops::Index}; 2 | 3 | use derive_more::IntoIterator; 4 | use strum::EnumCount; 5 | use winget_types::installer::Architecture; 6 | use zerocopy::{ 7 | FromBytes, Immutable, KnownLayout, 8 | little_endian::{U32, U64}, 9 | }; 10 | 11 | use crate::installers::nsis::{NsisError, section::Section}; 12 | 13 | #[derive(Clone, Debug, Default, FromBytes, KnownLayout, Immutable)] 14 | #[repr(C)] 15 | pub struct BlockHeader { 16 | pub offset: U64, 17 | pub num: U32, 18 | } 19 | 20 | #[derive(Copy, Clone, EnumCount)] 21 | pub enum BlockType { 22 | Pages, 23 | Sections, 24 | Entries, 25 | Strings, 26 | LangTables, 27 | CtlColors, 28 | BgFont, 29 | Data, 30 | } 31 | 32 | impl BlockType { 33 | pub fn get<'data>(self, data: &'data [u8], blocks: &BlockHeaders) -> &'data [u8] { 34 | let start = usize::try_from(blocks[self].offset.get()).unwrap(); 35 | let end = blocks 36 | .into_iter() 37 | .skip(self as usize + 1) 38 | .find(|b| b.offset > U64::ZERO) 39 | .map_or(start, |block| usize::try_from(block.offset.get()).unwrap()); 40 | &data[start..end] 41 | } 42 | } 43 | 44 | #[derive(Clone, Debug, Default, IntoIterator, FromBytes, KnownLayout, Immutable)] 45 | #[repr(transparent)] 46 | pub struct BlockHeaders(#[into_iterator(ref, ref_mut)] [BlockHeader; BlockType::COUNT]); 47 | 48 | impl BlockHeaders { 49 | /// If the NSIS installer is 64-bit, the offset value in the `BlockHeader` is a u64 rather than 50 | /// a u32. This aims to still use zerocopy as much as possible, although the data will need to 51 | /// be owned if the offsets are u32's. 52 | pub fn read_dynamic_from_prefix( 53 | data: &[u8], 54 | architecture: Architecture, 55 | ) -> Result<(Cow, &[u8]), NsisError> { 56 | if architecture.is_64_bit() { 57 | Self::ref_from_prefix(data) 58 | .map(|(headers, rest)| (Cow::Borrowed(headers), rest)) 59 | .map_err(|error| NsisError::ZeroCopy(error.to_string())) 60 | } else { 61 | let mut reader = Cursor::new(data); 62 | let mut block_headers = Self::default(); 63 | for header in &mut block_headers { 64 | *header = BlockHeader { 65 | offset: U64::from(U32::read_from_io(&mut reader)?), 66 | num: U32::read_from_io(&mut reader)?, 67 | } 68 | } 69 | Ok(( 70 | Cow::Owned(block_headers), 71 | &data[usize::try_from(reader.position()).unwrap_or_default()..], 72 | )) 73 | } 74 | } 75 | 76 | pub fn sections<'data>(&self, data: &'data [u8]) -> impl Iterator { 77 | let sections = BlockType::Sections.get(data, self); 78 | let section_size = sections.len() / self[BlockType::Sections].num.get() as usize; 79 | sections 80 | .chunks_exact(section_size) 81 | .flat_map(Section::ref_from_bytes) 82 | } 83 | } 84 | 85 | impl Index for BlockHeaders { 86 | type Output = BlockHeader; 87 | 88 | fn index(&self, index: BlockType) -> &Self::Output { 89 | self.0.index(index as usize) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/installers/nsis/header/compression.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | 3 | #[derive(Copy, Clone, Debug, Display)] 4 | pub enum Compression { 5 | Lzma(bool), 6 | BZip2, 7 | Zlib, 8 | None, 9 | } 10 | -------------------------------------------------------------------------------- /src/installers/nsis/header/decoder.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use bzip2::read::BzDecoder; 4 | use flate2::read::DeflateDecoder; 5 | use liblzma::read::XzDecoder; 6 | 7 | pub enum Decoder { 8 | Lzma(XzDecoder), 9 | BZip2(BzDecoder), 10 | Zlib(DeflateDecoder), 11 | None(R), 12 | } 13 | 14 | impl Read for Decoder { 15 | fn read(&mut self, buf: &mut [u8]) -> Result { 16 | match self { 17 | Self::Lzma(reader) => reader.read(buf), 18 | Self::BZip2(reader) => reader.read(buf), 19 | Self::Zlib(reader) => reader.read(buf), 20 | Self::None(reader) => reader.read(buf), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/installers/nsis/header/flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | use zerocopy::{FromBytes, Immutable, KnownLayout}; 3 | 4 | #[derive(Copy, Clone, Debug, FromBytes, KnownLayout, Immutable)] 5 | #[repr(transparent)] 6 | pub struct CommonHeaderFlags(u32); 7 | 8 | bitflags! { 9 | impl CommonHeaderFlags: u32 { 10 | const DETAILS_SHOWDETAILS = 1u32.to_le(); 11 | const DETAILS_NEVERSHOW = (1u32 << 1).to_le(); 12 | const PROGRESS_COLORED = (1u32 << 2).to_le(); 13 | const FORCE_CRC = (1u32 << 3).to_le(); 14 | const SILENT = (1u32 << 4).to_le(); 15 | const SILENT_LOG = (1u32 << 5).to_le(); 16 | const AUTO_CLOSE = (1u32 << 6).to_le(); 17 | const DIR_NO_SHOW = (1u32 << 7).to_le(); 18 | const NO_ROOT_DIR = (1u32 << 8).to_le(); 19 | const COMP_ONLY_ON_CUSTOM = (1u32 << 9).to_le(); 20 | const NO_CUSTOM = (1u32 << 10).to_le(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/installers/nsis/language/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod table; 2 | -------------------------------------------------------------------------------- /src/installers/nsis/language/table.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind, Result}; 2 | 3 | use itertools::Itertools; 4 | use zerocopy::{ 5 | FromBytes, Immutable, KnownLayout, 6 | little_endian::{I32, U16, U32}, 7 | }; 8 | 9 | use crate::installers::nsis::header::{ 10 | Header, 11 | block::{BlockHeaders, BlockType}, 12 | }; 13 | 14 | #[derive(Debug, FromBytes, KnownLayout, Immutable)] 15 | #[repr(C)] 16 | pub struct LanguageTable { 17 | pub id: U16, 18 | dialog_offset: U32, 19 | right_to_left: U32, 20 | pub string_offsets: [I32], 21 | } 22 | 23 | const EN_US_LANG_CODE: U16 = U16::new(1033); 24 | 25 | impl LanguageTable { 26 | pub fn get_main<'data>( 27 | data: &'data [u8], 28 | header: &Header, 29 | blocks: &BlockHeaders, 30 | ) -> Result<&'data Self> { 31 | BlockType::LangTables 32 | .get(data, blocks) 33 | .chunks_exact(header.langtable_size.get().unsigned_abs() as usize) 34 | .flat_map(Self::ref_from_bytes) 35 | .find_or_first(|lang_table| lang_table.id == EN_US_LANG_CODE) 36 | .ok_or_else(|| Error::new(ErrorKind::NotFound, "No NSIS language table found")) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/installers/nsis/registry.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::{Borrow, Cow}, 3 | hash::Hash, 4 | }; 5 | 6 | use indexmap::IndexMap; 7 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 8 | 9 | use crate::installers::utils::registry::RegRoot; 10 | 11 | type Key<'data> = Cow<'data, str>; 12 | 13 | type ValueName<'data> = Cow<'data, str>; 14 | 15 | type Value<'data> = Cow<'data, str>; 16 | 17 | type Values<'data> = IndexMap, Value<'data>>; 18 | 19 | type Keys<'data> = IndexMap, Values<'data>>; 20 | 21 | /// A mock registry for simulating execution of `WriteReg` and `DeleteReg` entries. 22 | /// 23 | /// This represents a hierarchical registry structure modeled as: 24 | /// 25 | /// - A **Registry root** maps to multiple **Keys** (one-to-many). 26 | /// - Each **Key** maps to multiple **Value names** (one-to-many). 27 | /// - Each **Value name** maps to a single **Value** (one-to-one). 28 | /// 29 | /// Hierarchy: 30 | /// 31 | /// ``` 32 | /// Registry root ─┬─> Key name ─┬─> Value name ──> Value 33 | /// │ └──────────────> ... 34 | /// └────────────> ... 35 | /// ``` 36 | #[derive(Debug)] 37 | pub struct Registry<'data>(IndexMap>); 38 | 39 | const CURRENT_VERSION_UNINSTALL: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall"; 40 | 41 | impl<'data> Registry<'data> { 42 | pub fn new() -> Self { 43 | Self(IndexMap::new()) 44 | } 45 | 46 | /// Finds the first `Software\Microsoft\Windows\CurrentVersion\Uninstall\{PRODUCT_CODE}` key 47 | /// under any root and extracts the product code from it. 48 | pub fn product_code(&self) -> Option<&str> { 49 | self.0.values().find_map(|keys| { 50 | keys.keys().find_map(|key| { 51 | key.rsplit_once('\\').and_then(|(parent, product_code)| { 52 | (parent == CURRENT_VERSION_UNINSTALL).then_some(product_code) 53 | }) 54 | }) 55 | }) 56 | } 57 | 58 | /// Inserts the value into the registry. 59 | /// 60 | /// If the registry did not have this value name present, [`None`] is returned. 61 | /// 62 | /// If the registry did have this value name present, the value is updated, and the old value is 63 | /// returned. 64 | pub fn insert_value( 65 | &mut self, 66 | root: RegRoot, 67 | key: K, 68 | name: N, 69 | value: V, 70 | ) -> Option> 71 | where 72 | K: Into>, 73 | N: Into>, 74 | V: Into>, 75 | { 76 | self.0 77 | .entry(root) 78 | .or_default() 79 | .entry(key.into()) 80 | .or_default() 81 | .insert(name.into(), value.into()) 82 | } 83 | 84 | /// Removes the entire set of values associated with a given key from a specific registry root. 85 | pub fn remove_key(&mut self, root: RegRoot, key: &K) -> Option> 86 | where 87 | Key<'data>: Borrow, 88 | K: Hash + Eq, 89 | { 90 | self.0.get_mut(&root)?.shift_remove(key) 91 | } 92 | 93 | /// Removes a specific named value from a key within a given registry root. 94 | pub fn remove_value_name( 95 | &mut self, 96 | root: RegRoot, 97 | key: &K, 98 | name: &N, 99 | ) -> Option> 100 | where 101 | Key<'data>: Borrow, 102 | ValueName<'data>: Borrow, 103 | K: Hash + Eq, 104 | N: Hash + Eq, 105 | { 106 | self.0.get_mut(&root)?.get_mut(key)?.shift_remove(name) 107 | } 108 | 109 | /// Removes the first occurrence of a value with the specified name across all registry roots 110 | /// and keys. 111 | pub fn remove_value_by_name(&mut self, name: &N) -> Option> 112 | where 113 | ValueName<'data>: Borrow, 114 | N: Hash + Eq, 115 | { 116 | self.0.values_mut().find_map(|keys| { 117 | keys.values_mut() 118 | .find_map(|values| values.shift_remove(name)) 119 | }) 120 | } 121 | } 122 | 123 | /// 124 | #[expect(dead_code)] 125 | #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable)] 126 | #[repr(u32)] 127 | pub enum RegType { 128 | #[default] 129 | None = 0u32.to_le(), 130 | String = 1u32.to_le(), 131 | ExpandedString = 2u32.to_le(), 132 | Binary = 3u32.to_le(), 133 | DWord = 4u32.to_le(), 134 | MultiString = 5u32.to_le(), 135 | } 136 | -------------------------------------------------------------------------------- /src/installers/nsis/section/flags.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bitflags::{Bits, bitflags}; 4 | use derive_more::Deref; 5 | use zerocopy::{FromBytes, Immutable, KnownLayout}; 6 | 7 | #[derive(Copy, Clone, Deref, FromBytes, KnownLayout, Immutable)] 8 | #[repr(transparent)] 9 | pub struct SectionFlags(i32); 10 | 11 | bitflags! { 12 | impl SectionFlags: i32 { 13 | const SELECTED = 1i32.to_le(); 14 | const SECTION_GROUP = (1i32 << 1).to_le(); 15 | const SECTION_GROUP_END = (1i32 << 2).to_le(); 16 | const BOLD = (1i32 << 3).to_le(); 17 | const RO = (1i32 << 4).to_le(); 18 | const EXPAND = (1i32 << 5).to_le(); 19 | const PSELECTED = (1i32 << 6).to_le(); 20 | const TOGGLED = (1i32 << 7).to_le(); 21 | const NAME_CHANGE = (1i32 << 8).to_le(); 22 | } 23 | } 24 | 25 | impl fmt::Debug for SectionFlags { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | if self.is_empty() { 28 | f.write_fmt(format_args!("{:#x}", ::EMPTY)) 29 | } else { 30 | fmt::Display::fmt(self, f) 31 | } 32 | } 33 | } 34 | 35 | impl fmt::Display for SectionFlags { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | bitflags::parser::to_writer(&Self(**self), f) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/installers/nsis/section/mod.rs: -------------------------------------------------------------------------------- 1 | mod flags; 2 | 3 | use derive_more::Debug; 4 | use flags::SectionFlags; 5 | use zerocopy::{FromBytes, I32, Immutable, KnownLayout, LittleEndian}; 6 | 7 | #[derive(Debug, FromBytes, KnownLayout, Immutable)] 8 | #[repr(C)] 9 | pub struct Section { 10 | pub name: I32, 11 | pub install_types: I32, 12 | pub flags: SectionFlags, 13 | pub code: I32, 14 | pub code_size: I32, 15 | pub size_kb: I32, 16 | #[debug(skip)] 17 | pub rest: [u8], 18 | } 19 | -------------------------------------------------------------------------------- /src/installers/nsis/strings/code.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU8; 2 | 3 | use crate::installers::nsis::version::NsisVersion; 4 | 5 | #[derive(Copy, Clone)] 6 | #[repr(u8)] 7 | pub enum NsCode { 8 | Lang, // 1 if >= NSIS 3, 255 otherwise 9 | Shell, // 2 or 254 10 | Var, // 3 or 253 11 | Skip, // 4 or 252 12 | } 13 | 14 | impl NsCode { 15 | pub const fn get(self, nsis_version: NsisVersion) -> u8 { 16 | if nsis_version.is_v3() { 17 | NonZeroU8::MIN.get() + self as u8 18 | } else { 19 | NonZeroU8::MAX.get() - self as u8 20 | } 21 | } 22 | 23 | pub const fn is_code(code: u8, nsis_version: NsisVersion) -> bool { 24 | if nsis_version.is_v3() { 25 | code <= Self::Skip.get(nsis_version) 26 | } else { 27 | code >= Self::Skip.get(nsis_version) 28 | } 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use crate::installers::nsis::{strings::code::NsCode, version::NsisVersion}; 35 | 36 | #[test] 37 | fn lang() { 38 | assert_eq!(NsCode::Lang.get(NsisVersion::_2), 255); 39 | assert_eq!(NsCode::Lang.get(NsisVersion::_3), 1); 40 | } 41 | 42 | #[test] 43 | fn shell() { 44 | assert_eq!(NsCode::Shell.get(NsisVersion::_2), 254); 45 | assert_eq!(NsCode::Shell.get(NsisVersion::_3), 2); 46 | } 47 | 48 | #[test] 49 | fn var() { 50 | assert_eq!(NsCode::Var.get(NsisVersion::_2), 253); 51 | assert_eq!(NsCode::Var.get(NsisVersion::_3), 3); 52 | } 53 | 54 | #[test] 55 | fn skip() { 56 | assert_eq!(NsCode::Skip.get(NsisVersion::_2), 252); 57 | assert_eq!(NsCode::Skip.get(NsisVersion::_3), 4); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/installers/nsis/strings/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod code; 2 | mod predefined; 3 | pub mod shell; 4 | pub mod var; 5 | 6 | pub use predefined::PredefinedVar; 7 | -------------------------------------------------------------------------------- /src/installers/nsis/strings/predefined.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Borrow, fmt}; 2 | 3 | use zerocopy::{Immutable, KnownLayout, TryFromBytes, ValidityError, try_transmute}; 4 | 5 | #[derive( 6 | Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, TryFromBytes, KnownLayout, Immutable, 7 | )] 8 | #[repr(usize)] 9 | pub enum PredefinedVar { 10 | CmdLine, 11 | InstDir, 12 | OutDir, 13 | ExeDir, 14 | Language, 15 | Temp, 16 | PluginsDir, 17 | ExePath, 18 | ExeFile, 19 | WindowParent, 20 | _Click, 21 | _OutDir, 22 | } 23 | 24 | impl PredefinedVar { 25 | pub const fn num_vars() -> usize { 26 | Self::all().len() 27 | } 28 | 29 | pub const fn all() -> &'static [Self; 12] { 30 | &[ 31 | Self::CmdLine, 32 | Self::InstDir, 33 | Self::OutDir, 34 | Self::ExeDir, 35 | Self::Language, 36 | Self::Temp, 37 | Self::PluginsDir, 38 | Self::ExePath, 39 | Self::ExeFile, 40 | Self::WindowParent, 41 | Self::_Click, 42 | Self::_OutDir, 43 | ] 44 | } 45 | 46 | pub const fn as_str(self) -> &'static str { 47 | match self { 48 | Self::CmdLine => "$CMDLINE", 49 | Self::InstDir => "InstallDir", 50 | Self::OutDir => "$OUTDIR", 51 | Self::ExeDir => "$EXEDIR", 52 | Self::Language => "$LANGUAGE", 53 | Self::Temp => "%Temp%", 54 | Self::PluginsDir => "Plugins", 55 | Self::ExePath => "$EXEPATH", 56 | Self::ExeFile => "$EXEFILE", 57 | Self::WindowParent => "$HWNDPARENT", 58 | Self::_Click => "$_CLICK", 59 | Self::_OutDir => "$_OUTDIR", 60 | } 61 | } 62 | } 63 | 64 | impl fmt::Display for PredefinedVar { 65 | #[inline] 66 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | self.as_str().fmt(f) 68 | } 69 | } 70 | 71 | impl Borrow for &PredefinedVar { 72 | #[inline] 73 | fn borrow(&self) -> &str { 74 | self.as_str() 75 | } 76 | } 77 | 78 | impl TryFrom for PredefinedVar { 79 | type Error = ValidityError; 80 | 81 | fn try_from(value: usize) -> Result { 82 | try_transmute!(value) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/installers/nsis/strings/shell.rs: -------------------------------------------------------------------------------- 1 | use crate::installers::{ 2 | nsis::state::NsisState, 3 | utils::{ 4 | RELATIVE_APP_DATA, RELATIVE_COMMON_FILES_32, RELATIVE_COMMON_FILES_64, 5 | RELATIVE_LOCAL_APP_DATA, RELATIVE_PROGRAM_FILES_32, RELATIVE_PROGRAM_FILES_64, 6 | RELATIVE_SYSTEM_ROOT, RELATIVE_WINDOWS_DIR, 7 | }, 8 | }; 9 | 10 | /// NSIS can use one name for two CSIDL_*** and `CSIDL_COMMON`_*** items (`CurrentUser` / `AllUsers`) 11 | /// Some NSIS shell names are not identical to WIN32 CSIDL_* names. 12 | /// NSIS doesn't use some CSIDL_* values. 13 | /// 14 | /// Some values have been adapted to use relative folders for winget. 15 | const STRINGS: &[Option<&str>; 62] = &[ 16 | Some("Desktop"), 17 | Some("Internet"), 18 | Some("SMPrograms"), 19 | Some("Controls"), 20 | Some("Printers"), 21 | Some("Documents"), 22 | Some("Favorites"), 23 | Some("SMStartup"), 24 | Some("Recent"), 25 | Some("SendTo"), 26 | Some("BitBucket"), 27 | Some("StartMenu"), 28 | None, 29 | Some("Music"), 30 | Some("Videos"), 31 | None, 32 | Some("Desktop"), 33 | Some("Drives"), 34 | Some("Network"), 35 | Some("NetHood"), 36 | Some("Fonts"), 37 | Some("Templates"), 38 | Some("StartMenu"), 39 | Some("SMPrograms"), 40 | Some("SMStartup"), 41 | Some("Desktop"), 42 | Some(RELATIVE_APP_DATA), 43 | Some("PrintHood"), 44 | Some(RELATIVE_LOCAL_APP_DATA), 45 | Some("ALTStartUp"), 46 | Some("ALTStartUp"), 47 | Some("Favorites"), 48 | Some("InternetCache"), 49 | Some("Cookies"), 50 | Some("History"), 51 | Some(RELATIVE_APP_DATA), 52 | Some(RELATIVE_WINDOWS_DIR), 53 | Some(RELATIVE_SYSTEM_ROOT), 54 | Some(RELATIVE_PROGRAM_FILES_64), 55 | Some("Pictures"), 56 | Some("Profile"), 57 | Some("System32"), 58 | Some(RELATIVE_PROGRAM_FILES_32), 59 | Some(RELATIVE_COMMON_FILES_64), 60 | Some(RELATIVE_COMMON_FILES_32), 61 | Some("Templates"), 62 | Some("Documents"), 63 | Some("AdminTools"), 64 | Some("AdminTools"), 65 | Some("Connections"), 66 | None, 67 | None, 68 | None, 69 | Some("Music"), 70 | Some("Pictures"), 71 | Some("Videos"), 72 | Some("Resources"), 73 | Some("ResourcesLocalized"), 74 | Some("CommonOEMLinks"), 75 | Some("CDBurnArea"), 76 | None, 77 | Some("ComputersNearMe"), 78 | ]; 79 | 80 | pub struct Shell; 81 | 82 | impl Shell { 83 | /// Adapted from 84 | pub fn resolve(buf: &mut String, state: &NsisState, character: u16) { 85 | const PROGRAM_FILES_DIR: &str = "ProgramFilesDir"; 86 | const COMMON_FILES_DIR: &str = "CommonFilesDir"; 87 | 88 | let (index1, index2): (u8, u8) = character.to_le_bytes().into(); 89 | 90 | if index1 & (1 << 7) != 0 { 91 | let offset = index1 & 0x3F; 92 | let is_64_bit = index1 & (1 << 6) != 0; 93 | let shell_string = state.get_string(i32::from(offset)); 94 | if shell_string == PROGRAM_FILES_DIR { 95 | buf.push_str(if is_64_bit { 96 | RELATIVE_PROGRAM_FILES_64 97 | } else { 98 | RELATIVE_PROGRAM_FILES_32 99 | }); 100 | } else if shell_string == COMMON_FILES_DIR { 101 | buf.push_str(if is_64_bit { 102 | RELATIVE_COMMON_FILES_64 103 | } else { 104 | RELATIVE_COMMON_FILES_32 105 | }); 106 | } 107 | } else if let Some(Some(shell_str)) = STRINGS.get(index1 as usize) { 108 | buf.push_str(shell_str); 109 | } else if let Some(Some(shell_str)) = STRINGS.get(index2 as usize) { 110 | buf.push_str(shell_str); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/installers/nsis/strings/var.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap}; 2 | 3 | use super::PredefinedVar; 4 | use crate::installers::nsis::version::NsisVersion; 5 | 6 | /// The VAR constants have 20 integer constants before the strings: 0-9 and R0-9 7 | const NUM_REGISTERS: usize = 20; 8 | 9 | const NUM_INTERNAL_VARS: usize = NUM_REGISTERS + PredefinedVar::num_vars(); 10 | 11 | pub struct NsVar; 12 | 13 | impl NsVar { 14 | pub fn resolve( 15 | buf: &mut String, 16 | index: usize, 17 | variables: &HashMap>, 18 | nsis_version: NsisVersion, 19 | ) { 20 | if let NUM_REGISTERS..NUM_INTERNAL_VARS = index { 21 | let mut offset = index - NUM_REGISTERS; 22 | if nsis_version == NsisVersion(2, 2, 5) && offset >= PredefinedVar::ExePath as usize { 23 | offset += size_of::(); 24 | } 25 | if let Ok(var) = PredefinedVar::try_from(offset) { 26 | buf.push_str(var.as_str()); 27 | } 28 | } else if let Some(var) = variables.get(&index) { 29 | buf.push_str(var); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/installers/possible_installers.rs: -------------------------------------------------------------------------------- 1 | use winget_types::installer::Installer; 2 | 3 | use crate::installers::{ 4 | burn::Burn, 5 | inno::Inno, 6 | msi::Msi, 7 | msix_family::{Msix, bundle::MsixBundle}, 8 | nsis::Nsis, 9 | }; 10 | 11 | pub enum PossibleInstaller { 12 | Burn(Burn), 13 | Msi(Msi), 14 | Msix(Msix), 15 | MsixBundle(MsixBundle), 16 | Zip(Vec), 17 | Inno(Inno), 18 | Nsis(Nsis), 19 | Other(Installer), 20 | } 21 | 22 | impl PossibleInstaller { 23 | pub fn installers(self) -> Vec { 24 | match self { 25 | Self::Burn(burn) => vec![burn.installer], 26 | Self::Msi(msi) => vec![msi.installer], 27 | Self::Msix(msix) => vec![msix.installer], 28 | Self::MsixBundle(msix_bundle) => msix_bundle.installers, 29 | Self::Zip(installers) => installers, 30 | Self::Inno(inno) => inno.installers, 31 | Self::Nsis(nsis) => vec![nsis.installer], 32 | Self::Other(installer) => vec![installer], 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/installers/utils/lzma_stream_header.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind, Read, Result}; 2 | 3 | use liblzma::stream::{Filters, Stream}; 4 | 5 | pub struct LzmaStreamHeader; 6 | 7 | impl LzmaStreamHeader { 8 | pub fn from_reader(reader: &mut R) -> Result { 9 | let mut properties = [0; 5]; 10 | reader.read_exact(&mut properties)?; 11 | 12 | let mut filters = Filters::new(); 13 | filters.lzma1_properties(&properties)?; 14 | 15 | Stream::new_raw_decoder(&filters).map_err(|error| Error::new(ErrorKind::InvalidData, error)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/installers/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lzma_stream_header; 2 | pub mod registry; 3 | 4 | pub const RELATIVE_PROGRAM_FILES_64: &str = "%ProgramFiles%"; 5 | pub const RELATIVE_PROGRAM_FILES_32: &str = "%ProgramFiles(x86)%"; 6 | pub const RELATIVE_COMMON_FILES_64: &str = "%CommonProgramFiles%"; 7 | pub const RELATIVE_COMMON_FILES_32: &str = "%CommonProgramFiles(x86)%"; 8 | pub const RELATIVE_LOCAL_APP_DATA: &str = "%LocalAppData%"; 9 | pub const RELATIVE_APP_DATA: &str = "%AppData%"; 10 | pub const RELATIVE_PROGRAM_DATA: &str = "%ProgramData%"; 11 | pub const RELATIVE_WINDOWS_DIR: &str = "%WinDir%"; 12 | pub const RELATIVE_SYSTEM_ROOT: &str = "%SystemRoot%"; 13 | pub const RELATIVE_SYSTEM_DRIVE: &str = "%SystemDrive%"; 14 | pub const RELATIVE_TEMP_FOLDER: &str = "%Temp%"; 15 | -------------------------------------------------------------------------------- /src/installers/utils/registry.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, fmt::Formatter}; 2 | 3 | use zerocopy::{Immutable, KnownLayout, TryFromBytes}; 4 | 5 | #[expect(dead_code)] 6 | #[derive( 7 | Copy, Clone, Debug, Default, Hash, PartialEq, Eq, TryFromBytes, KnownLayout, Immutable, 8 | )] 9 | #[repr(u32)] 10 | pub enum RegRoot { 11 | #[default] 12 | ShellContext = 0u32.to_le(), 13 | HKeyClassesRoot = 0x8000_0000u32.to_le(), 14 | HKeyCurrentUser = 0x8000_0001u32.to_le(), 15 | HKeyLocalMachine = 0x8000_0002u32.to_le(), 16 | HKeyUsers = 0x8000_0003u32.to_le(), 17 | HKeyPerformanceData = 0x8000_0004u32.to_le(), 18 | HKeyCurrentConfig = 0x8000_0005u32.to_le(), 19 | HKeyDynamicData = 0x8000_0006u32.to_le(), 20 | HKeyPerformanceText = 0x8000_0050u32.to_le(), 21 | HKeyPerformanceNLSText = 0x8000_0060u32.to_le(), 22 | } 23 | 24 | impl fmt::Display for RegRoot { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 26 | match self { 27 | Self::ShellContext => f.write_str("SHELL_CONTEXT"), 28 | Self::HKeyClassesRoot => f.write_str("HKEY_CLASSES_ROOT"), 29 | Self::HKeyCurrentUser => f.write_str("HKEY_CURRENT_USER"), 30 | Self::HKeyLocalMachine => f.write_str("HKEY_LOCAL_MACHINE"), 31 | Self::HKeyUsers => f.write_str("HKEY_USERS"), 32 | Self::HKeyPerformanceData => f.write_str("HKEY_PERFORMANCE_DATA"), 33 | Self::HKeyCurrentConfig => f.write_str("HKEY_CURRENT_CONFIG"), 34 | Self::HKeyDynamicData => f.write_str("HKEY_DYNAMIC_DATA"), 35 | Self::HKeyPerformanceText => f.write_str("HKEY_PERFORMANCE"), 36 | Self::HKeyPerformanceNLSText => f.write_str("HKEY_PERFORMANCE_NLSTEXT"), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | 3 | use clap::{Parser, Subcommand, crate_name}; 4 | use color_eyre::eyre::Result; 5 | use tracing::{Level, metadata::LevelFilter}; 6 | use tracing_indicatif::IndicatifLayer; 7 | use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt}; 8 | 9 | use crate::commands::{ 10 | analyse::Analyse, 11 | cleanup::Cleanup, 12 | complete::Complete, 13 | list_versions::ListVersions, 14 | new_version::NewVersion, 15 | remove_dead_versions::RemoveDeadVersions, 16 | remove_version::RemoveVersion, 17 | show_version::ShowVersion, 18 | submit::Submit, 19 | sync_fork::SyncFork, 20 | token::commands::{TokenArgs, TokenCommands}, 21 | update_version::UpdateVersion, 22 | }; 23 | 24 | mod commands; 25 | mod credential; 26 | mod download; 27 | mod download_file; 28 | mod editor; 29 | mod file_analyser; 30 | mod github; 31 | mod installers; 32 | mod manifests; 33 | mod match_installers; 34 | mod prompts; 35 | mod terminal; 36 | mod traits; 37 | mod update_state; 38 | 39 | #[tokio::main] 40 | async fn main() -> Result<()> { 41 | color_eyre::config::HookBuilder::default() 42 | .display_env_section(false) 43 | .install()?; 44 | 45 | let indicatif_layer = IndicatifLayer::new(); 46 | 47 | tracing_subscriber::registry() 48 | .with( 49 | tracing_subscriber::fmt::layer() 50 | .with_writer(indicatif_layer.get_stderr_writer()) 51 | .with_target(cfg!(debug_assertions)) 52 | .without_time(), 53 | ) 54 | .with(indicatif_layer) 55 | .with( 56 | filter::Targets::new() 57 | .with_default(LevelFilter::INFO) 58 | .with_target(crate_name!(), Level::TRACE), 59 | ) 60 | .init(); 61 | 62 | match Cli::parse().command { 63 | Commands::New(new_version) => new_version.run().await, 64 | Commands::Update(update_version) => update_version.run().await, 65 | Commands::Cleanup(cleanup) => cleanup.run().await, 66 | Commands::Remove(remove_version) => remove_version.run().await, 67 | Commands::Token(token_args) => match token_args.command { 68 | TokenCommands::Remove(remove_token) => remove_token.run(), 69 | TokenCommands::Update(update_token) => update_token.run().await, 70 | }, 71 | Commands::ListVersions(list_versions) => list_versions.run().await, 72 | Commands::Show(show_version) => show_version.run().await, 73 | Commands::SyncFork(sync_fork) => sync_fork.run().await, 74 | Commands::Complete(complete) => complete.run(), 75 | Commands::Analyse(analyse) => analyse.run(), 76 | Commands::RemoveDeadVersions(remove_dead_versions) => remove_dead_versions.run().await, 77 | Commands::Submit(submit) => submit.run().await, 78 | } 79 | } 80 | 81 | #[derive(Parser)] 82 | #[command(author, version, about, long_about = None, disable_version_flag = true)] 83 | struct Cli { 84 | #[arg(short = 'v', short_alias = 'V', long, action = clap::builder::ArgAction::Version)] 85 | version: (), 86 | #[command(subcommand)] 87 | command: Commands, 88 | } 89 | 90 | #[derive(Subcommand)] 91 | enum Commands { 92 | New(Box), // Comparatively large so boxed to store on the heap 93 | Update(Box), // Comparatively large so boxed to store on the heap 94 | Remove(RemoveVersion), 95 | Cleanup(Cleanup), 96 | Token(TokenArgs), 97 | ListVersions(ListVersions), 98 | Show(ShowVersion), 99 | SyncFork(SyncFork), 100 | Complete(Complete), 101 | Analyse(Analyse), 102 | RemoveDeadVersions(RemoveDeadVersions), 103 | Submit(Submit), 104 | } 105 | -------------------------------------------------------------------------------- /src/manifests/manifest.rs: -------------------------------------------------------------------------------- 1 | use winget_types::{ 2 | PackageIdentifier, PackageVersion, 3 | installer::InstallerManifest, 4 | locale::{DefaultLocaleManifest, LocaleManifest}, 5 | version::VersionManifest, 6 | }; 7 | 8 | pub enum Manifest { 9 | Installer(InstallerManifest), 10 | DefaultLocale(DefaultLocaleManifest), 11 | Locale(LocaleManifest), 12 | Version(VersionManifest), 13 | } 14 | 15 | impl Manifest { 16 | pub const fn package_identifier(&self) -> &PackageIdentifier { 17 | match self { 18 | Self::Installer(installer) => &installer.package_identifier, 19 | Self::DefaultLocale(default_locale) => &default_locale.package_identifier, 20 | Self::Locale(locale) => &locale.package_identifier, 21 | Self::Version(version) => &version.package_identifier, 22 | } 23 | } 24 | 25 | pub const fn package_version(&self) -> &PackageVersion { 26 | match self { 27 | Self::Installer(installer) => &installer.package_version, 28 | Self::DefaultLocale(default_locale) => &default_locale.package_version, 29 | Self::Locale(locale) => &locale.package_version, 30 | Self::Version(version) => &version.package_version, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/manifests/url.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | ops::{Deref, DerefMut}, 4 | str::FromStr, 5 | }; 6 | 7 | use url::ParseError; 8 | use winget_types::{installer::Architecture, url::DecodedUrl}; 9 | 10 | #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] 11 | pub struct Url { 12 | inner: DecodedUrl, 13 | override_architecture: Option, 14 | } 15 | 16 | impl Url { 17 | #[inline] 18 | pub fn override_architecture(&self) -> Option { 19 | self.override_architecture 20 | } 21 | 22 | #[inline] 23 | pub fn inner(&self) -> &DecodedUrl { 24 | &self.inner 25 | } 26 | 27 | #[inline] 28 | pub fn inner_mut(&mut self) -> &mut DecodedUrl { 29 | &mut self.inner 30 | } 31 | 32 | #[inline] 33 | pub fn into_inner(self) -> DecodedUrl { 34 | self.inner 35 | } 36 | } 37 | 38 | impl Deref for Url { 39 | type Target = DecodedUrl; 40 | 41 | #[inline] 42 | fn deref(&self) -> &Self::Target { 43 | &self.inner 44 | } 45 | } 46 | 47 | impl DerefMut for Url { 48 | #[inline] 49 | fn deref_mut(&mut self) -> &mut Self::Target { 50 | &mut self.inner 51 | } 52 | } 53 | 54 | impl fmt::Display for Url { 55 | #[inline] 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | self.inner().fmt(f) 58 | } 59 | } 60 | 61 | impl FromStr for Url { 62 | type Err = ParseError; 63 | 64 | fn from_str(s: &str) -> Result { 65 | let (url, architecture) = s.rsplit_once('|').unwrap_or((s, "")); 66 | 67 | Ok(Url { 68 | inner: url.parse()?, 69 | override_architecture: architecture.parse().ok(), 70 | }) 71 | } 72 | } 73 | 74 | impl From for Url { 75 | fn from(url: DecodedUrl) -> Self { 76 | Self { 77 | inner: url, 78 | override_architecture: None, 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/prompts/list.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeSet, fmt::Display, str::FromStr}; 2 | 3 | use inquire::{Text, validator::Validation}; 4 | use winget_types::{ 5 | installer::{Command, FileExtension, InstallerReturnCode, Protocol}, 6 | locale::Tag, 7 | }; 8 | 9 | use crate::{prompts::handle_inquire_error, traits::Name}; 10 | 11 | pub trait ListPrompt: Name { 12 | const HELP_MESSAGE: &'static str; 13 | const MAX_ITEMS: u16; 14 | } 15 | 16 | impl ListPrompt for InstallerReturnCode { 17 | const HELP_MESSAGE: &'static str = "List of additional non-zero installer success exit codes other than known default values by winget"; 18 | const MAX_ITEMS: u16 = 16; 19 | } 20 | 21 | impl ListPrompt for Protocol { 22 | const HELP_MESSAGE: &'static str = "List of protocols the package provides a handler for"; 23 | const MAX_ITEMS: u16 = 16; 24 | } 25 | 26 | impl ListPrompt for FileExtension { 27 | const HELP_MESSAGE: &'static str = "List of file extensions the package could support"; 28 | const MAX_ITEMS: u16 = 512; 29 | } 30 | 31 | impl ListPrompt for Tag { 32 | const HELP_MESSAGE: &'static str = "Example: zip, c++, photos, OBS"; 33 | const MAX_ITEMS: u16 = 16; 34 | } 35 | 36 | impl ListPrompt for Command { 37 | const HELP_MESSAGE: &'static str = "List of commands or aliases to run the package"; 38 | const MAX_ITEMS: u16 = 16; 39 | } 40 | 41 | pub fn list_prompt() -> color_eyre::Result> 42 | where 43 | T: FromStr + ListPrompt + Ord, 44 | ::Err: Display, 45 | { 46 | const DELIMITERS: [char; 2] = [' ', ',']; 47 | let items = Text::new(&format!("{}:", ::NAME)) 48 | .with_help_message(T::HELP_MESSAGE) 49 | .with_validator(|input: &str| { 50 | let items = input 51 | .split(|char| DELIMITERS.contains(&char)) 52 | .filter(|str| !str.is_empty()) 53 | .collect::>(); 54 | let items_len = items.len(); 55 | if items_len > T::MAX_ITEMS as usize { 56 | return Ok(Validation::Invalid( 57 | format!( 58 | "There is a maximum of {} items. There were {items_len} provided", 59 | T::MAX_ITEMS, 60 | ) 61 | .into(), 62 | )); 63 | } 64 | for item in items { 65 | if let Err(error) = item.parse::() { 66 | return Ok(Validation::Invalid(format!("{item}: {error}").into())); 67 | } 68 | } 69 | Ok(Validation::Valid) 70 | }) 71 | .prompt() 72 | .map_err(handle_inquire_error)? 73 | .split(|char| DELIMITERS.contains(&char)) 74 | .flat_map(T::from_str) 75 | .collect::>(); 76 | Ok(items) 77 | } 78 | -------------------------------------------------------------------------------- /src/prompts/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, ops::BitOr, process}; 2 | 3 | use bitflags::Flags; 4 | use inquire::{InquireError, MultiSelect, Select, error::InquireResult}; 5 | use winget_types::installer::UpgradeBehavior; 6 | 7 | use crate::traits::Name; 8 | 9 | pub mod list; 10 | pub mod text; 11 | 12 | pub trait AllItems { 13 | type Item: Display; 14 | 15 | fn all() -> impl IntoIterator; 16 | } 17 | 18 | impl AllItems for UpgradeBehavior { 19 | type Item = Self; 20 | 21 | fn all() -> impl IntoIterator { 22 | [ 23 | Self::Item::Install, 24 | Self::Item::UninstallPrevious, 25 | Self::Item::Deny, 26 | ] 27 | } 28 | } 29 | 30 | pub fn radio_prompt() -> InquireResult 31 | where 32 | T: Name + AllItems + Display, 33 | { 34 | Select::new( 35 | &format!("{}:", ::NAME), 36 | ::all().into_iter().collect(), 37 | ) 38 | .prompt() 39 | .map_err(handle_inquire_error) 40 | } 41 | 42 | pub fn check_prompt() -> InquireResult 43 | where 44 | T: Name + Flags + Display + BitOr + Copy, 45 | { 46 | MultiSelect::new( 47 | &format!("{}:", ::NAME), 48 | T::all().iter().collect(), 49 | ) 50 | .prompt() 51 | .map(|items| items.iter().fold(T::empty(), |flags, flag| flags | *flag)) 52 | .map_err(handle_inquire_error) 53 | } 54 | 55 | /// Inquire captures Ctrl+C and returns an error. This will instead exit normally if the prompt is 56 | /// interrupted. 57 | pub fn handle_inquire_error(error: InquireError) -> InquireError { 58 | if matches!( 59 | error, 60 | InquireError::OperationCanceled | InquireError::OperationInterrupted 61 | ) { 62 | process::exit(0); 63 | } 64 | error 65 | } 66 | -------------------------------------------------------------------------------- /src/terminal/hyperlink.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, sync::LazyLock}; 2 | 3 | static SUPPORTS_HYPERLINKS: LazyLock = 4 | LazyLock::new(supports_hyperlinks::supports_hyperlinks); 5 | 6 | pub struct Hyperlink<'text, D: fmt::Display, U: fmt::Display> { 7 | text: &'text D, 8 | url: U, 9 | } 10 | 11 | impl fmt::Display for Hyperlink<'_, D, U> { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | if *SUPPORTS_HYPERLINKS { 14 | write!(f, "\x1B]8;;{}\x1B\\{}\x1B]8;;\x1B\\", self.url, self.text) 15 | } else { 16 | write!(f, "{}", self.text) 17 | } 18 | } 19 | } 20 | 21 | pub trait Hyperlinkable { 22 | fn hyperlink(&self, url: U) -> Hyperlink; 23 | } 24 | 25 | impl Hyperlinkable for D { 26 | fn hyperlink(&self, url: U) -> Hyperlink { 27 | Hyperlink { text: self, url } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | mod hyperlink; 2 | 3 | pub use hyperlink::Hyperlinkable; 4 | -------------------------------------------------------------------------------- /src/traits/name.rs: -------------------------------------------------------------------------------- 1 | use winget_types::{ 2 | LanguageTag, PackageIdentifier, PackageVersion, 3 | installer::{ 4 | Channel, Command, FileExtension, InstallModes, InstallerReturnCode, PortableCommandAlias, 5 | Protocol, UpgradeBehavior, 6 | switches::{CustomSwitch, SilentSwitch, SilentWithProgressSwitch}, 7 | }, 8 | locale::{ 9 | Author, Copyright, Description, InstallationNotes, License, Moniker, PackageName, 10 | Publisher, ShortDescription, Tag, 11 | }, 12 | url::{ 13 | CopyrightUrl, LicenseUrl, PackageUrl, PublisherSupportUrl, PublisherUrl, ReleaseNotesUrl, 14 | }, 15 | }; 16 | 17 | pub trait Name { 18 | const NAME: &'static str; 19 | } 20 | 21 | macro_rules! impl_names { 22 | ($( $name:ty => $name_str:literal ),*$(,)?) => { 23 | $( 24 | impl Name for $name { 25 | const NAME: &'static str = $name_str; 26 | } 27 | )* 28 | }; 29 | } 30 | 31 | impl_names!( 32 | Author => "Author", 33 | Channel => "Channel", 34 | Command => "Command", 35 | Copyright => "Copyright", 36 | CopyrightUrl => "Copyright URL", 37 | CustomSwitch => "Custom switch", 38 | Description => "Description", 39 | FileExtension => "File extension", 40 | InstallationNotes => "Installation notes", 41 | InstallerReturnCode => "Installer return code", 42 | InstallModes => "Install modes", 43 | LanguageTag => "Language tag", 44 | License => "License", 45 | LicenseUrl => "License URL", 46 | Moniker => "Moniker", 47 | PackageIdentifier => "Package identifier", 48 | PackageName => "Package name", 49 | PackageUrl => "Package URL", 50 | PackageVersion => "Package version", 51 | PortableCommandAlias => "Portable command alias", 52 | Protocol => "Protocol", 53 | Publisher => "Publisher", 54 | PublisherSupportUrl => "Publisher support URL", 55 | PublisherUrl => "Publisher URL", 56 | ReleaseNotesUrl => "Release notes URL", 57 | ShortDescription => "Short description", 58 | SilentSwitch => "Silent switch", 59 | SilentWithProgressSwitch => "Silent with progress switch", 60 | Tag => "Tag", 61 | UpgradeBehavior => "Upgrade behavior", 62 | ); 63 | -------------------------------------------------------------------------------- /src/traits/path.rs: -------------------------------------------------------------------------------- 1 | use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; 2 | 3 | pub trait NormalizePath { 4 | /// Normalize a path without performing I/O. 5 | /// 6 | /// All redundant separator and up-level references are collapsed. 7 | /// 8 | /// However, this does not resolve links. 9 | fn normalize(&self) -> Utf8PathBuf; 10 | } 11 | 12 | impl NormalizePath for Utf8Path { 13 | fn normalize(&self) -> Utf8PathBuf { 14 | let mut components = self.components().peekable(); 15 | let mut ret = if let Some(c @ Utf8Component::Prefix(..)) = components.peek() { 16 | let buf = Utf8PathBuf::from(c.as_str()); 17 | components.next(); 18 | buf 19 | } else { 20 | Utf8PathBuf::new() 21 | }; 22 | 23 | for component in components { 24 | match component { 25 | Utf8Component::Prefix(..) => unreachable!(), 26 | Utf8Component::RootDir => { 27 | ret.push(component.as_str()); 28 | } 29 | Utf8Component::CurDir => {} 30 | Utf8Component::ParentDir => { 31 | ret.pop(); 32 | } 33 | Utf8Component::Normal(c) => { 34 | ret.push(c); 35 | } 36 | } 37 | } 38 | 39 | ret 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/update_state.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::max, collections::BTreeSet}; 2 | 3 | use derive_more::Display; 4 | use winget_types::PackageVersion; 5 | 6 | #[derive(Copy, Clone, Display)] 7 | pub enum UpdateState { 8 | #[display("New package")] 9 | NewPackage, 10 | #[display("New version")] 11 | NewVersion, 12 | #[display("Add version")] 13 | AddVersion, 14 | #[display("Update version")] 15 | UpdateVersion, 16 | #[display("Remove version")] 17 | RemoveVersion, 18 | } 19 | impl UpdateState { 20 | pub fn get(version: &PackageVersion, versions: Option<&BTreeSet>) -> Self { 21 | match version { 22 | version if versions.is_some_and(|versions| versions.contains(version)) => { 23 | Self::UpdateVersion 24 | } 25 | version 26 | if versions 27 | .and_then(BTreeSet::last) 28 | .is_some_and(|latest| max(version, latest) == version) => 29 | { 30 | Self::NewVersion 31 | } 32 | _ if versions.is_none() => Self::NewPackage, 33 | _ => Self::AddVersion, 34 | } 35 | } 36 | } 37 | --------------------------------------------------------------------------------