zipfixx 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zipfixx-0.1.0/LICENSE +21 -0
- zipfixx-0.1.0/PKG-INFO +179 -0
- zipfixx-0.1.0/README.md +165 -0
- zipfixx-0.1.0/pyproject.toml +23 -0
- zipfixx-0.1.0/setup.cfg +4 -0
- zipfixx-0.1.0/tests/test_fixer.py +74 -0
- zipfixx-0.1.0/tests/test_winpathfix.py +26 -0
- zipfixx-0.1.0/zipfixx.egg-info/PKG-INFO +179 -0
- zipfixx-0.1.0/zipfixx.egg-info/SOURCES.txt +14 -0
- zipfixx-0.1.0/zipfixx.egg-info/dependency_links.txt +1 -0
- zipfixx-0.1.0/zipfixx.egg-info/entry_points.txt +2 -0
- zipfixx-0.1.0/zipfixx.egg-info/top_level.txt +1 -0
- zipfixx-0.1.0/zipflix/__init__.py +3 -0
- zipfixx-0.1.0/zipflix/__main__.py +4 -0
- zipfixx-0.1.0/zipflix/cli.py +59 -0
- zipfixx-0.1.0/zipflix/fixer.py +84 -0
zipfixx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SouzaF19
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
zipfixx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zipfixx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI para corrigir e extrair arquivos ZIP com nomes Unicode no Windows.
|
|
5
|
+
Author: SouzaF19
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# ZipFix
|
|
16
|
+
|
|
17
|
+
**Um CLI para corrigir e extrair arquivos ZIP com nomes Unicode no Windows.**
|
|
18
|
+
|
|
19
|
+
[](https://github.com/SouzaF19/zipfix/actions/workflows/ci.yml)
|
|
20
|
+
[](https://github.com/SouzaF19/zipfix/releases)
|
|
21
|
+
[](https://github.com/SouzaF19/zipfix/blob/main/LICENSE)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🎯 Sobre o problema
|
|
26
|
+
|
|
27
|
+
Ao descompactar arquivos `.zip` no Windows, nomes de arquivos que contêm caracteres
|
|
28
|
+
especiais ou Unicode (acentuação, símbolos, emojis) podem chegar corrompidos — virando
|
|
29
|
+
`????`, `ã`, ou sequências ininteligíveis.
|
|
30
|
+
|
|
31
|
+
Isso acontece porque a especificação do formato ZIP exige que nomes não-ASCII sejam
|
|
32
|
+
marcados com o *flag* de UTF-8 (bit `0x800`) na entrada. Dependendo da ferramenta que
|
|
33
|
+
escreveu ou leu o ZIP, esse flag é ignorado ou aplicado de forma inconsistente, resultando
|
|
34
|
+
em perda de caracteres.
|
|
35
|
+
|
|
36
|
+
### Por que isso importa
|
|
37
|
+
|
|
38
|
+
- Extrair um ZIP com `Compress-Archive` / `Expand-Archive` no PowerShell pode gerar nomes
|
|
39
|
+
quebrados;
|
|
40
|
+
- Prefixos e nomes com `ä`, `ç`, `ã`, `€`, emojis e combinações passam a ser ininteligíveis;
|
|
41
|
+
- Leitores em outras plataformas (Linux, Web, Android) reagem mal a nomes malformados.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🔧 A solução
|
|
46
|
+
|
|
47
|
+
O **ZipFix** é um pequeno CLI que:
|
|
48
|
+
|
|
49
|
+
1. Lê um arquivo `.zip`;
|
|
50
|
+
2. Valida e sanitiza cada entrada (remove caracteres proibidos e limita o tamanho);
|
|
51
|
+
3. Reescreve o ZIP com nomes seguros e com encoding correto (UTF-8);
|
|
52
|
+
4. Extrai os arquivos de forma determinística.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ✨ Funcionalidades
|
|
57
|
+
|
|
58
|
+
- ✅ Correção de nomes em entradas de ZIP;
|
|
59
|
+
- ✅ Sanitização de caracteres inválidos no Windows;
|
|
60
|
+
- ✅ Limitação de tamanho de nome;
|
|
61
|
+
- ✅ Reescrita de ZIP com encoding correto;
|
|
62
|
+
- ✅ Extração com nomes sanitizados;
|
|
63
|
+
- ✅ Relatório de entradas reescritas/extraídas.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🧰 Instalação
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install zipfix
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
ou, via ferramenta de empacotamento:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv tool install zipfix
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🚀 Uso
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Corrigir (reescrever) um ZIP
|
|
85
|
+
zipflix fix entrada.zip --out saida_corrigida.zip
|
|
86
|
+
|
|
87
|
+
# Extrair um ZIP com nomes sanitizados
|
|
88
|
+
zipflix extract entrada.zip destino/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Exemplo
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
zipflix fix ./downloads/arquivo.zip --out ./downloads/arquivo_corrigido.zip
|
|
95
|
+
zipflix extract ./downloads/arquivo_corrigido.zip ./extraido/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🧠 Como funciona
|
|
101
|
+
|
|
102
|
+
- Usa o módulo nativo de ZIP (stdlib) que trata UTF-8;
|
|
103
|
+
- Emite o novo arquivo com o flag UTF-8 nas entradas;
|
|
104
|
+
- Sanitiza nomes proibidos no Windows (`|`, `<`, `>`, `:`, `"`, `?`, `*`, `\`) e caracteres de controle.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 🗂️ Estrutura do projeto
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
C:/zipflix
|
|
112
|
+
├── zipflix/
|
|
113
|
+
│ ├── __init__.py
|
|
114
|
+
│ ├── cli.py
|
|
115
|
+
│ ├── fixer.py
|
|
116
|
+
│ └── __main__.py
|
|
117
|
+
├── tests/
|
|
118
|
+
│ └── test_fixer.py
|
|
119
|
+
├── .github/
|
|
120
|
+
│ └── workflows/
|
|
121
|
+
│ └── ci.yml
|
|
122
|
+
├── Makefile
|
|
123
|
+
├── pyproject.toml
|
|
124
|
+
├── README.md
|
|
125
|
+
└── LICENSE
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 🔭 Próximos passos
|
|
131
|
+
|
|
132
|
+
- Testes com ZIPs reais, incluindo nomes com emoji;
|
|
133
|
+
- Suporte a criptografia (AES).
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 📄 Licença
|
|
138
|
+
|
|
139
|
+
MIT
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📦 WinPathFix
|
|
144
|
+
|
|
145
|
+
**Um CLI para renomear arquivos soltos com caracteres invalidos no Windows.**
|
|
146
|
+
|
|
147
|
+
- Detecta e renomeia arquivos com caracteres proibidos (`|`, `<`, `>`, `:`, `"`, `?`, `*`, `\`).
|
|
148
|
+
- Preserva Unicode e estrutura de pastas.
|
|
149
|
+
- Limita tamanho de nome a 255 caracteres por segmento.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
winpathfix fix ./pastainvalida/
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 📦 PyPI
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install zipfix
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
ou
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pip install git+https://github.com/SouzaF19/zipfix.git
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📄 Licença
|
|
172
|
+
|
|
173
|
+
MIT — ver [LICENSE](LICENSE).
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 📬 Contato
|
|
178
|
+
|
|
179
|
+
- GitHub: [@SouzaF19](https://github.com/SouzaF19)
|
zipfixx-0.1.0/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# ZipFix
|
|
2
|
+
|
|
3
|
+
**Um CLI para corrigir e extrair arquivos ZIP com nomes Unicode no Windows.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/SouzaF19/zipfix/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/SouzaF19/zipfix/releases)
|
|
7
|
+
[](https://github.com/SouzaF19/zipfix/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🎯 Sobre o problema
|
|
12
|
+
|
|
13
|
+
Ao descompactar arquivos `.zip` no Windows, nomes de arquivos que contêm caracteres
|
|
14
|
+
especiais ou Unicode (acentuação, símbolos, emojis) podem chegar corrompidos — virando
|
|
15
|
+
`????`, `ã`, ou sequências ininteligíveis.
|
|
16
|
+
|
|
17
|
+
Isso acontece porque a especificação do formato ZIP exige que nomes não-ASCII sejam
|
|
18
|
+
marcados com o *flag* de UTF-8 (bit `0x800`) na entrada. Dependendo da ferramenta que
|
|
19
|
+
escreveu ou leu o ZIP, esse flag é ignorado ou aplicado de forma inconsistente, resultando
|
|
20
|
+
em perda de caracteres.
|
|
21
|
+
|
|
22
|
+
### Por que isso importa
|
|
23
|
+
|
|
24
|
+
- Extrair um ZIP com `Compress-Archive` / `Expand-Archive` no PowerShell pode gerar nomes
|
|
25
|
+
quebrados;
|
|
26
|
+
- Prefixos e nomes com `ä`, `ç`, `ã`, `€`, emojis e combinações passam a ser ininteligíveis;
|
|
27
|
+
- Leitores em outras plataformas (Linux, Web, Android) reagem mal a nomes malformados.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🔧 A solução
|
|
32
|
+
|
|
33
|
+
O **ZipFix** é um pequeno CLI que:
|
|
34
|
+
|
|
35
|
+
1. Lê um arquivo `.zip`;
|
|
36
|
+
2. Valida e sanitiza cada entrada (remove caracteres proibidos e limita o tamanho);
|
|
37
|
+
3. Reescreve o ZIP com nomes seguros e com encoding correto (UTF-8);
|
|
38
|
+
4. Extrai os arquivos de forma determinística.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ✨ Funcionalidades
|
|
43
|
+
|
|
44
|
+
- ✅ Correção de nomes em entradas de ZIP;
|
|
45
|
+
- ✅ Sanitização de caracteres inválidos no Windows;
|
|
46
|
+
- ✅ Limitação de tamanho de nome;
|
|
47
|
+
- ✅ Reescrita de ZIP com encoding correto;
|
|
48
|
+
- ✅ Extração com nomes sanitizados;
|
|
49
|
+
- ✅ Relatório de entradas reescritas/extraídas.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🧰 Instalação
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install zipfix
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
ou, via ferramenta de empacotamento:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv tool install zipfix
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🚀 Uso
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Corrigir (reescrever) um ZIP
|
|
71
|
+
zipflix fix entrada.zip --out saida_corrigida.zip
|
|
72
|
+
|
|
73
|
+
# Extrair um ZIP com nomes sanitizados
|
|
74
|
+
zipflix extract entrada.zip destino/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Exemplo
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
zipflix fix ./downloads/arquivo.zip --out ./downloads/arquivo_corrigido.zip
|
|
81
|
+
zipflix extract ./downloads/arquivo_corrigido.zip ./extraido/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 🧠 Como funciona
|
|
87
|
+
|
|
88
|
+
- Usa o módulo nativo de ZIP (stdlib) que trata UTF-8;
|
|
89
|
+
- Emite o novo arquivo com o flag UTF-8 nas entradas;
|
|
90
|
+
- Sanitiza nomes proibidos no Windows (`|`, `<`, `>`, `:`, `"`, `?`, `*`, `\`) e caracteres de controle.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 🗂️ Estrutura do projeto
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
C:/zipflix
|
|
98
|
+
├── zipflix/
|
|
99
|
+
│ ├── __init__.py
|
|
100
|
+
│ ├── cli.py
|
|
101
|
+
│ ├── fixer.py
|
|
102
|
+
│ └── __main__.py
|
|
103
|
+
├── tests/
|
|
104
|
+
│ └── test_fixer.py
|
|
105
|
+
├── .github/
|
|
106
|
+
│ └── workflows/
|
|
107
|
+
│ └── ci.yml
|
|
108
|
+
├── Makefile
|
|
109
|
+
├── pyproject.toml
|
|
110
|
+
├── README.md
|
|
111
|
+
└── LICENSE
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🔭 Próximos passos
|
|
117
|
+
|
|
118
|
+
- Testes com ZIPs reais, incluindo nomes com emoji;
|
|
119
|
+
- Suporte a criptografia (AES).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 📄 Licença
|
|
124
|
+
|
|
125
|
+
MIT
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 📦 WinPathFix
|
|
130
|
+
|
|
131
|
+
**Um CLI para renomear arquivos soltos com caracteres invalidos no Windows.**
|
|
132
|
+
|
|
133
|
+
- Detecta e renomeia arquivos com caracteres proibidos (`|`, `<`, `>`, `:`, `"`, `?`, `*`, `\`).
|
|
134
|
+
- Preserva Unicode e estrutura de pastas.
|
|
135
|
+
- Limita tamanho de nome a 255 caracteres por segmento.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
winpathfix fix ./pastainvalida/
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📦 PyPI
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pip install zipfix
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
ou
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install git+https://github.com/SouzaF19/zipfix.git
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 📄 Licença
|
|
158
|
+
|
|
159
|
+
MIT — ver [LICENSE](LICENSE).
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 📬 Contato
|
|
164
|
+
|
|
165
|
+
- GitHub: [@SouzaF19](https://github.com/SouzaF19)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zipfixx"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI para corrigir e extrair arquivos ZIP com nomes Unicode no Windows."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "SouzaF19" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
zipfix = "zipflix.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
packages = ["zipflix"]
|
zipfixx-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Testes do ZipFix: sanitização e reescrita/extração de nomes em ZIP."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
import zipfile
|
|
7
|
+
|
|
8
|
+
from zipflix import fixer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestFixer(unittest.TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
14
|
+
self.addCleanup(self.tmp.cleanup)
|
|
15
|
+
self.tmpdir = self.tmp.name
|
|
16
|
+
|
|
17
|
+
def _make_zip(self, path):
|
|
18
|
+
entries = {
|
|
19
|
+
"docs/relatorio-final.txt": b"conteudo final\n",
|
|
20
|
+
"docs/relatorio com acento-ãã.txt": b"acentos\n",
|
|
21
|
+
"emoji-🚀-teste.txt": b"emoji\n",
|
|
22
|
+
"invalid|name*.txt": b"invalido\n",
|
|
23
|
+
}
|
|
24
|
+
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
25
|
+
for name, data in entries.items():
|
|
26
|
+
info = zipfile.ZipInfo(filename=name, date_time=(2026, 11, 7, 12, 0, 0))
|
|
27
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
28
|
+
zf.writestr(info, data)
|
|
29
|
+
|
|
30
|
+
def test_sanitize_name(self):
|
|
31
|
+
self.assertEqual(fixer.sanitize_name("docs/relatorio-ãã.txt"), "docs/relatorio-ãã.txt")
|
|
32
|
+
self.assertEqual(fixer.sanitize_name("invalid|name*.txt"), "invalidname.txt")
|
|
33
|
+
self.assertEqual(fixer.sanitize_name("bad:char*|name?.txt"), "badcharname.txt")
|
|
34
|
+
|
|
35
|
+
def test_sanitize_blocks_parent_traversal(self):
|
|
36
|
+
self.assertEqual(fixer.sanitize_name("../../evil.txt"), "evil.txt")
|
|
37
|
+
self.assertEqual(fixer.sanitize_name("../evil.txt"), "evil.txt")
|
|
38
|
+
self.assertNotIn("..", fixer.sanitize_name("../../evil.txt"))
|
|
39
|
+
|
|
40
|
+
def test_sanitize_truncates_per_segment(self):
|
|
41
|
+
long_name = "a" * 300
|
|
42
|
+
self.assertEqual(len(fixer.sanitize_name(long_name)) <= 255, True)
|
|
43
|
+
|
|
44
|
+
def test_fix_zip_rewrites_names(self):
|
|
45
|
+
src = os.path.join(self.tmpdir, "entrada.zip")
|
|
46
|
+
dst = os.path.join(self.tmpdir, "saida.zip")
|
|
47
|
+
self._make_zip(src)
|
|
48
|
+
results = fixer.fix_zip(src, dst)
|
|
49
|
+
self.assertTrue(len(results) > 0)
|
|
50
|
+
with zipfile.ZipFile(dst, "r") as zf:
|
|
51
|
+
names = zf.namelist()
|
|
52
|
+
self.assertIn("docs/relatorio-final.txt", names)
|
|
53
|
+
self.assertIn("docs/relatorio com acento-ãã.txt", names)
|
|
54
|
+
self.assertIn("emoji-🚀-teste.txt", names)
|
|
55
|
+
self.assertIn("invalidname.txt", names)
|
|
56
|
+
self.assertNotIn("invalid|name*.txt", names)
|
|
57
|
+
|
|
58
|
+
def test_fix_zip_same_path_raises(self):
|
|
59
|
+
src = os.path.join(self.tmpdir, "entrada.zip")
|
|
60
|
+
self._make_zip(src)
|
|
61
|
+
with self.assertRaises(ValueError):
|
|
62
|
+
fixer.fix_zip(src, src)
|
|
63
|
+
|
|
64
|
+
def test_extract_zip_sanitizes(self):
|
|
65
|
+
src = os.path.join(self.tmpdir, "entrada2.zip")
|
|
66
|
+
dest = os.path.join(self.tmpdir, "destino")
|
|
67
|
+
self._make_zip(src)
|
|
68
|
+
count = fixer.extract_zip(src, dest)
|
|
69
|
+
self.assertEqual(count, 4)
|
|
70
|
+
self.assertTrue(os.path.exists(os.path.join(dest, "emoji-🚀-teste.txt")))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
unittest.main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Testes do WinPathFix: coleta de renomes em diretorio."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from winpathfix import fixer
|
|
8
|
+
from winpathfix import cli
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestWinPathFix(unittest.TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
14
|
+
self.addCleanup(self.tmp.cleanup)
|
|
15
|
+
self.tmpdir = self.tmp.name
|
|
16
|
+
|
|
17
|
+
def test_collect_renames_empty_for_clean_dir(self):
|
|
18
|
+
open(os.path.join(self.tmpdir, "ok_renome.txt"), "w").write("x")
|
|
19
|
+
self.assertEqual(fixer.collect_renames(self.tmpdir), [])
|
|
20
|
+
|
|
21
|
+
def test_collect_renames_blocks_traversal(self):
|
|
22
|
+
self.assertEqual(fixer.sanitize_name("../../evil.txt"), "evil.txt")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
unittest.main()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zipfixx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI para corrigir e extrair arquivos ZIP com nomes Unicode no Windows.
|
|
5
|
+
Author: SouzaF19
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# ZipFix
|
|
16
|
+
|
|
17
|
+
**Um CLI para corrigir e extrair arquivos ZIP com nomes Unicode no Windows.**
|
|
18
|
+
|
|
19
|
+
[](https://github.com/SouzaF19/zipfix/actions/workflows/ci.yml)
|
|
20
|
+
[](https://github.com/SouzaF19/zipfix/releases)
|
|
21
|
+
[](https://github.com/SouzaF19/zipfix/blob/main/LICENSE)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🎯 Sobre o problema
|
|
26
|
+
|
|
27
|
+
Ao descompactar arquivos `.zip` no Windows, nomes de arquivos que contêm caracteres
|
|
28
|
+
especiais ou Unicode (acentuação, símbolos, emojis) podem chegar corrompidos — virando
|
|
29
|
+
`????`, `ã`, ou sequências ininteligíveis.
|
|
30
|
+
|
|
31
|
+
Isso acontece porque a especificação do formato ZIP exige que nomes não-ASCII sejam
|
|
32
|
+
marcados com o *flag* de UTF-8 (bit `0x800`) na entrada. Dependendo da ferramenta que
|
|
33
|
+
escreveu ou leu o ZIP, esse flag é ignorado ou aplicado de forma inconsistente, resultando
|
|
34
|
+
em perda de caracteres.
|
|
35
|
+
|
|
36
|
+
### Por que isso importa
|
|
37
|
+
|
|
38
|
+
- Extrair um ZIP com `Compress-Archive` / `Expand-Archive` no PowerShell pode gerar nomes
|
|
39
|
+
quebrados;
|
|
40
|
+
- Prefixos e nomes com `ä`, `ç`, `ã`, `€`, emojis e combinações passam a ser ininteligíveis;
|
|
41
|
+
- Leitores em outras plataformas (Linux, Web, Android) reagem mal a nomes malformados.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🔧 A solução
|
|
46
|
+
|
|
47
|
+
O **ZipFix** é um pequeno CLI que:
|
|
48
|
+
|
|
49
|
+
1. Lê um arquivo `.zip`;
|
|
50
|
+
2. Valida e sanitiza cada entrada (remove caracteres proibidos e limita o tamanho);
|
|
51
|
+
3. Reescreve o ZIP com nomes seguros e com encoding correto (UTF-8);
|
|
52
|
+
4. Extrai os arquivos de forma determinística.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ✨ Funcionalidades
|
|
57
|
+
|
|
58
|
+
- ✅ Correção de nomes em entradas de ZIP;
|
|
59
|
+
- ✅ Sanitização de caracteres inválidos no Windows;
|
|
60
|
+
- ✅ Limitação de tamanho de nome;
|
|
61
|
+
- ✅ Reescrita de ZIP com encoding correto;
|
|
62
|
+
- ✅ Extração com nomes sanitizados;
|
|
63
|
+
- ✅ Relatório de entradas reescritas/extraídas.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🧰 Instalação
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install zipfix
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
ou, via ferramenta de empacotamento:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv tool install zipfix
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🚀 Uso
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Corrigir (reescrever) um ZIP
|
|
85
|
+
zipflix fix entrada.zip --out saida_corrigida.zip
|
|
86
|
+
|
|
87
|
+
# Extrair um ZIP com nomes sanitizados
|
|
88
|
+
zipflix extract entrada.zip destino/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Exemplo
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
zipflix fix ./downloads/arquivo.zip --out ./downloads/arquivo_corrigido.zip
|
|
95
|
+
zipflix extract ./downloads/arquivo_corrigido.zip ./extraido/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🧠 Como funciona
|
|
101
|
+
|
|
102
|
+
- Usa o módulo nativo de ZIP (stdlib) que trata UTF-8;
|
|
103
|
+
- Emite o novo arquivo com o flag UTF-8 nas entradas;
|
|
104
|
+
- Sanitiza nomes proibidos no Windows (`|`, `<`, `>`, `:`, `"`, `?`, `*`, `\`) e caracteres de controle.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 🗂️ Estrutura do projeto
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
C:/zipflix
|
|
112
|
+
├── zipflix/
|
|
113
|
+
│ ├── __init__.py
|
|
114
|
+
│ ├── cli.py
|
|
115
|
+
│ ├── fixer.py
|
|
116
|
+
│ └── __main__.py
|
|
117
|
+
├── tests/
|
|
118
|
+
│ └── test_fixer.py
|
|
119
|
+
├── .github/
|
|
120
|
+
│ └── workflows/
|
|
121
|
+
│ └── ci.yml
|
|
122
|
+
├── Makefile
|
|
123
|
+
├── pyproject.toml
|
|
124
|
+
├── README.md
|
|
125
|
+
└── LICENSE
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 🔭 Próximos passos
|
|
131
|
+
|
|
132
|
+
- Testes com ZIPs reais, incluindo nomes com emoji;
|
|
133
|
+
- Suporte a criptografia (AES).
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 📄 Licença
|
|
138
|
+
|
|
139
|
+
MIT
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📦 WinPathFix
|
|
144
|
+
|
|
145
|
+
**Um CLI para renomear arquivos soltos com caracteres invalidos no Windows.**
|
|
146
|
+
|
|
147
|
+
- Detecta e renomeia arquivos com caracteres proibidos (`|`, `<`, `>`, `:`, `"`, `?`, `*`, `\`).
|
|
148
|
+
- Preserva Unicode e estrutura de pastas.
|
|
149
|
+
- Limita tamanho de nome a 255 caracteres por segmento.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
winpathfix fix ./pastainvalida/
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 📦 PyPI
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install zipfix
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
ou
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pip install git+https://github.com/SouzaF19/zipfix.git
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📄 Licença
|
|
172
|
+
|
|
173
|
+
MIT — ver [LICENSE](LICENSE).
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 📬 Contato
|
|
178
|
+
|
|
179
|
+
- GitHub: [@SouzaF19](https://github.com/SouzaF19)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_fixer.py
|
|
5
|
+
tests/test_winpathfix.py
|
|
6
|
+
zipfixx.egg-info/PKG-INFO
|
|
7
|
+
zipfixx.egg-info/SOURCES.txt
|
|
8
|
+
zipfixx.egg-info/dependency_links.txt
|
|
9
|
+
zipfixx.egg-info/entry_points.txt
|
|
10
|
+
zipfixx.egg-info/top_level.txt
|
|
11
|
+
zipflix/__init__.py
|
|
12
|
+
zipflix/__main__.py
|
|
13
|
+
zipflix/cli.py
|
|
14
|
+
zipflix/fixer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zipflix
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Interface de linha de comando do ZipFix."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
import zipfile
|
|
6
|
+
|
|
7
|
+
from zipflix import __version__
|
|
8
|
+
from zipflix import fixer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="zipflix",
|
|
14
|
+
description="Corrige e extrai arquivos ZIP com nomes Unicode no Windows.",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("--version", action="version", version=f"zipflix {__version__}")
|
|
17
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
18
|
+
|
|
19
|
+
p_fix = sub.add_parser("fix", help="Reescreve um ZIP com nomes sanitizados.")
|
|
20
|
+
p_fix.add_argument("input", help="Caminho do ZIP de entrada.")
|
|
21
|
+
p_fix.add_argument("--out", required=True, help="Caminho de saída do ZIP corrigido.")
|
|
22
|
+
|
|
23
|
+
p_extract = sub.add_parser("extract", help="Extrai um ZIP com nomes sanitizados.")
|
|
24
|
+
p_extract.add_argument("input", help="Caminho do ZIP de entrada.")
|
|
25
|
+
p_extract.add_argument("dest", help="Diretório de destino da extração.")
|
|
26
|
+
|
|
27
|
+
return parser
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(argv=None) -> int:
|
|
31
|
+
for stream in (sys.stdout, sys.stderr):
|
|
32
|
+
try:
|
|
33
|
+
stream.reconfigure(encoding="utf-8")
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
parser = _build_parser()
|
|
37
|
+
args = parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
if args.command == "fix":
|
|
41
|
+
results = fixer.fix_zip(args.input, args.out)
|
|
42
|
+
print(f"OK: {len(results)} entries reescritas -> {args.out}")
|
|
43
|
+
for orig, new in results:
|
|
44
|
+
print(f" {orig} ==> {new}")
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
if args.command == "extract":
|
|
48
|
+
count = fixer.extract_zip(args.input, args.dest)
|
|
49
|
+
print(f"OK: {count} entries extraidos para {args.dest}")
|
|
50
|
+
return 0
|
|
51
|
+
except (OSError, ValueError, zipfile.BadZipFile) as exc:
|
|
52
|
+
print(f"zipflix: erro: {exc}", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
return 2
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Correção de nomes em arquivos ZIP com suporte a Unicode."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import zipfile
|
|
7
|
+
|
|
8
|
+
INVALID_CHARS = re.compile(r'[<>:\"|?*\x00-\x1f]')
|
|
9
|
+
MAX_NAME_LENGTH = 255
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sanitize_name(name: str) -> str:
|
|
13
|
+
r"""Remove caracteres proibidos no Windows, descarta segmentos perigosos e limita tamanhos.
|
|
14
|
+
|
|
15
|
+
- Normaliza `\` -> `/`.
|
|
16
|
+
- Remove caracteres inválidos (<>:"/\|?* e controle) e descarta segmentos vazios/`.`/`..`.
|
|
17
|
+
- Limita cada componente a `MAX_NAME_LENGTH`.
|
|
18
|
+
"""
|
|
19
|
+
cleaned = name.replace("\\", "/")
|
|
20
|
+
trailing = cleaned.endswith("/")
|
|
21
|
+
parts = []
|
|
22
|
+
for part in cleaned.split("/"):
|
|
23
|
+
p = INVALID_CHARS.sub("", part).strip()
|
|
24
|
+
if p and p not in (".", ".."):
|
|
25
|
+
parts.append(p)
|
|
26
|
+
cleaned = "/".join(parts)
|
|
27
|
+
cleaned = cleaned.rstrip(".")
|
|
28
|
+
segments = cleaned.split("/") if cleaned else []
|
|
29
|
+
cleaned = "/".join(seg[:MAX_NAME_LENGTH].rstrip() for seg in segments)
|
|
30
|
+
if trailing and cleaned and not cleaned.endswith("/"):
|
|
31
|
+
cleaned = cleaned + "/"
|
|
32
|
+
return cleaned
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def read_entries(path: str) -> list:
|
|
36
|
+
with zipfile.ZipFile(path, "r") as zf:
|
|
37
|
+
return zf.infolist()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _clean_entries(path: str) -> list:
|
|
41
|
+
cleaned = []
|
|
42
|
+
seen = set()
|
|
43
|
+
with zipfile.ZipFile(path, "r") as zf:
|
|
44
|
+
for info in zf.infolist():
|
|
45
|
+
name = sanitize_name(info.filename)
|
|
46
|
+
if not name or name in seen:
|
|
47
|
+
continue
|
|
48
|
+
seen.add(name)
|
|
49
|
+
cleaned.append((name, info))
|
|
50
|
+
return cleaned
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fix_zip(src: str, dst: str) -> list:
|
|
54
|
+
"""Reescreve o ZIP de entrada com nomes sanitizados (UTF-8) em um novo ZIP."""
|
|
55
|
+
if os.path.abspath(src) == os.path.abspath(dst):
|
|
56
|
+
raise ValueError("src and dst must differ")
|
|
57
|
+
results = []
|
|
58
|
+
with zipfile.ZipFile(src, "r") as zin, \
|
|
59
|
+
zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
60
|
+
for name, info in _clean_entries(src):
|
|
61
|
+
new_info = zipfile.ZipInfo(filename=name, date_time=info.date_time)
|
|
62
|
+
new_info.compress_type = info.compress_type
|
|
63
|
+
new_info.external_attr = info.external_attr
|
|
64
|
+
new_info.create_system = info.create_system
|
|
65
|
+
zout.writestr(new_info, zin.read(info))
|
|
66
|
+
results.append((info.filename, name))
|
|
67
|
+
return results
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_zip(src: str, dest: str) -> int:
|
|
71
|
+
"""Extrai um ZIP para o diretório, com nomes sanitizados."""
|
|
72
|
+
os.makedirs(dest, exist_ok=True)
|
|
73
|
+
count = 0
|
|
74
|
+
with zipfile.ZipFile(src, "r") as zf:
|
|
75
|
+
for name, info in _clean_entries(src):
|
|
76
|
+
target = os.path.join(dest, *name.split("/"))
|
|
77
|
+
if info.filename.endswith("/"):
|
|
78
|
+
os.makedirs(target, exist_ok=True)
|
|
79
|
+
continue
|
|
80
|
+
os.makedirs(os.path.dirname(target) or dest, exist_ok=True)
|
|
81
|
+
with zf.open(info) as srcf, open(target, "wb") as f:
|
|
82
|
+
shutil.copyfileobj(srcf, f)
|
|
83
|
+
count += 1
|
|
84
|
+
return count
|