chore: initial commit (extracted from Launchers monorepo)
Plugin: UniversalShellExt v1.0.0 Architectures: x64 License: MIT
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{yml,yaml,json}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{cmd,bat,ps1}]
|
||||
end_of_line = crlf
|
||||
|
||||
[*.{nsi,nsh,sln,vcxproj,filters}]
|
||||
end_of_line = crlf
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
@@ -0,0 +1,52 @@
|
||||
# Normalizzazione line endings
|
||||
* text=auto eol=lf
|
||||
|
||||
# File di codice
|
||||
*.c text eol=lf
|
||||
*.cpp text eol=lf
|
||||
*.h text eol=lf
|
||||
*.hpp text eol=lf
|
||||
*.def text eol=lf
|
||||
*.py text eol=lf
|
||||
*.ps1 text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
# NSIS
|
||||
*.nsi text eol=crlf
|
||||
*.nsh text eol=crlf
|
||||
|
||||
# Markdown / docs / config
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.json text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.xml text eol=lf
|
||||
|
||||
# Visual Studio (CRLF richiesto)
|
||||
*.sln text eol=crlf
|
||||
*.vcxproj text eol=crlf merge=union
|
||||
*.filters text eol=crlf merge=union
|
||||
*.rc binary
|
||||
*.rc2 binary
|
||||
|
||||
# Binari
|
||||
*.dll binary
|
||||
*.exe binary
|
||||
*.lib binary
|
||||
*.obj binary
|
||||
*.pdb binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.png binary
|
||||
*.ico binary
|
||||
*.gif binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.bmp binary
|
||||
*.pdf binary
|
||||
|
||||
# Linguistic detection (GitHub Linguist / Gitea analogo)
|
||||
*.nsi linguist-language=NSIS
|
||||
*.nsh linguist-language=NSIS
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Bug Report
|
||||
description: Segnala un bug riproducibile
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Grazie per la segnalazione. Compila i campi sotto per aiutarci a riprodurre il problema.
|
||||
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: Versione plugin
|
||||
description: Contenuto del file VERSION o tag della release scaricata
|
||||
placeholder: "1.6.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: architecture
|
||||
attributes:
|
||||
label: Architettura DLL usata
|
||||
options:
|
||||
- x86-ansi
|
||||
- x86-unicode
|
||||
- amd64-unicode
|
||||
- non so / multiple
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: nsis-version
|
||||
attributes:
|
||||
label: Versione NSIS
|
||||
placeholder: "3.10"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Sistema operativo
|
||||
placeholder: "Windows 11 23H2 x64"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: Passi per riprodurre
|
||||
description: Script .nsi minimale che riproduce il problema
|
||||
render: nsis
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Comportamento atteso
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Comportamento osservato
|
||||
description: Includi messaggi di errore esatti, output del log NSIS, screenshot
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Note aggiuntive
|
||||
description: Qualunque altra informazione utile (workaround tentati, ecc.)
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Domande generali
|
||||
url: https://gitea.emulab.it/Simone/universal-shell-ext/issues?type=question
|
||||
about: Per domande di utilizzo che non sono bug, apri una issue con label "question"
|
||||
- name: Security
|
||||
url: https://gitea.emulab.it/Simone/universal-shell-ext/security/advisories/new
|
||||
about: Segnala vulnerabilità di security in privato (non aprire issue pubbliche)
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Feature Request
|
||||
description: Suggerisci una nuova funzionalità o miglioramento
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problema / Caso d'uso
|
||||
description: Quale problema vorresti risolvere? Quale workflow vuoi abilitare?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposta
|
||||
description: Come dovrebbe funzionare? Includi esempio di API/uso negli script NSIS se possibile.
|
||||
render: nsis
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternative considerate
|
||||
description: Altri approcci che hai valutato e perché sono stati scartati
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Note aggiuntive
|
||||
@@ -0,0 +1,31 @@
|
||||
## Descrizione
|
||||
|
||||
<!-- Descrivi cosa cambia questa PR e perché. Riferisci issue se applicabile (Closes #XXX). -->
|
||||
|
||||
## Tipo di Modifica
|
||||
|
||||
- [ ] Bug fix (modifica non breaking che risolve un problema)
|
||||
- [ ] Feature (modifica non breaking che aggiunge funzionalità)
|
||||
- [ ] Breaking change (fix o feature che rompe la compatibilità)
|
||||
- [ ] Documentazione
|
||||
- [ ] Build / CI
|
||||
|
||||
## Architetture Testate
|
||||
|
||||
- [ ] `x86-ansi`
|
||||
- [ ] `x86-unicode`
|
||||
- [ ] `amd64-unicode`
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Build completa senza warning: `python build_plugin.py`
|
||||
- [ ] CHANGELOG.md aggiornato (sezione `[Unreleased]`)
|
||||
- [ ] README / docs aggiornati se l'API è cambiata
|
||||
- [ ] Esempi aggiornati se l'API è cambiata
|
||||
- [ ] Conventional Commits rispettato
|
||||
- [ ] Nessun file in `dist/` o `__pycache__/` committato
|
||||
- [ ] Self-review completata
|
||||
|
||||
## Note Aggiuntive
|
||||
|
||||
<!-- Eventuali considerazioni per i reviewer, screenshot, output di test, ecc. -->
|
||||
@@ -0,0 +1,63 @@
|
||||
# Build (GitHub) — pipeline CI primaria
|
||||
#
|
||||
# Strategia hosting:
|
||||
# - Repo primario su Gitea (push iniziale)
|
||||
# - Push mirror automatico Gitea -> GitHub
|
||||
# - GitHub esegue TUTTA la CI (Gitea non esegue Actions)
|
||||
#
|
||||
# Trigger:
|
||||
# - push su main (incluso quello propagato dal mirror Gitea)
|
||||
# - PR aperte su GitHub
|
||||
# - run manuale via UI
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config: [x86-ansi, x86-unicode, x64-unicode]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Read VERSION
|
||||
id: version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$v = (Get-Content VERSION -Raw).Trim()
|
||||
"version=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
|
||||
- name: Build ${{ matrix.config }}
|
||||
run: python build_plugin.py --config ${{ matrix.config }} --verbose
|
||||
|
||||
- name: Upload DLL
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ github.event.repository.name }}-${{ steps.version.outputs.version }}-${{ matrix.config }}
|
||||
path: dist/**
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,99 @@
|
||||
# Release (GitHub) — pipeline release primaria
|
||||
#
|
||||
# Triggerata quando un tag v* viene propagato dal mirror Gitea.
|
||||
# Costruisce gli artifact per tutte le architetture e crea la GitHub Release.
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag versione (es. v1.7.0)'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
config: [x86-ansi, x86-unicode, x64-unicode]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Build ${{ matrix.config }}
|
||||
run: python build_plugin.py --config ${{ matrix.config }}
|
||||
|
||||
- name: Determine version & arch
|
||||
id: meta
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tag = "${{ github.ref_name }}"
|
||||
if ([string]::IsNullOrEmpty($tag) -or $tag -eq "main") { $tag = "${{ inputs.tag }}" }
|
||||
$v = $tag -replace '^v',''
|
||||
$map = @{ 'x86-ansi' = 'x86-ansi'; 'x86-unicode' = 'x86-unicode'; 'x64-unicode' = 'amd64-unicode' }
|
||||
$dir = $map['${{ matrix.config }}']
|
||||
echo "version=$v" >> $env:GITHUB_OUTPUT
|
||||
echo "tag=$tag" >> $env:GITHUB_OUTPUT
|
||||
echo "dir=$dir" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
$name = "${{ github.event.repository.name }}-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.dir }}"
|
||||
$stage = "stage/$name"
|
||||
New-Item -ItemType Directory -Force -Path $stage | Out-Null
|
||||
Copy-Item "dist/${{ steps.meta.outputs.dir }}/*" $stage -Recurse
|
||||
Copy-Item README.md, LICENSE, VERSION, CHANGELOG.md $stage
|
||||
if (Test-Path "include") { Copy-Item "include" $stage -Recurse }
|
||||
if (Test-Path "examples") { Copy-Item "examples" $stage -Recurse }
|
||||
Compress-Archive -Path "$stage/*" -DestinationPath "$name.zip" -Force
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pkg-${{ matrix.config }}
|
||||
path: '*.zip'
|
||||
|
||||
publish:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: SHA256SUMS
|
||||
run: |
|
||||
cd artifacts
|
||||
sha256sum *.zip > SHA256SUMS.txt
|
||||
cat SHA256SUMS.txt
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
artifacts/*.zip
|
||||
artifacts/SHA256SUMS.txt
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
**Repository primario**: vedi link Gitea nel README.
|
||||
|
||||
Il mirror GitHub è generato automaticamente dal repo Gitea (sorgente di verità).
|
||||
Per issue e PR aprire ticket sul repository primario Gitea.
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
# Build output (src/ subdirs only — dist/ is tracked, contains pre-built DLLs)
|
||||
bin/
|
||||
obj/
|
||||
Build/
|
||||
build/
|
||||
|
||||
# Visual Studio
|
||||
*.user
|
||||
*.suo
|
||||
*.ncb
|
||||
*.sdf
|
||||
.vs/
|
||||
ipch/
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
*.opendb
|
||||
*.aps
|
||||
*.tlog
|
||||
*.idb
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.exp
|
||||
*.lib
|
||||
*.obj
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
*.egg-info/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Editor / IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
.DS_Store
|
||||
|
||||
# NSIS
|
||||
*.nsh.bak
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
.logs/
|
||||
|
||||
# Sensitive
|
||||
.env
|
||||
.env.local
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# Migration/automation scripts (temporary, contain tokens — never commit)
|
||||
tools/t*.py
|
||||
tools/check_*.py
|
||||
tools/legacy/
|
||||
@@ -0,0 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
Tutte le modifiche rilevanti a questo progetto sono documentate in questo file.
|
||||
|
||||
Il formato segue [Keep a Changelog](https://keepachangelog.com/it/1.0.0/),
|
||||
e il progetto aderisce al [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0] — 2026-04-29
|
||||
|
||||
### Added
|
||||
- Prima release come repository indipendente
|
||||
- Build script unificato (`build_plugin.py`) con CLI canonica
|
||||
- CI/CD via GitHub Actions (mirror automatico Gitea → GitHub)
|
||||
- Documentazione completa (README, CONTRIBUTING, SECURITY)
|
||||
|
||||
[Unreleased]: https://gitea.emulab.it/Simone/universal-shell-ext/compare/v1.0.0...HEAD
|
||||
[1.0.0]: https://gitea.emulab.it/Simone/universal-shell-ext/releases/tag/v1.0.0
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# Contribuire a UniversalShellExt
|
||||
|
||||
Grazie per il tuo interesse a contribuire!
|
||||
|
||||
> Repository primario: [https://gitea.emulab.it/Simone/universal-shell-ext](https://gitea.emulab.it/Simone/universal-shell-ext)
|
||||
> Mirror GitHub: [https://github.com/systempal/universal-shell-ext](https://github.com/systempal/universal-shell-ext) (read-only — apri PR su Gitea)
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
| Strumento | Versione | Note |
|
||||
|-----------|----------|------|
|
||||
| Visual Studio | 2022 / 2026 | Workload: Desktop development with C++ (toolset v143/v145) |
|
||||
| Python | 3.7+ | Per `build_plugin.py` |
|
||||
| Git | 2.30+ | Con supporto submoduli se applicabile |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```powershell
|
||||
git clone https://gitea.emulab.it/Simone/universal-shell-ext.git
|
||||
cd universal-shell-ext
|
||||
python build_plugin.py
|
||||
```
|
||||
|
||||
Output in `dist/{x86-ansi,x86-unicode,amd64-unicode}/`.
|
||||
|
||||
## Workflow Sviluppo
|
||||
|
||||
### 1. Branch
|
||||
|
||||
```powershell
|
||||
git checkout -b feat/nome-feature # nuova feature
|
||||
git checkout -b fix/descrizione-bug # bugfix
|
||||
git checkout -b docs/aggiornamento # solo doc
|
||||
git checkout -b chore/manutenzione # build, dipendenze
|
||||
```
|
||||
|
||||
### 2. Commit
|
||||
|
||||
Segui [Conventional Commits](https://www.conventionalcommits.org/it/v1.0.0/):
|
||||
|
||||
```
|
||||
feat: aggiunge supporto per formato XYZ
|
||||
fix: corregge crash su Windows 11 ARM
|
||||
docs: aggiorna esempio di utilizzo
|
||||
chore: aggiorna toolset MSVC
|
||||
refactor: estrae logica parsing in funzione separata
|
||||
test: aggiunge test per edge case
|
||||
build: aggiorna script di build
|
||||
ci: aggiorna workflow GitHub Actions
|
||||
```
|
||||
|
||||
Breaking changes:
|
||||
|
||||
```
|
||||
feat!: rinomina API Foo() in Bar()
|
||||
|
||||
BREAKING CHANGE: Foo() rinominata in Bar(); aggiornare tutti i chiamanti.
|
||||
```
|
||||
|
||||
### 3. Pull Request
|
||||
|
||||
PR aperte verso il branch `main` su Gitea. Verifica:
|
||||
|
||||
- [ ] Build completa senza warning per tutte le architetture: `python build_plugin.py`
|
||||
- [ ] Esempi aggiornati se cambia l'API
|
||||
- [ ] `CHANGELOG.md` aggiornato (sezione `[Unreleased]`)
|
||||
- [ ] Documentazione aggiornata (README, docs/API.md se esiste)
|
||||
- [ ] Conventional Commits rispettato
|
||||
- [ ] Nessun file in `dist/` o `__pycache__/` committato
|
||||
|
||||
## Build Script
|
||||
|
||||
```
|
||||
python build_plugin.py [opzioni]
|
||||
```
|
||||
|
||||
| Opzione | Default | Descrizione |
|
||||
|---------|---------|-------------|
|
||||
| `--config` | `all` | Architettura: `x86-ansi`, `x86-unicode`, `x64-unicode`, `all` |
|
||||
| `--toolset` | `auto` | Visual Studio: `2022`, `2026`, `auto` (rileva il più recente) |
|
||||
| `--jobs` | numero CPU | Thread paralleli per la build |
|
||||
| `--clean` | false | Pulisce `dist/` prima di compilare |
|
||||
| `--install-dir` | none | Copia DLL anche in `<dir>/{arch}/` (es. NSIS plugin folder) |
|
||||
| `--verbose` | false | Output dettagliato (mostra comandi MSBuild) |
|
||||
|
||||
Exit codes:
|
||||
- `0` — Successo
|
||||
- `1` — Errore di build
|
||||
- `2` — Prerequisiti mancanti
|
||||
- `3` — Argomenti invalidi
|
||||
|
||||
## Release Workflow
|
||||
|
||||
Le release sono automatiche via tag Git:
|
||||
|
||||
```powershell
|
||||
# 1. Aggiorna VERSION e CHANGELOG.md
|
||||
echo "1.7.0" > VERSION
|
||||
# (modifica CHANGELOG.md spostando entry da [Unreleased] a [1.7.0])
|
||||
|
||||
# 2. Commit
|
||||
git add VERSION CHANGELOG.md
|
||||
git commit -m "chore(release): v1.7.0"
|
||||
|
||||
# 3. Tag annotato
|
||||
git tag -a v1.7.0 -m "Release v1.7.0"
|
||||
|
||||
# 4. Push
|
||||
git push origin main
|
||||
git push origin v1.7.0
|
||||
```
|
||||
|
||||
Il workflow `.github/workflows/release.yml` compilerà tutte le architetture
|
||||
e creerà la release su Gitea con gli artifact ZIP allegati. Il mirror GitHub
|
||||
riceverà automaticamente il tag.
|
||||
|
||||
## Code Style
|
||||
|
||||
- C/C++: stile coerente con il file modificato
|
||||
- Python: PEP 8 (4 spazi indent, line length flessibile)
|
||||
- Header file: `#include` ordinati: stdlib → Win32 → progetto
|
||||
- Encoding: UTF-8 senza BOM (eccetto `.rc` che richiede UTF-16 LE)
|
||||
- Line endings: LF in repo (`.gitattributes` normalizza)
|
||||
|
||||
## Reporting Bug
|
||||
|
||||
Apri una [issue](https://gitea.emulab.it/Simone/universal-shell-ext/issues/new) usando il template "Bug Report".
|
||||
Includi:
|
||||
- Versione plugin e architettura DLL usata
|
||||
- Versione NSIS
|
||||
- Sistema operativo
|
||||
- Script `.nsi` minimale che riproduce il problema
|
||||
- Output / errore esatto
|
||||
|
||||
## Domande
|
||||
|
||||
Per domande generali, apri una [issue](https://gitea.emulab.it/Simone/universal-shell-ext/issues) con label `question`.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Simone
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,171 @@
|
||||
# UniversalShellExt
|
||||
|
||||
Shell Extension COM generica per Windows che aggiunge una voce personalizzabile al menu contestuale di cartelle.
|
||||
|
||||
## Funzionalità
|
||||
|
||||
- ✅ **Menu Contestuale Personalizzabile**:
|
||||
- Voce nel menu contestuale per cartelle (click destro su cartella)
|
||||
- Voce nel menu contestuale per sfondo cartella (click destro su area vuota)
|
||||
- Testo, icona e comando completamente configurabili via registro
|
||||
- ✅ **Configurazione Flessibile**:
|
||||
- Tutti i parametri letti dal registro di Windows
|
||||
- Supporto per argomenti personalizzati con placeholder `%1`
|
||||
- Icona estratta automaticamente dall'eseguibile
|
||||
- ✅ **Sicurezza e Compatibilità**:
|
||||
- Supporto UAC elevation automatico se necessario
|
||||
- Compatibile con Windows 10/11 (x64)
|
||||
- Accesso registro WOW64 corretto (64-bit)
|
||||
- Thread-safe
|
||||
|
||||
## File del Progetto
|
||||
|
||||
| File | Descrizione |
|
||||
|------|-------------|
|
||||
| `UniversalShellExt.cpp` | Codice sorgente C++ della Shell Extension COM |
|
||||
| `UniversalShellExt.def` | Definizioni esportazioni DLL |
|
||||
| `UniversalShellExt.rc` | Risorse della DLL (Version Info) |
|
||||
| `build.py` | Script Python per compilazione, firma e registrazione |
|
||||
|
||||
## Configurazione
|
||||
|
||||
La DLL legge la configurazione dal registro di Windows:
|
||||
|
||||
### Chiavi di Registro
|
||||
|
||||
**Percorso:** `HKEY_LOCAL_MACHINE\SOFTWARE\UniversalShellExt`
|
||||
|
||||
*(Fallback: `HKEY_CURRENT_USER\SOFTWARE\UniversalShellExt`)*
|
||||
|
||||
| Valore | Tipo | Descrizione | Esempio |
|
||||
|--------|------|-------------|---------|
|
||||
| `Command` | REG_SZ | Percorso completo dell'eseguibile | `N:\VSCode.exe` |
|
||||
| `Arguments` | REG_SZ | Argomenti da passare (`%1` = path selezionato) | `"%1"` |
|
||||
| `MenuText` | REG_SZ | Testo visualizzato nel menu | `Apri con VSCode` |
|
||||
| `Icon` | REG_SZ | Percorso icona (exe o ico) | `N:\VSCode.exe` |
|
||||
|
||||
### Template Configurazione
|
||||
|
||||
```reg
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
[HKEY_LOCAL_MACHINE\SOFTWARE\UniversalShellExt]
|
||||
"Command"="N:\\VSCode.exe"
|
||||
"Arguments"="\"%1\""
|
||||
"MenuText"="Apri con VSCode Portable"
|
||||
"Icon"="N:\\VSCode.exe"
|
||||
```
|
||||
|
||||
### Valori Default
|
||||
|
||||
Se non configurati:
|
||||
- `Arguments` → `"%1"` (passa il percorso selezionato)
|
||||
- `MenuText` → `"Open with Application"`
|
||||
- `Icon` → usa lo stesso percorso di `Command`
|
||||
|
||||
## Identificatori
|
||||
|
||||
| Tipo | Valore |
|
||||
|------|--------|
|
||||
| **CLSID** | `{5D849F86-F2A9-41FA-8A24-E01CD6D6696C}` |
|
||||
| **Nome COM** | `Universal Portable Shell Extension` |
|
||||
|
||||
## Compilazione
|
||||
|
||||
### Prerequisiti
|
||||
- **Visual Studio 2022** (con workload "Desktop development with C++")
|
||||
- **Windows SDK** (per `signtool.exe` e `rc.exe`)
|
||||
- **Python 3.x**
|
||||
- **Certificato di firma** (opzionale, per firma digitale)
|
||||
|
||||
### Build Script
|
||||
|
||||
```powershell
|
||||
# Build completo (compila, copia, firma, registra)
|
||||
python build.py
|
||||
|
||||
# Solo pulizia
|
||||
python build.py --clean
|
||||
|
||||
# Solo deregistrazione
|
||||
python build.py --unregister
|
||||
|
||||
# Build senza firma
|
||||
python build.py --no-sign
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
La DLL compilata viene copiata in:
|
||||
```
|
||||
plugins\UniversalShellExt64.dll
|
||||
```
|
||||
|
||||
## Registrazione
|
||||
|
||||
### Metodo Consigliato (tramite Launcher)
|
||||
|
||||
Il launcher `VSCode.exe` gestisce automaticamente la registrazione:
|
||||
|
||||
```powershell
|
||||
N:\VSCode.exe /register # Registra la shell extension
|
||||
N:\VSCode.exe /unregister # Rimuove la shell extension
|
||||
N:\VSCode.exe /register /silent # Modalità silenziosa
|
||||
```
|
||||
|
||||
### Metodo Manuale (regsvr32)
|
||||
|
||||
Eseguire come **Amministratore**:
|
||||
|
||||
```cmd
|
||||
:: Registrazione
|
||||
regsvr32 "N:\Code\Workspace\Launchers\plugins\UniversalShellExt64.dll"
|
||||
|
||||
:: Rimozione
|
||||
regsvr32 /u "N:\Code\Workspace\Launchers\plugins\UniversalShellExt64.dll"
|
||||
```
|
||||
|
||||
## Chiavi di Registro Create
|
||||
|
||||
Durante la registrazione (`DllRegisterServer`):
|
||||
|
||||
| Chiave | Scopo |
|
||||
|--------|-------|
|
||||
| `HKCR\CLSID\{5D849F86-...}` | Registrazione classe COM |
|
||||
| `HKCR\CLSID\{5D849F86-...}\InprocServer32` | Percorso DLL |
|
||||
| `HKCR\Directory\shellex\ContextMenuHandlers\UniversalShellExt` | Handler per cartelle |
|
||||
| `HKCR\Directory\Background\shellex\ContextMenuHandlers\...` | Handler per sfondo |
|
||||
| `HKLM\...\Shell Extensions\Approved` | Whitelist Microsoft |
|
||||
|
||||
## Come Funziona
|
||||
|
||||
1. **Inizializzazione**: Quando l'utente fa click destro, Explorer carica la DLL
|
||||
2. **Configurazione**: La DLL legge `Command`, `Arguments`, `MenuText`, `Icon` dal registro
|
||||
3. **Menu**: Inserisce la voce nel menu contestuale con l'icona configurata
|
||||
4. **Esecuzione**: Al click, sostituisce `%1` con il percorso e lancia il comando
|
||||
5. **UAC**: Se il comando richiede elevazione, la richiede automaticamente
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### La voce non appare nel menu
|
||||
1. Verificare che la DLL sia registrata (`regsvr32`)
|
||||
2. Controllare che le chiavi di registro siano configurate
|
||||
3. Riavviare Explorer (`taskkill /F /IM explorer.exe && explorer.exe`)
|
||||
|
||||
### Errore di registrazione
|
||||
- Eseguire come Amministratore
|
||||
- Verificare che la DLL sia firmata (Windows potrebbe bloccare DLL non firmate)
|
||||
|
||||
### Il comando non viene eseguito
|
||||
- Verificare il percorso in `Command` sia corretto
|
||||
- Controllare che `Arguments` contenga `%1` per ricevere il percorso
|
||||
|
||||
## Versioni
|
||||
|
||||
| Versione | Note |
|
||||
|----------|------|
|
||||
| **1.0.0** | Prima release - Shell extension generica configurabile via registro |
|
||||
|
||||
## Licenza
|
||||
|
||||
Uso interno - emulab.it
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Security Policy
|
||||
|
||||
## Versioni Supportate
|
||||
|
||||
Solo l'ultima versione minor riceve fix di security.
|
||||
|
||||
| Versione | Supporto |
|
||||
|----------|----------|
|
||||
| Latest minor (es. 1.0.0) | ✅ |
|
||||
| Versioni precedenti | ❌ |
|
||||
|
||||
## Reporting di una Vulnerabilità
|
||||
|
||||
**Non aprire issue pubbliche per vulnerabilità di security.**
|
||||
|
||||
Per segnalare una vulnerabilità:
|
||||
|
||||
1. **Preferito**: usa la funzione [Security Advisory privato](https://gitea.emulab.it/Simone/universal-shell-ext/security/advisories/new) di Gitea
|
||||
2. **Alternativa**: invia email a `security@example.com` (sostituire con email reale prima del primo release)
|
||||
|
||||
Includi:
|
||||
- Descrizione della vulnerabilità
|
||||
- Passi per riprodurla
|
||||
- Versione/i affette
|
||||
- Possibile mitigazione (se nota)
|
||||
|
||||
## Tempo di Risposta
|
||||
|
||||
- Acknowledgment iniziale: entro 7 giorni
|
||||
- Valutazione: entro 14 giorni
|
||||
- Patch (se confermata): tempistiche dipendono dalla severità
|
||||
|
||||
## Hardening
|
||||
|
||||
Questo plugin gira con i permessi del processo NSIS chiamante (tipicamente
|
||||
l'utente che esegue l'installer). Non richiede privilegi elevati per la
|
||||
compilazione né per l'esecuzione, salvo dove esplicitamente documentato.
|
||||
|
||||
DLL distribuite tramite Releases sono compilate da CI GitHub Actions con
|
||||
toolchain pinnata. Verificare hash SHA256 pubblicati nelle release.
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build script per UniversalShellExt64.dll
|
||||
Compila, copia, firma e registra la shell extension generica.
|
||||
Richiede Visual Studio 2022 e Windows SDK.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import shutil
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
import winreg
|
||||
|
||||
# =============================================================================
|
||||
# Configurazione
|
||||
# =============================================================================
|
||||
|
||||
# Percorsi
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
SOURCE_FILE = SCRIPT_DIR / "UniversalShellExt.cpp"
|
||||
DEF_FILE = SCRIPT_DIR / "UniversalShellExt.def"
|
||||
RC_FILE = SCRIPT_DIR / "UniversalShellExt.rc"
|
||||
RES_FILE = SCRIPT_DIR / "UniversalShellExt.res"
|
||||
|
||||
OUTPUT_DIR = SCRIPT_DIR / "bin" / "x64"
|
||||
OUTPUT_DLL = OUTPUT_DIR / "UniversalShellExt64.dll"
|
||||
|
||||
# Destinazione finale (cartella plugins)
|
||||
DEST_DLL = SCRIPT_DIR.parent.parent / "plugins" / "UniversalShellExt64.dll"
|
||||
|
||||
# Visual Studio
|
||||
VS_PATH = Path("C:/Program Files/Microsoft Visual Studio/2022/Community")
|
||||
VCVARS_BAT = VS_PATH / "VC/Auxiliary/Build/vcvars64.bat"
|
||||
|
||||
# Signtool
|
||||
SIGNTOOL = Path("C:/Program Files (x86)/Windows Kits/10/bin/10.0.22621.0/x64/signtool.exe")
|
||||
CERT_THUMBPRINT = "DFF03ABA0E0156D8E7823A893F9588530A364410"
|
||||
TIMESTAMP_URL = "http://timestamp.digicert.com"
|
||||
|
||||
# Librerie
|
||||
LIBS = [
|
||||
"ole32.lib",
|
||||
"uuid.lib",
|
||||
"shell32.lib",
|
||||
"user32.lib",
|
||||
"gdi32.lib",
|
||||
"advapi32.lib",
|
||||
"shlwapi.lib"
|
||||
]
|
||||
|
||||
CLSID_STRING = "{5D849F86-F2A9-41FA-8A24-E01CD6D6696C}"
|
||||
|
||||
def run_command(cmd, shell=False, check=True):
|
||||
print(f" > {cmd if isinstance(cmd, str) else ' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, shell=shell, capture_output=True, text=True)
|
||||
if result.stdout: print(result.stdout)
|
||||
if result.stderr: print(result.stderr)
|
||||
if check and result.returncode != 0:
|
||||
print(f"ERRORE: Comando fallito con codice {result.returncode}")
|
||||
sys.exit(1)
|
||||
return result
|
||||
|
||||
def check_prerequisites():
|
||||
print("\n=== Verifica prerequisiti ===")
|
||||
errors = []
|
||||
if not SOURCE_FILE.exists(): errors.append(f"Manca sorgente: {SOURCE_FILE}")
|
||||
if not DEF_FILE.exists(): errors.append(f"Manca DEF: {DEF_FILE}")
|
||||
if not VCVARS_BAT.exists(): errors.append(f"Manca VS: {VCVARS_BAT}")
|
||||
if not SIGNTOOL.exists(): errors.append(f"Manca Signtool: {SIGNTOOL}")
|
||||
if errors:
|
||||
for e in errors: print(f" ERRORE: {e}")
|
||||
sys.exit(1)
|
||||
print(" Prerequisiti OK")
|
||||
|
||||
def update_version():
|
||||
print("\n=== Incremento Versione ===")
|
||||
import re
|
||||
|
||||
if not RC_FILE.exists():
|
||||
print(" ERRORE: .rc non trovato!")
|
||||
return
|
||||
|
||||
content = RC_FILE.read_text(encoding="utf-8")
|
||||
|
||||
# Cerca VER_FILEVERSION 1,0,0,X
|
||||
# Regex per catturare i primi 3 numeri e l'ultimo
|
||||
pattern = r'(#define VER_FILEVERSION\s+\d+,\d+,\d+,)(\d+)'
|
||||
match = re.search(pattern, content)
|
||||
|
||||
if match:
|
||||
prefix = match.group(1)
|
||||
build_num = int(match.group(2))
|
||||
new_build_num = build_num + 1
|
||||
|
||||
# Sostituisci VER_FILEVERSION
|
||||
new_content = re.sub(pattern, f'{prefix}{new_build_num}', content)
|
||||
|
||||
# Aggiorna anche VER_FILEVERSION_STR "1.0.0.X\0"
|
||||
str_pattern = r'(#define VER_FILEVERSION_STR\s+"[^"]*?)(\d+)(\\0")'
|
||||
# Nota: questa regex assume che la stringa finisca col numero da incrementare.
|
||||
# Se è "1.0.0.0\0", cattura "1.0.0." come gruppo 1, "0" come 2.
|
||||
|
||||
# Miglior approccio: ricostruire la stringa in base al nuovo numero
|
||||
# Ma dobbiamo parsare i primi 3 numeri per essere sicuri
|
||||
v_parts = prefix.strip().replace("#define VER_FILEVERSION", "").strip().split(',')
|
||||
if len(v_parts) >= 3:
|
||||
major, minor, patch = v_parts[0], v_parts[1], v_parts[2]
|
||||
new_ver_str = f'{major}.{minor}.{patch}.{new_build_num}'
|
||||
|
||||
# Sostituisci la stringa intera con gestione corretta di virgolette e backslash
|
||||
# La regex deve catturare tutto fino ai doppi apici finali
|
||||
new_content = re.sub(r'(#define VER_FILEVERSION_STR\s+)"(.*?)"', f'#define VER_FILEVERSION_STR "{new_ver_str}\\\\0"', new_content)
|
||||
|
||||
# Aggiorna anche VER_PRODUCTVERSION (spesso uguale)
|
||||
new_content = re.sub(r'(#define VER_PRODUCTVERSION\s+).*', f'#define VER_PRODUCTVERSION {major},{minor},{patch},{new_build_num}', new_content)
|
||||
|
||||
# Aggiorna VER_PRODUCTVERSION_STR (spesso solo 3 cifre, ma qui mettiamo 4 per coerenza o lasciamo stare?)
|
||||
# Nel file attuale è "1.0.0\0". Se vogliamo incrementare anche quello:
|
||||
# new_content = re.sub(r'#define VER_PRODUCTVERSION_STR\s+".*?"', f'#define VER_PRODUCTVERSION_STR "{major}.{minor}.{patch}\\0"', new_content)
|
||||
|
||||
RC_FILE.write_text(new_content, encoding="utf-8")
|
||||
print(f" Versione aggiornata a: {new_ver_str}")
|
||||
else:
|
||||
print(" Non riesco a parsare la versione corrente.")
|
||||
else:
|
||||
print(" Pattern versione non trovato.")
|
||||
|
||||
def clean_build():
|
||||
print("\n=== Clean build ===")
|
||||
to_remove = [
|
||||
SCRIPT_DIR / "bin",
|
||||
SCRIPT_DIR / "Release",
|
||||
SCRIPT_DIR / "Debug",
|
||||
SCRIPT_DIR / "x64",
|
||||
SCRIPT_DIR / "UniversalShellExt64.dll",
|
||||
SCRIPT_DIR / "UniversalShellExt64.exp",
|
||||
SCRIPT_DIR / "UniversalShellExt64.lib",
|
||||
SCRIPT_DIR / "UniversalShellExt64.pdb",
|
||||
SCRIPT_DIR / "UniversalShellExt.obj",
|
||||
RES_FILE,
|
||||
]
|
||||
for path in to_remove:
|
||||
if path.exists():
|
||||
if path.is_dir():
|
||||
try: shutil.rmtree(path)
|
||||
except: pass
|
||||
else:
|
||||
try: path.unlink()
|
||||
except: pass
|
||||
print(" Clean completato")
|
||||
|
||||
def kill_explorer():
|
||||
print("\n=== Termina Explorer ===")
|
||||
subprocess.run(["taskkill", "/F", "/IM", "explorer.exe"], capture_output=True, check=False)
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
def start_explorer():
|
||||
print("\n=== Riavvia Explorer ===")
|
||||
subprocess.Popen(["explorer.exe"])
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
def unregister_dll():
|
||||
print("\n=== Deregistra DLL ===")
|
||||
if DEST_DLL.exists():
|
||||
run_command(["regsvr32", "/u", "/s", str(DEST_DLL)], check=False)
|
||||
print(" DLL deregistrata")
|
||||
else:
|
||||
print(" DLL non trovata, skip")
|
||||
|
||||
def compile_dll():
|
||||
print("\n=== Compilazione DLL ===")
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Compile Resource
|
||||
rc_cmd = f'''call "{VCVARS_BAT}" && cd /D "{SCRIPT_DIR}" && rc.exe /fo"{RES_FILE.name}" "{RC_FILE.name}"'''
|
||||
subprocess.run(f'cmd /c "{rc_cmd}"', shell=True, check=True)
|
||||
|
||||
# Compile DLL
|
||||
libs_str = " ".join(LIBS)
|
||||
compile_cmd = f'''call "{VCVARS_BAT}" && cd /D "{SCRIPT_DIR}" && cl.exe /LD /EHsc /O2 /DUNICODE /D_UNICODE "{SOURCE_FILE.name}" "{RES_FILE.name}" /Fe:"{OUTPUT_DLL}" /link /DEF:"{DEF_FILE.name}" {libs_str}'''
|
||||
|
||||
result = subprocess.run(f'cmd /c "{compile_cmd}"', shell=True, capture_output=True, text=True)
|
||||
if result.stdout:
|
||||
for line in result.stdout.split('\n'):
|
||||
if line.strip() and not line.startswith('**') and not line.startswith('['):
|
||||
print(f" {line}")
|
||||
if result.returncode != 0 or not OUTPUT_DLL.exists():
|
||||
print("ERRORE: Compilazione fallita")
|
||||
sys.exit(1)
|
||||
|
||||
print(f" Compilata: {OUTPUT_DLL}")
|
||||
|
||||
def copy_dll():
|
||||
print("\n=== Copia DLL ===")
|
||||
shutil.copy2(OUTPUT_DLL, DEST_DLL)
|
||||
print(f" Copiata in: {DEST_DLL}")
|
||||
|
||||
def sign_dll():
|
||||
print("\n=== Firma DLL ===")
|
||||
cmd = [str(SIGNTOOL), "sign", "/sha1", CERT_THUMBPRINT, "/fd", "SHA256", "/t", TIMESTAMP_URL, str(DEST_DLL)]
|
||||
run_command(cmd)
|
||||
print(" Firma OK")
|
||||
|
||||
def register_dll():
|
||||
print("\n=== Registra DLL ===")
|
||||
result = run_command(["regsvr32", "/s", str(DEST_DLL)], check=False)
|
||||
if result.returncode == 0:
|
||||
print(" Registrazione OK")
|
||||
else:
|
||||
print(f" FALLITA: {result.returncode} (Serve Admin?)")
|
||||
|
||||
def verify_registration():
|
||||
print("\n=== Verifica Registrazione ===")
|
||||
try:
|
||||
key_path = fr"SOFTWARE\Classes\CLSID\{CLSID_STRING}\InprocServer32"
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key:
|
||||
val, _ = winreg.QueryValueEx(key, "")
|
||||
print(f" CLSID registrato: {val}")
|
||||
except FileNotFoundError:
|
||||
print(" ERRORE: CLSID non trovato")
|
||||
return False
|
||||
|
||||
try:
|
||||
key_path = r"SOFTWARE\Classes\Directory\shellex\ContextMenuHandlers\UniversalShellExt"
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key:
|
||||
val, _ = winreg.QueryValueEx(key, "")
|
||||
print(f" Handler directory registrato: {val}")
|
||||
except FileNotFoundError:
|
||||
print(" ERRORE: Handler non trovato")
|
||||
return False
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("UniversalShellExt Builder")
|
||||
args = sys.argv[1:]
|
||||
|
||||
if "--clean" in args:
|
||||
clean_build()
|
||||
return
|
||||
|
||||
if "--unregister" in args:
|
||||
unregister_dll()
|
||||
return
|
||||
|
||||
# STEP 0: Clean pre-build
|
||||
clean_build()
|
||||
|
||||
update_version()
|
||||
check_prerequisites()
|
||||
unregister_dll()
|
||||
kill_explorer()
|
||||
compile_dll()
|
||||
copy_dll()
|
||||
|
||||
if "--no-sign" not in args:
|
||||
sign_dll()
|
||||
|
||||
start_explorer()
|
||||
register_dll()
|
||||
verify_registration()
|
||||
clean_build()
|
||||
|
||||
print("\nDONE!")
|
||||
print(f"DLL: {DEST_DLL}")
|
||||
print("Ricorda di configurare HKLM\\Software\\UniversalShellExt")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,700 @@
|
||||
// UniversalShellExt.cpp - Generic Shell Extension
|
||||
// Configurable via Registry
|
||||
// Version: 1.1.0
|
||||
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <shlwapi.h>
|
||||
#include <strsafe.h>
|
||||
#include <new>
|
||||
|
||||
#pragma comment(lib, "shlwapi.lib")
|
||||
|
||||
// DLL Version
|
||||
#define DLL_VERSION L"1.1.0"
|
||||
|
||||
// GUID for UniversalShellExt
|
||||
// {5D849F86-F2A9-41FA-8A24-E01CD6D6696C}
|
||||
static const CLSID CLSID_UniversalShellExt =
|
||||
{ 0x5d849f86, 0xf2a9, 0x41fa, { 0x8a, 0x24, 0xe0, 0x1c, 0xd6, 0xd6, 0x69, 0x6c } };
|
||||
|
||||
// CLSID as string
|
||||
#define CLSID_STRING L"{5D849F86-F2A9-41FA-8A24-E01CD6D6696C}"
|
||||
|
||||
// Registry Configuration - now per-CLSID
|
||||
#define REG_KEY_BASE L"Software\\UniversalShellExt\\"
|
||||
#define REG_VALUE_COMMAND L"Command"
|
||||
#define REG_VALUE_ARGS L"Arguments"
|
||||
#define REG_VALUE_ICON L"Icon"
|
||||
#define REG_VALUE_TEXT L"MenuText"
|
||||
#define REG_VALUE_ENABLE_LOG L"EnableLog"
|
||||
|
||||
// Global Variables
|
||||
HINSTANCE g_hInst = NULL;
|
||||
LONG g_cDllRef = 0;
|
||||
|
||||
// Internal Helper for File Logging
|
||||
void WriteLogFile(const wchar_t* buffer) {
|
||||
FILE* f;
|
||||
if (_wfopen_s(&f, L"C:\\Users\\Simone\\.gemini\\shellext_debug.log", L"a") == 0) {
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
fwprintf(f, L"[%02d:%02d:%02d.%03d] %s\n", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, buffer);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Global Logger (Legacy/Global scope)
|
||||
void WriteLog(const wchar_t* format, ...) {
|
||||
// Global logging is now MINIMAL or DISABLED by default unless needed.
|
||||
// Uncomment for deep debugging of DLL loading.
|
||||
// wchar_t buffer[1024];
|
||||
// va_list args;
|
||||
// va_start(args, format);
|
||||
// _vswprintf_p(buffer, 1024, format, args);
|
||||
// va_end(args);
|
||||
// WriteLogFile(buffer);
|
||||
}
|
||||
|
||||
// Configuration Cache globals removed.
|
||||
// They are now member variables of the class.
|
||||
|
||||
//==============================================================================
|
||||
// Helper: Icon to Bitmap
|
||||
//==============================================================================
|
||||
|
||||
//==============================================================================
|
||||
// Helper: Icon to Bitmap
|
||||
//==============================================================================
|
||||
HBITMAP IconToBitmap(HICON hIcon)
|
||||
{
|
||||
if (!hIcon) return NULL;
|
||||
ICONINFO iconInfo;
|
||||
if (!GetIconInfo(hIcon, &iconInfo)) return NULL;
|
||||
|
||||
BITMAP bm;
|
||||
GetObject(iconInfo.hbmColor ? iconInfo.hbmColor : iconInfo.hbmMask, sizeof(bm), &bm);
|
||||
|
||||
int width = bm.bmWidth;
|
||||
int height = bm.bmHeight;
|
||||
|
||||
if (iconInfo.hbmColor) DeleteObject(iconInfo.hbmColor);
|
||||
if (iconInfo.hbmMask) DeleteObject(iconInfo.hbmMask);
|
||||
|
||||
HDC hdcScreen = GetDC(NULL);
|
||||
HDC hdcMem = CreateCompatibleDC(hdcScreen);
|
||||
|
||||
BITMAPINFO bmi = {0};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = width;
|
||||
bmi.bmiHeader.biHeight = -height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* pBits;
|
||||
HBITMAP hBitmap = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &pBits, NULL, 0);
|
||||
|
||||
if (hBitmap)
|
||||
{
|
||||
HBITMAP hOldBitmap = (HBITMAP)SelectObject(hdcMem, hBitmap);
|
||||
RECT rc = {0, 0, width, height};
|
||||
FillRect(hdcMem, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH)); // Transparent assumes 32bit alpha
|
||||
DrawIconEx(hdcMem, 0, 0, hIcon, width, height, 0, NULL, DI_NORMAL);
|
||||
SelectObject(hdcMem, hOldBitmap);
|
||||
}
|
||||
|
||||
DeleteDC(hdcMem);
|
||||
ReleaseDC(NULL, hdcScreen);
|
||||
return hBitmap;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Class UniversalShellExt
|
||||
//==============================================================================
|
||||
class UniversalShellExt : public IShellExtInit, public IContextMenu
|
||||
{
|
||||
public:
|
||||
// IUnknown
|
||||
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv)
|
||||
{
|
||||
static const QITAB qit[] =
|
||||
{
|
||||
QITABENT(UniversalShellExt, IContextMenu),
|
||||
QITABENT(UniversalShellExt, IShellExtInit),
|
||||
{ 0 },
|
||||
};
|
||||
return QISearch(this, qit, riid, ppv);
|
||||
}
|
||||
|
||||
IFACEMETHODIMP_(ULONG) AddRef()
|
||||
{
|
||||
return InterlockedIncrement(&m_cRef);
|
||||
}
|
||||
|
||||
IFACEMETHODIMP_(ULONG) Release()
|
||||
{
|
||||
ULONG cRef = InterlockedDecrement(&m_cRef);
|
||||
if (cRef == 0) delete this;
|
||||
return cRef;
|
||||
}
|
||||
|
||||
// IShellExtInit
|
||||
IFACEMETHODIMP Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID)
|
||||
{
|
||||
LPOLESTR szCLSID = NULL;
|
||||
StringFromCLSID(m_clsid, &szCLSID);
|
||||
Log(L"UniversalShellExt::Initialize called. CLSID: %s", szCLSID ? szCLSID : L"Unknown");
|
||||
if (szCLSID) CoTaskMemFree(szCLSID);
|
||||
|
||||
if (pDataObj == NULL) {
|
||||
Log(L"Initialize - Error: pDataObj is NULL");
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
FORMATETC fe = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
|
||||
STGMEDIUM stm;
|
||||
|
||||
if (SUCCEEDED(pDataObj->GetData(&fe, &stm)))
|
||||
{
|
||||
UINT nFiles = DragQueryFile((HDROP)stm.hGlobal, 0xFFFFFFFF, NULL, 0);
|
||||
Log(L"Initialize - Files selected: %d", nFiles);
|
||||
if (nFiles > 0)
|
||||
{
|
||||
DragQueryFile((HDROP)stm.hGlobal, 0, m_szSelectedPath, MAX_PATH);
|
||||
Log(L"Initialize - Selected Path: %s", m_szSelectedPath);
|
||||
}
|
||||
ReleaseStgMedium(&stm);
|
||||
}
|
||||
else if (pidlFolder != NULL)
|
||||
{
|
||||
SHGetPathFromIDList(pidlFolder, m_szSelectedPath);
|
||||
Log(L"Initialize - From PIDL: %s", m_szSelectedPath);
|
||||
}
|
||||
else {
|
||||
Log(L"Initialize - Failed to get path data.");
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// IContextMenu
|
||||
IFACEMETHODIMP QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
|
||||
{
|
||||
LPOLESTR szCLSID = NULL;
|
||||
StringFromCLSID(m_clsid, &szCLSID);
|
||||
Log(L"UniversalShellExt::QueryContextMenu called for CLSID: %s", szCLSID ? szCLSID : L"Unknown");
|
||||
Log(L"QueryContextMenu - indexMenu: %d, idCmdFirst: %d, idCmdLast: %d, uFlags: 0x%X", indexMenu, idCmdFirst, idCmdLast, uFlags);
|
||||
if (szCLSID) CoTaskMemFree(szCLSID);
|
||||
|
||||
if (uFlags & CMF_DEFAULTONLY) return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0);
|
||||
|
||||
LoadConfiguration(); // Load config into member variables
|
||||
|
||||
// Fixed position at top
|
||||
UINT targetPosition = 0;
|
||||
|
||||
Log(L"QueryContextMenu - indexMenu: %d, idCmdFirst: %d, idCmdLast: %d", indexMenu, idCmdFirst, idCmdLast);
|
||||
Log(L"QueryContextMenu - Using fixed position: %d", targetPosition);
|
||||
Log(L"QueryContextMenu called. MenuText: %s", m_szMenuText);
|
||||
|
||||
// Dont show if no command configured
|
||||
if (m_szCommand[0] == L'\0') {
|
||||
Log(L"QueryContextMenu - Error: Command is empty!");
|
||||
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0);
|
||||
}
|
||||
|
||||
HICON hIcon = NULL;
|
||||
HBITMAP hBitmap = NULL;
|
||||
|
||||
// Extract icon
|
||||
if (m_szIcon[0] != L'\0')
|
||||
{
|
||||
Log(L"QueryContextMenu - Icon configured: %s", m_szIcon);
|
||||
// Try to find index if comma exists e.g. "dll,1"
|
||||
// Simple extraction for now
|
||||
ExtractIconEx(m_szIcon, 0, NULL, &hIcon, 1);
|
||||
}
|
||||
|
||||
if (hIcon)
|
||||
{
|
||||
hBitmap = IconToBitmap(hIcon);
|
||||
DestroyIcon(hIcon);
|
||||
}
|
||||
|
||||
if (m_hMenuBitmap) { DeleteObject(m_hMenuBitmap); m_hMenuBitmap = NULL; }
|
||||
|
||||
MENUITEMINFO mii = { sizeof(mii) };
|
||||
mii.fMask = MIIM_STRING | MIIM_ID | MIIM_STATE | MIIM_BITMAP;
|
||||
mii.wID = idCmdFirst;
|
||||
mii.dwTypeData = m_szMenuText;
|
||||
mii.fState = MFS_ENABLED;
|
||||
mii.hbmpItem = hBitmap;
|
||||
|
||||
if (!InsertMenuItem(hMenu, targetPosition, TRUE, &mii))
|
||||
{
|
||||
if (hBitmap) DeleteObject(hBitmap);
|
||||
Log(L"QueryContextMenu - InsertMenuItem FAILED. Error: %d", GetLastError());
|
||||
return HRESULT_FROM_WIN32(GetLastError());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(L"QueryContextMenu - InsertMenuItem Success. ID: %d", idCmdFirst);
|
||||
}
|
||||
|
||||
m_hMenuBitmap = hBitmap;
|
||||
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 1);
|
||||
}
|
||||
|
||||
IFACEMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici)
|
||||
{
|
||||
LPOLESTR szCLSID = NULL;
|
||||
StringFromCLSID(m_clsid, &szCLSID);
|
||||
Log(L"UniversalShellExt::InvokeCommand called for CLSID: %s", szCLSID ? szCLSID : L"Unknown");
|
||||
|
||||
// Debug MessageBox removed for production
|
||||
|
||||
|
||||
// Debug Box (Temporary - UNCOMMENTED FOR DEBUG)
|
||||
// MessageBox(NULL, L"InvokeCommand Called!", L"Debug", MB_OK);
|
||||
|
||||
bool isMatch = false;
|
||||
|
||||
// 1. Check for Integer Offset (Standard Right-Click)
|
||||
if (HIWORD(pici->lpVerb) == 0)
|
||||
{
|
||||
// We only add 1 item, so offset 0 is the only valid one.
|
||||
if (LOWORD(pici->lpVerb) == 0) isMatch = true;
|
||||
}
|
||||
// 2. Check for String Verb (Programmatic/Windows 11)
|
||||
else
|
||||
{
|
||||
LPCSTR pszVerb = pici->lpVerb;
|
||||
// Compare with our CLSID string (which we return in GetCommandString)
|
||||
// Need to convert our WCHAR CLSID to MultiByte for comparison
|
||||
if (szCLSID) {
|
||||
// Construct EXPECTED verb: Verb_XXXX (without braces)
|
||||
WCHAR szVerbW[128];
|
||||
StringCchPrintf(szVerbW, 128, L"Verb_%s", szCLSID + 1); // Skip '{'
|
||||
size_t lenW = 0;
|
||||
StringCchLength(szVerbW, 128, &lenW);
|
||||
if (lenW > 0 && szVerbW[lenW-1] == L'}') szVerbW[lenW-1] = L'\0';
|
||||
|
||||
// Convert to ANSI for comparison
|
||||
int lenA = WideCharToMultiByte(CP_ACP, 0, szVerbW, -1, NULL, 0, NULL, NULL);
|
||||
char* pszVerbA = new (std::nothrow) char[lenA];
|
||||
if (pszVerbA) {
|
||||
WideCharToMultiByte(CP_ACP, 0, szVerbW, -1, pszVerbA, lenA, NULL, NULL);
|
||||
|
||||
if (lstrcmpiA(pszVerb, pszVerbA) == 0) isMatch = true;
|
||||
else if (lstrcmpiA(pszVerb, "UniversalShellExt") == 0) isMatch = true; // Fallback
|
||||
|
||||
delete[] pszVerbA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (szCLSID) CoTaskMemFree(szCLSID);
|
||||
|
||||
if (!isMatch) {
|
||||
Log(L"InvokeCommand - Verb mismatch. Rejecting.");
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
// If command is empty, try reloading config (fallback)
|
||||
if (m_szCommand[0] == L'\0') {
|
||||
Log(L"InvokeCommand - Command empty, reloading...");
|
||||
LoadConfiguration();
|
||||
}
|
||||
|
||||
// Check if command file exists
|
||||
if (m_szCommand[0] == L'\0' || GetFileAttributes(m_szCommand) == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
WCHAR szMsg[MAX_PATH + 128];
|
||||
if (m_szCommand[0] == L'\0')
|
||||
{
|
||||
StringCchCopy(szMsg, ARRAYSIZE(szMsg), L"Nessun comando configurato.\n\nConfigurare la chiave di registro:\nHKLM\\SOFTWARE\\UniversalShellExt\\Command");
|
||||
}
|
||||
else
|
||||
{
|
||||
StringCchPrintf(szMsg, ARRAYSIZE(szMsg), L"File non trovato:\n\n%s\n\nVerificare la configurazione nel registro.", m_szCommand);
|
||||
}
|
||||
Log(L"InvokeCommand - Error: %s", szMsg);
|
||||
MessageBox(NULL, szMsg, L"UniversalShellExt - Errore", MB_OK | MB_ICONERROR);
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
// Prepare Parameters
|
||||
// Replace %1 in m_szArguments with m_szSelectedPath (quoted)
|
||||
WCHAR szArgsExpanded[MAX_PATH * 2];
|
||||
WCHAR szPathQuoted[MAX_PATH + 2];
|
||||
StringCchPrintf(szPathQuoted, MAX_PATH+2, L"\"%s\"", m_szSelectedPath);
|
||||
|
||||
// Simple Replace of %1
|
||||
// Note: This is a basic implementation.
|
||||
// It searches for %1 and replaces it.
|
||||
|
||||
// Copy arguments to buffer
|
||||
WCHAR *pFound = wcsstr(m_szArguments, L"%1");
|
||||
if (pFound)
|
||||
{
|
||||
size_t prefixLen = pFound - m_szArguments;
|
||||
wcsncpy_s(szArgsExpanded, ARRAYSIZE(szArgsExpanded), m_szArguments, prefixLen);
|
||||
szArgsExpanded[prefixLen] = L'\0';
|
||||
StringCchCat(szArgsExpanded, ARRAYSIZE(szArgsExpanded), szPathQuoted); // Insert Path (quoted)
|
||||
StringCchCat(szArgsExpanded, ARRAYSIZE(szArgsExpanded), pFound + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No %1 found, just use arguments as is.
|
||||
// The provided snippet had a different logic here, appending the path if no %1.
|
||||
// Sticking to the original logic for now, which means if no %1, path is not appended.
|
||||
StringCchCopy(szArgsExpanded, ARRAYSIZE(szArgsExpanded), m_szArguments);
|
||||
}
|
||||
|
||||
SHELLEXECUTEINFO sei = { sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
sei.lpVerb = L"open";
|
||||
sei.lpFile = m_szCommand;
|
||||
sei.lpParameters = szArgsExpanded;
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
|
||||
// Set Working Directory to the folder containing the executable
|
||||
WCHAR szDir[MAX_PATH];
|
||||
StringCchCopy(szDir, MAX_PATH, m_szCommand);
|
||||
PathRemoveFileSpec(szDir); // Requires shlwapi.h and linking shlwapi.lib
|
||||
sei.lpDirectory = szDir;
|
||||
|
||||
Log(L"InvokeCommand - Executing: %s Params: %s Dir: %s", sei.lpFile, sei.lpParameters, sei.lpDirectory);
|
||||
|
||||
if (!ShellExecuteEx(&sei))
|
||||
{
|
||||
DWORD lastError = GetLastError();
|
||||
Log(L"InvokeCommand - ShellExecuteEx failed with error: %d", lastError);
|
||||
if (lastError == ERROR_ELEVATION_REQUIRED)
|
||||
{
|
||||
sei.lpVerb = L"runas";
|
||||
Log(L"InvokeCommand - Retrying with 'runas' verb.");
|
||||
ShellExecuteEx(&sei);
|
||||
} else {
|
||||
MessageBox(NULL, L"Failed to launch command.", L"Error", MB_ICONERROR);
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
if (sei.hProcess) CloseHandle(sei.hProcess);
|
||||
Log(L"InvokeCommand - Success");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP GetCommandString(UINT_PTR idCmd, UINT uType, UINT *pReserved, CHAR *pszName, UINT cchMax)
|
||||
{
|
||||
Log(L"GetCommandString called. uType: %d", uType);
|
||||
if (idCmd != 0) return E_INVALIDARG;
|
||||
|
||||
LoadConfiguration(); // Reload config
|
||||
|
||||
switch (uType)
|
||||
{
|
||||
case GCS_HELPTEXTW:
|
||||
StringCchCopyW((LPWSTR)pszName, cchMax, m_szMenuText);
|
||||
return S_OK;
|
||||
|
||||
case GCS_HELPTEXTA:
|
||||
{
|
||||
int len = WideCharToMultiByte(CP_ACP, 0, m_szMenuText, -1, NULL, 0, NULL, NULL);
|
||||
if (len <= cchMax) WideCharToMultiByte(CP_ACP, 0, m_szMenuText, -1, pszName, cchMax, NULL, NULL);
|
||||
}
|
||||
return S_OK;
|
||||
|
||||
case GCS_VERBW:
|
||||
case GCS_VALIDATEW:
|
||||
{
|
||||
LPOLESTR szCLSID = NULL;
|
||||
StringFromCLSID(m_clsid, &szCLSID);
|
||||
if (szCLSID)
|
||||
{
|
||||
// Sanitize Verb: replace {CLSID} with Verb_CLSID (no braces)
|
||||
WCHAR szVerb[128];
|
||||
StringCchPrintf(szVerb, 128, L"Verb_%s", szCLSID + 1); // Skip '{'
|
||||
size_t len = 0;
|
||||
StringCchLength(szVerb, 128, &len);
|
||||
if (len > 0 && szVerb[len-1] == L'}') szVerb[len-1] = L'\0';
|
||||
|
||||
Log(L"GetCommandString - Returning VERB: %s", szVerb);
|
||||
|
||||
if (uType == GCS_VERBW && pszName) {
|
||||
StringCchCopyW((LPWSTR)pszName, cchMax, szVerb);
|
||||
}
|
||||
CoTaskMemFree(szCLSID);
|
||||
return S_OK;
|
||||
}
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
case GCS_VERBA:
|
||||
case GCS_VALIDATEA:
|
||||
// Return "UniversalShellExt" for legacy ANSI, but maybe we should return Verb_GUID here too if possible?
|
||||
// Sticking to simplified "Valid" response for now to minimize charset risk.
|
||||
// If Verb is requested (ANSI), we return the generic string.
|
||||
if (uType == GCS_VERBA && pszName) {
|
||||
StringCchCopyA(pszName, cchMax, "UniversalShellExt");
|
||||
}
|
||||
return S_OK;
|
||||
|
||||
default:
|
||||
Log(L"GetCommandString - Unknown uType: %d", uType);
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
}
|
||||
|
||||
UniversalShellExt(REFCLSID clsid) : m_cRef(1), m_hMenuBitmap(NULL), m_clsid(clsid)
|
||||
{
|
||||
InterlockedIncrement(&g_cDllRef);
|
||||
|
||||
// Stringify CLSID for log
|
||||
LPOLESTR szCLSID = NULL;
|
||||
StringFromCLSID(m_clsid, &szCLSID);
|
||||
Log(L"UniversalShellExt::Constructor - CLSID: %s", szCLSID ? szCLSID : L"Unknown");
|
||||
if (szCLSID) CoTaskMemFree(szCLSID);
|
||||
|
||||
m_szSelectedPath[0] = L'\0';
|
||||
m_szCommand[0] = L'\0';
|
||||
m_szArguments[0] = L'\0';
|
||||
m_szIcon[0] = L'\0';
|
||||
m_szMenuText[0] = L'\0';
|
||||
m_bEnableLog = FALSE;
|
||||
}
|
||||
|
||||
protected:
|
||||
~UniversalShellExt()
|
||||
{
|
||||
LPOLESTR szCLSID = NULL;
|
||||
StringFromCLSID(m_clsid, &szCLSID);
|
||||
Log(L"UniversalShellExt::Destructor - CLSID: %s", szCLSID ? szCLSID : L"Unknown");
|
||||
if (szCLSID) CoTaskMemFree(szCLSID);
|
||||
|
||||
if (m_hMenuBitmap) DeleteObject(m_hMenuBitmap);
|
||||
InterlockedDecrement(&g_cDllRef);
|
||||
}
|
||||
|
||||
private:
|
||||
LONG m_cRef;
|
||||
WCHAR m_szSelectedPath[MAX_PATH];
|
||||
HBITMAP m_hMenuBitmap;
|
||||
CLSID m_clsid;
|
||||
// Member variables to store configuration for this instance
|
||||
WCHAR m_szCommand[MAX_PATH];
|
||||
WCHAR m_szArguments[MAX_PATH];
|
||||
WCHAR m_szIcon[MAX_PATH];
|
||||
WCHAR m_szMenuText[128];
|
||||
BOOL m_bEnableLog;
|
||||
|
||||
// Member helper for logging
|
||||
void Log(const wchar_t* format, ...) {
|
||||
if (!m_bEnableLog) return;
|
||||
|
||||
wchar_t buffer[1024];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
_vswprintf_p(buffer, 1024, format, args);
|
||||
va_end(args);
|
||||
|
||||
WriteLogFile(buffer);
|
||||
}
|
||||
|
||||
// Internal LoadConfiguration method
|
||||
void LoadConfiguration()
|
||||
{
|
||||
m_szCommand[0] = L'\0';
|
||||
m_szArguments[0] = L'\0';
|
||||
m_szIcon[0] = L'\0';
|
||||
m_szMenuText[0] = L'\0';
|
||||
|
||||
// Build registry path: Software\UniversalShellExt\{CLSID}
|
||||
WCHAR szKeyPath[256];
|
||||
WCHAR szClsid[64];
|
||||
StringFromGUID2(m_clsid, szClsid, ARRAYSIZE(szClsid));
|
||||
StringCchPrintf(szKeyPath, ARRAYSIZE(szKeyPath), L"%s%s", REG_KEY_BASE, szClsid);
|
||||
|
||||
HKEY hKey;
|
||||
DWORD dwSize;
|
||||
DWORD dwType = REG_SZ;
|
||||
|
||||
// Try to open key from HKLM (64-bit view)
|
||||
LSTATUS status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, szKeyPath, 0, KEY_READ | KEY_WOW64_64KEY, &hKey);
|
||||
if (status != ERROR_SUCCESS)
|
||||
{
|
||||
status = RegOpenKeyEx(HKEY_CURRENT_USER, szKeyPath, 0, KEY_READ, &hKey);
|
||||
}
|
||||
|
||||
if (status == ERROR_SUCCESS)
|
||||
{
|
||||
// Read Command
|
||||
dwSize = sizeof(m_szCommand);
|
||||
RegQueryValueEx(hKey, REG_VALUE_COMMAND, NULL, &dwType, (LPBYTE)m_szCommand, &dwSize);
|
||||
|
||||
// Read Arguments
|
||||
dwSize = sizeof(m_szArguments);
|
||||
RegQueryValueEx(hKey, REG_VALUE_ARGS, NULL, &dwType, (LPBYTE)m_szArguments, &dwSize);
|
||||
|
||||
// Read Icon
|
||||
dwSize = sizeof(m_szIcon);
|
||||
RegQueryValueEx(hKey, REG_VALUE_ICON, NULL, &dwType, (LPBYTE)m_szIcon, &dwSize);
|
||||
|
||||
// Read MenuText
|
||||
dwSize = sizeof(m_szMenuText);
|
||||
dwSize = sizeof(m_szMenuText);
|
||||
RegQueryValueEx(hKey, REG_VALUE_TEXT, NULL, &dwType, (LPBYTE)m_szMenuText, &dwSize);
|
||||
|
||||
// Read EnableLog
|
||||
DWORD dwLog = 0;
|
||||
dwSize = sizeof(dwLog);
|
||||
DWORD dwTypeDw = REG_DWORD;
|
||||
if (RegQueryValueEx(hKey, REG_VALUE_ENABLE_LOG, NULL, &dwTypeDw, (LPBYTE)&dwLog, &dwSize) == ERROR_SUCCESS) {
|
||||
m_bEnableLog = (dwLog != 0);
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
// Defaults
|
||||
if (m_szCommand[0] == L'\0') {
|
||||
// Nothing to run?
|
||||
}
|
||||
|
||||
if (m_szArguments[0] == L'\0') {
|
||||
StringCchCopy(m_szArguments, MAX_PATH, L"\"%1\"");
|
||||
}
|
||||
|
||||
if (m_szIcon[0] == L'\0') {
|
||||
// Default icon to command exe
|
||||
StringCchCopy(m_szIcon, MAX_PATH, m_szCommand);
|
||||
}
|
||||
|
||||
if (m_szMenuText[0] == L'\0') {
|
||||
StringCchCopy(m_szMenuText, 128, L"Open with Application");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// Class Factory
|
||||
//==============================================================================
|
||||
class ClassFactory : public IClassFactory
|
||||
{
|
||||
public:
|
||||
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv)
|
||||
{
|
||||
static const QITAB qit[] = { QITABENT(ClassFactory, IClassFactory), { 0 } };
|
||||
return QISearch(this, qit, riid, ppv);
|
||||
}
|
||||
IFACEMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&m_cRef); }
|
||||
IFACEMETHODIMP_(ULONG) Release()
|
||||
{
|
||||
ULONG cRef = InterlockedDecrement(&m_cRef);
|
||||
if (cRef == 0) delete this;
|
||||
return cRef;
|
||||
}
|
||||
IFACEMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv)
|
||||
{
|
||||
if (pUnkOuter != NULL) return CLASS_E_NOAGGREGATION;
|
||||
UniversalShellExt *pExt = new (std::nothrow) UniversalShellExt(m_clsid);
|
||||
if (pExt == NULL) return E_OUTOFMEMORY;
|
||||
HRESULT hr = pExt->QueryInterface(riid, ppv);
|
||||
pExt->Release();
|
||||
return hr;
|
||||
}
|
||||
IFACEMETHODIMP LockServer(BOOL fLock)
|
||||
{
|
||||
if (fLock) InterlockedIncrement(&g_cDllRef);
|
||||
else InterlockedDecrement(&g_cDllRef);
|
||||
return S_OK;
|
||||
}
|
||||
ClassFactory(const CLSID& clsid) : m_cRef(1), m_clsid(clsid) { }
|
||||
protected:
|
||||
~ClassFactory() { }
|
||||
private:
|
||||
LONG m_cRef;
|
||||
CLSID m_clsid;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// DLL Exports
|
||||
//==============================================================================
|
||||
|
||||
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv)
|
||||
{
|
||||
LPOLESTR szCLSID = NULL;
|
||||
StringFromCLSID(rclsid, &szCLSID);
|
||||
WriteLog(L"DllGetClassObject - Requesting CLSID: %s", szCLSID ? szCLSID : L"Unknown");
|
||||
if (szCLSID) CoTaskMemFree(szCLSID);
|
||||
|
||||
*ppv = NULL;
|
||||
if (!IsEqualIID(riid, IID_IClassFactory))
|
||||
return CLASS_E_CLASSNOTAVAILABLE;
|
||||
|
||||
ClassFactory* pFactory = new (std::nothrow) ClassFactory(rclsid); // Pass CLSID to factory
|
||||
if (!pFactory)
|
||||
return E_OUTOFMEMORY;
|
||||
|
||||
HRESULT hr = pFactory->QueryInterface(riid, ppv);
|
||||
pFactory->Release();
|
||||
return hr;
|
||||
}
|
||||
|
||||
STDAPI DllCanUnloadNow(void) { return g_cDllRef > 0 ? S_FALSE : S_OK; }
|
||||
|
||||
HRESULT SetRegistryValue(HKEY hKeyRoot, LPCWSTR pszSubKey, LPCWSTR pszValueName, LPCWSTR pszValue)
|
||||
{
|
||||
HKEY hKey;
|
||||
HRESULT hr = HRESULT_FROM_WIN32(RegCreateKeyEx(hKeyRoot, pszSubKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL));
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
if (pszValue != NULL)
|
||||
{
|
||||
DWORD cbData = (lstrlen(pszValue) + 1) * sizeof(WCHAR);
|
||||
hr = HRESULT_FROM_WIN32(RegSetValueEx(hKey, pszValueName, 0, REG_SZ, (const BYTE*)pszValue, cbData));
|
||||
}
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
|
||||
STDAPI DllRegisterServer(void)
|
||||
{
|
||||
WCHAR szModulePath[MAX_PATH];
|
||||
if (!GetModuleFileName(g_hInst, szModulePath, ARRAYSIZE(szModulePath))) return HRESULT_FROM_WIN32(GetLastError());
|
||||
|
||||
HRESULT hr = S_OK;
|
||||
hr = SetRegistryValue(HKEY_CLASSES_ROOT, L"CLSID\\" CLSID_STRING, NULL, L"Universal Portable Shell Extension");
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = SetRegistryValue(HKEY_CLASSES_ROOT, L"CLSID\\" CLSID_STRING L"\\InprocServer32", NULL, szModulePath);
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = SetRegistryValue(HKEY_CLASSES_ROOT, L"CLSID\\" CLSID_STRING L"\\InprocServer32", L"ThreadingModel", L"Apartment");
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
// Register Context Menu Handler
|
||||
hr = SetRegistryValue(HKEY_CLASSES_ROOT, L"Directory\\shellex\\ContextMenuHandlers\\UniversalShellExt", NULL, CLSID_STRING);
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = SetRegistryValue(HKEY_CLASSES_ROOT, L"Directory\\Background\\shellex\\ContextMenuHandlers\\UniversalShellExt", NULL, CLSID_STRING);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
// Approve
|
||||
hr = SetRegistryValue(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", CLSID_STRING, L"Universal Shell Extension");
|
||||
return hr;
|
||||
}
|
||||
|
||||
STDAPI DllUnregisterServer(void)
|
||||
{
|
||||
RegDeleteTree(HKEY_CLASSES_ROOT, L"CLSID\\" CLSID_STRING);
|
||||
RegDeleteTree(HKEY_CLASSES_ROOT, L"Directory\\shellex\\ContextMenuHandlers\\UniversalShellExt");
|
||||
RegDeleteTree(HKEY_CLASSES_ROOT, L"Directory\\Background\\shellex\\ContextMenuHandlers\\UniversalShellExt");
|
||||
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", 0, KEY_WRITE, &hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegDeleteValue(hKey, CLSID_STRING);
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
LIBRARY UniversalShellExt
|
||||
EXPORTS
|
||||
DllGetClassObject PRIVATE
|
||||
DllCanUnloadNow PRIVATE
|
||||
DllRegisterServer PRIVATE
|
||||
DllUnregisterServer PRIVATE
|
||||
@@ -0,0 +1,44 @@
|
||||
// VSCodeShellExt.rc - Version Information Resource
|
||||
|
||||
#include <winver.h>
|
||||
|
||||
#define VER_FILEVERSION 1,0,0,6
|
||||
#define VER_FILEVERSION_STR "1.0.0.6\0"
|
||||
#define VER_PRODUCTVERSION 1,0,0,6
|
||||
#define VER_PRODUCTVERSION_STR "1.0.0\0"
|
||||
|
||||
#define VER_COMPANYNAME_STR "Generic Shell Ext\0"
|
||||
#define VER_FILEDESCRIPTION_STR "Universal Portable Shell Extension\0"
|
||||
#define VER_INTERNALNAME_STR "UniversalShellExt\0"
|
||||
#define VER_LEGALCOPYRIGHT_STR "Copyright (C) 2024\0"
|
||||
#define VER_ORIGINALFILENAME_STR "UniversalShellExt64.dll\0"
|
||||
#define VER_PRODUCTNAME_STR "Universal Portable Shell Extension\0"
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION VER_FILEVERSION
|
||||
PRODUCTVERSION VER_PRODUCTVERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
FILEFLAGS 0
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", VER_COMPANYNAME_STR
|
||||
VALUE "FileDescription", VER_FILEDESCRIPTION_STR
|
||||
VALUE "FileVersion", VER_FILEVERSION_STR
|
||||
VALUE "InternalName", VER_INTERNALNAME_STR
|
||||
VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR
|
||||
VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR
|
||||
VALUE "ProductName", VER_PRODUCTNAME_STR
|
||||
VALUE "ProductVersion", VER_PRODUCTVERSION_STR
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<RootNamespace>VSCodeShellExt</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<OutDir>$(SolutionDir)bin\x86\</OutDir>
|
||||
<IntDir>$(Configuration)\x86\</IntDir>
|
||||
<TargetName>VSCodeShellExt32</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(SolutionDir)bin\x64\</OutDir>
|
||||
<IntDir>$(Configuration)\x64\</IntDir>
|
||||
<TargetName>UniversalShellExt64</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<ModuleDefinitionFile>UniversalShellExt.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<ModuleDefinitionFile>UniversalShellExt.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="UniversalShellExt.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="UniversalShellExt.def" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
Reference in New Issue
Block a user