Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Build release executable

on:
release:
types: [published]

permissions:
contents: read

jobs:
build:
runs-on: windows-2022
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
architecture: x64
- name: Install package and executable builder
run: python -m pip install . pyinstaller==6.22.2
- name: Run offline tests
run: python -m unittest discover -s tests -v
- name: Build console executable
run: python -m PyInstaller --clean --noconfirm --onefile --console --name cc2flash --hidden-import hid packaging/entrypoint.py
- name: Smoke-test frozen application outside source tree
run: python packaging/smoke_test.py dist/cc2flash.exe
- name: Hash executable
shell: pwsh
run: |
$hash = (Get-FileHash dist/cc2flash.exe -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash cc2flash.exe" | Set-Content dist/cc2flash.exe.sha256 -Encoding ascii
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-executable
path: |
dist/cc2flash.exe
dist/cc2flash.exe.sha256
if-no-files-found: error

attach:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-executable
path: dist
- name: Attach to the existing manually published release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" dist/cc2flash.exe dist/cc2flash.exe.sha256
47 changes: 47 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Offline tests
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-2022, macos-latest]
python: ['3.10', '3.12']
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python }}
- run: python -m pip install .
- run: python -m unittest discover -s tests -v
- run: python -m cc2flash --help
- name: Install executable builder
if: matrix.os == 'windows-2022' && matrix.python == '3.12'
run: python -m pip install pyinstaller==6.22.2
- name: Build and test the Windows executable without publishing
if: matrix.os == 'windows-2022' && matrix.python == '3.12'
run: |
python -m PyInstaller --clean --noconfirm --onefile --console --name cc2flash --hidden-import hid packaging/entrypoint.py
if ($LASTEXITCODE -ne 0) { throw 'Executable build failed' }
python packaging/smoke_test.py dist/cc2flash.exe

workflow-audit:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- run: python -m pip install zizmor==1.30.0
- name: Audit all workflows
env:
GH_TOKEN: ${{ github.token }}
run: zizmor --strict-collection .github/workflows
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist/
Thumbs.db
*.swp
*~
*.spec
37 changes: 18 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,8 @@ and writes that image through USB without requiring an SPI programmer.
- access to a printer with a 0.2 mm nozzle;
- four P50 pogo pins, such as P50-J1;
- the USB-A plug and cable from an unused USB data cable;
- Python 3.10 or later;
- the Android platform `adb` executable;
- the optional Python `hidapi` package installed with `cc2flash`.
- [cc2flash installed](docs/INSTALLATION.md), either the Windows executable or Python package;
- the Android platform `adb` executable as described in the installation guide.

### Build the camera-to-USB cable

Expand Down Expand Up @@ -123,26 +122,28 @@ confirm that +5 V is not shorted to ground or either data line.
### USB prevention command sequence

Installation and detailed stop conditions are documented in the
[USB-maintenance reference](usb-maintenance/REFERENCE.md). After `cc2flash`
and `adb` are installed, the supported sequence is:
[USB-maintenance reference](usb-maintenance/REFERENCE.md). After following the [installation guide](docs/INSTALLATION.md) for `cc2flash`
and `adb`, the supported sequence is:

```sh
cc2flash start-adb
cc2flash backup backup.zip
python hardware-recovery/cc2_sig_tool.py build backup.zip
cc2flash plan-restore backup-cc2-recovery/cc2-camera-recovery.bin --backup backup.zip
cc2flash build-image backup.zip
cc2flash restore backup-cc2-recovery/cc2-camera-recovery.bin --backup backup.zip
```

