chore: initial commit (extracted from Launchers monorepo)

Plugin: UniversalShellExt v1.0.0
Architectures: x64
License: MIT
This commit is contained in:
Simone
2026-04-29 14:06:20 +02:00
commit 6ac95e43cb
21 changed files with 1947 additions and 0 deletions
+24
View File
@@ -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
+52
View File
@@ -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
+76
View File
@@ -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.)
+8
View File
@@ -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
+31
View File
@@ -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. -->
+63
View File
@@ -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
+99
View File
@@ -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
View File
@@ -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/
+19
View File
@@ -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
View File
@@ -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`.
+21
View File
@@ -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.
+171
View File
@@ -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
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
1.0.0
+271
View File
@@ -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()
Binary file not shown.
+700
View File
@@ -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;
}
+6
View File
@@ -0,0 +1,6 @@
LIBRARY UniversalShellExt
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
+44
View File
@@ -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
+89
View File
@@ -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>