zipfixx 0.1.0__py3-none-any.whl

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.
@@ -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
+ [![CI](https://github.com/SouzaF19/zipfix/actions/workflows/ci.yml/badge.svg)](https://github.com/SouzaF19/zipfix/actions/workflows/ci.yml)
20
+ [![Release](https://img.shields.io/github/v/tag/SouzaF19/zipfix)](https://github.com/SouzaF19/zipfix/releases)
21
+ [![License](https://img.shields.io/badge/license-MIT-brightgreen)](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,10 @@
1
+ zipfixx-0.1.0.dist-info/licenses/LICENSE,sha256=-0LgJTg5ItbQUt96iJXvhM1AWd8vXhX14GZG5SaNp8E,1065
2
+ zipflix/__init__.py,sha256=is-P7gkWJVZhBD29AkOdwDekX4hjCk-Ntl01GFM9bc0,113
3
+ zipflix/__main__.py,sha256=Q5bWlxC9W0jbNlN4ITQJzwOJvbhpjhyQS8XWRTb6GNo,86
4
+ zipflix/cli.py,sha256=ekwPUyqBZR70e4WBJi0rYmuanLo9N4BF6c9DC7-X9-s,1922
5
+ zipflix/fixer.py,sha256=a8bpJid03QuZEyS9ZCm0eH5Ct_OVYGIt6YSxBN0rYXU,2942
6
+ zipfixx-0.1.0.dist-info/METADATA,sha256=nM403gQfKSo4K9i2CyZqcDPmyGxusbajJvVih0oojU4,4199
7
+ zipfixx-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ zipfixx-0.1.0.dist-info/entry_points.txt,sha256=ziBq2maYudE-hDZ7e__QtBl-hEV6P-aAb2dvABDTcvw,44
9
+ zipfixx-0.1.0.dist-info/top_level.txt,sha256=RfMSqKzbGEAMCjbaU8HJKbypUBpgRu-18eIPZJlIXq4,8
10
+ zipfixx-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zipfix = zipflix.cli:main
@@ -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.
@@ -0,0 +1 @@
1
+ zipflix
zipflix/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ZipFix - CLI para correção e extração segura de arquivos ZIP com nomes Unicode."""
2
+
3
+ __version__ = "0.1.0"
zipflix/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from zipflix.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
zipflix/cli.py ADDED
@@ -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())
zipflix/fixer.py ADDED
@@ -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