On Windows, use `py` instead of `python` if that is how Python was installed.
Use the command prefix from your [platform's installation instructions](docs/INSTALLATION.md).
If ADB is not on PATH, add `--adb "path/to/adb"` to connected-camera commands,
but not to offline `build-image`. `restore --dry-run` accepts these options
without using them.

`start-adb` temporarily starts the camera's existing root ADB service.
`backup` then reads the complete flash repeatedly and publishes `backup.zip`
only after obtaining three consecutive identical images. Keep that ZIP
unchanged and in a separate safe location.

The builder validates the backup and creates the camera-specific preventive
image. `plan-restore` performs the same-camera and allowed-change checks
image. The optional `restore --dry-run` performs the same-camera and allowed-change checks
without opening USB. `restore` repeats those checks, validates the live camera
against the preserved backup, temporarily prepares the known stock SFC driver,
and requires the literal confirmation `RESTORE-CC2` before its first write.
Expand All @@ -155,7 +156,7 @@ generated image. Do not work around a validation failure.
## Failed camera: hardware recovery

This route reads and rewrites the camera's eight-pin SPI flash with an external
programmer. The recommended beginner setup uses the NeoProgrammer graphical
programmer. The recommended setup uses the NeoProgrammer graphical
application for every flash operation; only the recovery-image build requires
a command line.

Expand All @@ -166,8 +167,7 @@ a command line.
- an SOIC-8 test clip and cable;
- a multimeter;
- NeoProgrammer;
- Python 3.10 or later;
- the files from this repository.
- [cc2flash installed](docs/INSTALLATION.md); the standalone Windows executable needs no Python or Git.

The CH341 worked with the flash still soldered on the tested camera. A Bus
Pirate is possible but was not reliable in-circuit on that setup; see the
Expand Down Expand Up @@ -214,12 +214,11 @@ the only verified backup of this camera's identity-bearing data.

### 4. Build the recovery image

In Windows Explorer, open the folder containing `cc2_sig_tool.py` and the three
dump files. Click the address bar, type `cmd`, and press Enter. Then run this
single command:
Open a terminal in the folder containing your three dump files, as described
in the [installation guide](docs/INSTALLATION.md). Run:

```bat
py cc2_sig_tool.py build cc2-camera-1.bin --confirm cc2-camera-2.bin cc2-camera-3.bin
```sh
cc2flash build-image cc2-camera-1.bin --confirm-read cc2-camera-2.bin --confirm-read cc2-camera-3.bin
```

The command performs the stability, firmware, flash-layout, identity, patch,
Expand Down Expand Up @@ -275,7 +274,8 @@ No full camera dump or vendor firmware image is included in this repository.

## Advanced and audit documentation

- [Hardware-recovery CLI reference](hardware-recovery/CLI_REFERENCE.md)
- [Installation and release downloads](docs/INSTALLATION.md)
- [Complete cc2flash command reference](docs/CLI.md)
- [Programmer and alternative-hardware reference](hardware-recovery/PROGRAMMER_REFERENCE.md)
- [Independent recovery-tool verification](hardware-recovery/CC2_RECOVERY_VERIFICATION.md)
- [Hardware-recovery regression results](hardware-recovery/TEST_RESULTS.md)
Expand All @@ -284,4 +284,3 @@ No full camera dump or vendor firmware image is included in this repository.
- [Physical USB restore validation](usb-maintenance/PHYSICAL-VALIDATION.md)
- [Recovered USB protocol](usb-maintenance/PROTOCOL.md)
- [USB reverse-engineering evidence](usb-maintenance/EVIDENCE.md)

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

__all__ = ["__version__", "commands"]

__version__ = "0.6.0"
__version__ = "0.7.0"
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

raise SystemExit(main())


16 changes: 12 additions & 4 deletions usb-maintenance/cc2flash/adb_backup.py → cc2flash/adb_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class AdbUnavailable(ProtocolError):
("config", 0x020000),
)

# Keep these aligned with hardware-recovery/cc2_sig_tool.py. Three matching
# Keep these aligned with cc2flash/image.py. Three matching
# physical reads are the repository-wide minimum for a non-reference image.
# USB acquisition gets two extra chances because live testing showed JFFS2 may
# still change immediately after the one-shot ADB startup command.
Expand All @@ -66,7 +66,7 @@ class AdbUnavailable(ProtocolError):
)
MAX_MANIFEST_SIZE = 1024 * 1024

# hardware-recovery/cc2_sig_tool.py is intentionally allowed to change only
# cc2flash/image.py is intentionally allowed to change only
# its audited SquashFS window and the writable config partition. Restore uses
# these boundaries to prove that a candidate still belongs to the camera whose
# preserved USB backup authorizes the write.
Expand Down Expand Up @@ -146,7 +146,11 @@ def run(self, *args: str, timeout: float = 30) -> bytes:
try:
result = subprocess.run(command, capture_output=True, timeout=timeout, check=False)
except FileNotFoundError as exc:
raise ProtocolError(f"ADB executable not found: {self.executable}") from exc
raise ProtocolError(
f"ADB executable not found: {self.executable}. "
"Install Google SDK Platform Tools, then pass --adb with the path to adb. "
"Setup: https://github.com/phryneas/cc2_camera_fix/blob/main/docs/INSTALLATION.md"
) from exc
except subprocess.TimeoutExpired as exc:
raise ProtocolError(f"ADB command timed out: {' '.join(command)}") from exc
if result.returncode:
Expand Down Expand Up @@ -263,7 +267,11 @@ def ensure_available(self, *, timeout: float = 10.0) -> None:
command, capture_output=True, timeout=timeout, check=False
)
except FileNotFoundError as exc:
raise ProtocolError(f"ADB executable not found: {self.executable}") from exc
raise ProtocolError(
f"ADB executable not found: {self.executable}. "
"Install Google SDK Platform Tools, then pass --adb with the path to adb. "
"Setup: https://github.com/phryneas/cc2_camera_fix/blob/main/docs/INSTALLATION.md"
) from exc
except subprocess.TimeoutExpired as exc:
raise ProtocolError(f"ADB availability check timed out: {' '.join(command)}") from exc
state = result.stdout.decode("utf-8", "replace").strip()
Expand Down
46 changes: 46 additions & 0 deletions cc2flash/bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Publish a complete directory without replacing an existing destination."""

from contextlib import contextmanager
import ctypes
import errno
import os
from pathlib import Path
import sys
import tempfile


def rename_new(source: Path, destination: Path) -> None:
# POSIX rename alone can replace an existing empty directory. Use the
# platform's exclusive rename so a concurrent creator is also protected.
if sys.platform == "win32":
os.rename(source, destination)
return
libc = ctypes.CDLL(None, use_errno=True)
if sys.platform.startswith("linux") and hasattr(libc, "renameat2"):
rename = libc.renameat2
rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int,
ctypes.c_char_p, ctypes.c_uint]
args = (-100, os.fsencode(source), -100, os.fsencode(destination), 1)
elif sys.platform == "darwin" and hasattr(libc, "renamex_np"):
rename = libc.renamex_np
rename.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint]
args = (os.fsencode(source), os.fsencode(destination), 4)
else:
raise OSError(errno.ENOTSUP, "Atomic exclusive directory publication is unavailable")
rename.restype = ctypes.c_int
if rename(*args) != 0:
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), str(destination))


@contextmanager
def staged_directory(destination: Path):
"""Yield a private staging directory, then publish it only on success."""
if os.path.lexists(destination):
raise FileExistsError(errno.EEXIST, "Output already exists", str(destination))
destination.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".cc2flash-build-", dir=destination.parent) as temporary:
staging = Path(temporary) / "bundle"
staging.mkdir()
yield staging
rename_new(staging, destination)
Loading