|
11 | 11 | import tempfile |
12 | 12 | import zipfile |
13 | 13 | import shlex |
| 14 | +import tarfile |
14 | 15 | from io import BytesIO |
15 | 16 | from functools import partial |
16 | 17 |
|
@@ -144,6 +145,11 @@ def _build_ui(self): |
144 | 145 | # command display |
145 | 146 | layout.addWidget(QLabel('Comando FFmpeg:')) |
146 | 147 | self.command_display = QTextEdit(); self.command_display.setReadOnly(True) |
| 148 | + # cap log length to avoid unbounded growth (drop oldest lines automatically) |
| 149 | + try: |
| 150 | + self.command_display.document().setMaximumBlockCount(2000) |
| 151 | + except Exception: |
| 152 | + pass |
147 | 153 | layout.addWidget(self.command_display) |
148 | 154 |
|
149 | 155 | # buttons |
@@ -338,28 +344,26 @@ def _on_proc_output(self): |
338 | 344 | text = str(data) |
339 | 345 | self._append_log(text) |
340 | 346 |
|
341 | | - def _on_proc_finished(self, code, status): |
342 | | - ok = (code == 0) |
| 347 | + def _finalize_proc(self): |
343 | 348 | self.convert_btn.setEnabled(True) |
344 | 349 | self.cancel_btn.setEnabled(False) |
345 | 350 | self._proc = None |
| 351 | + |
| 352 | + def _on_proc_finished(self, code, status): |
| 353 | + ok = (code == 0) |
| 354 | + self._finalize_proc() |
346 | 355 | if ok: |
347 | 356 | QtWidgets.QMessageBox.information(self, 'Sucesso', 'Vídeo convertido com sucesso!') |
348 | 357 | else: |
349 | 358 | QtWidgets.QMessageBox.critical(self, 'Erro', f'Falha ao converter vídeo (código {code}).') |
350 | 359 |
|
351 | 360 | def _on_proc_error(self, err): |
352 | | - self.convert_btn.setEnabled(True) |
353 | | - self.cancel_btn.setEnabled(False) |
354 | | - self._proc = None |
| 361 | + self._finalize_proc() |
355 | 362 | QtWidgets.QMessageBox.critical(self, 'Erro', f'Erro de processo: {err}') |
356 | 363 |
|
357 | 364 | def _append_log(self, msg): |
358 | | - prev = self.command_display.toPlainText() |
359 | | - if prev: |
360 | | - self.command_display.setPlainText(prev + "\n" + msg) |
361 | | - else: |
362 | | - self.command_display.setPlainText(msg) |
| 365 | + # Efficient append without rewriting whole buffer |
| 366 | + self.command_display.append(msg) |
363 | 367 |
|
364 | 368 | def show_about(self): |
365 | 369 | QtWidgets.QMessageBox.information(self, 'About', 'Mauricio Menon (+AI)\nhttps://github.com/mauriciomenon\nPyQt6 version of the GUI') |
@@ -450,7 +454,7 @@ def _extract_ffmpeg_zip(self, zip_bytes): |
450 | 454 | base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bin')) |
451 | 455 | os.makedirs(base_dir, exist_ok=True) |
452 | 456 | with zipfile.ZipFile(BytesIO(zip_bytes)) as zf: |
453 | | - zf.extractall(base_dir) |
| 457 | + self._safe_zip_extract(zf, base_dir) |
454 | 458 | # try to find ffmpeg(.exe) |
455 | 459 | ffmpeg_path = None |
456 | 460 | for root, dirs, files in os.walk(base_dir): |
@@ -493,9 +497,8 @@ def worker(): |
493 | 497 | # extrair para ./bin e apontar para bin/ffmpeg |
494 | 498 | base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bin')) |
495 | 499 | os.makedirs(base_dir, exist_ok=True) |
496 | | - import tarfile |
497 | 500 | with tarfile.open(fileobj=BytesIO(content), mode='r:xz') as tf: |
498 | | - tf.extractall(base_dir) |
| 501 | + self._safe_tar_extract(tf, base_dir) |
499 | 502 | # procurar binário |
500 | 503 | ffmpeg_path = None |
501 | 504 | for root, dirs, files in os.walk(base_dir): |
@@ -558,6 +561,37 @@ def show_info_msg(self, msg): |
558 | 561 | def show_error_msg(self, msg): |
559 | 562 | QtWidgets.QMessageBox.critical(self, 'Erro', msg) |
560 | 563 |
|
| 564 | + # ---- security helpers ---- |
| 565 | + def _safe_tar_extract(self, tf: tarfile.TarFile, base_dir: str): |
| 566 | + base = os.path.realpath(base_dir) |
| 567 | + for member in tf.getmembers(): |
| 568 | + member_path = os.path.realpath(os.path.join(base, member.name)) |
| 569 | + if not member_path.startswith(base + os.sep) and member_path != base: |
| 570 | + raise RuntimeError(f"Entrada insegura no tar: {member.name}") |
| 571 | + tf.extractall(base) |
| 572 | + |
| 573 | + def _safe_zip_extract(self, zf: zipfile.ZipFile, base_dir: str): |
| 574 | + base = os.path.realpath(base_dir) |
| 575 | + for zi in zf.infolist(): |
| 576 | + name = zi.filename |
| 577 | + # reject absolute paths or drive letters |
| 578 | + if os.path.isabs(name) or ( |
| 579 | + len(name) > 1 and name[1] == ':' |
| 580 | + ): |
| 581 | + raise RuntimeError(f"Entrada insegura no zip (absoluta): {name}") |
| 582 | + dest = os.path.realpath(os.path.join(base, name)) |
| 583 | + if not dest.startswith(base + os.sep) and dest != base: |
| 584 | + raise RuntimeError(f"Entrada insegura no zip: {name}") |
| 585 | + # after validation, extract members |
| 586 | + for zi in zf.infolist(): |
| 587 | + dest = os.path.realpath(os.path.join(base, zi.filename)) |
| 588 | + if zi.is_dir() or zi.filename.endswith('/'): |
| 589 | + os.makedirs(dest, exist_ok=True) |
| 590 | + continue |
| 591 | + os.makedirs(os.path.dirname(dest), exist_ok=True) |
| 592 | + with zf.open(zi, 'r') as src, open(dest, 'wb') as out: |
| 593 | + out.write(src.read()) |
| 594 | + |
561 | 595 | if __name__ == '__main__': |
562 | 596 | app = QApplication(sys.argv) |
563 | 597 | w = FFmpegGuiPyQt6() |
|
0 commit comments