feat: import Apple carrier bundles safely (#50)

* feat: import Apple carrier bundles safely

* Delete docs/CARRIER_IPCC_IMPORT.md

---------

Co-authored-by: Meng Meng <[email protected]>
This commit is contained in:
Rain Seven
2026-08-17 19:27:17 +08:00
committed by GitHub
co-authored by Meng Meng
parent 95299a0a39
commit fdb2ac45ce
15 changed files with 1637 additions and 53 deletions
+95
View File
@@ -0,0 +1,95 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"path/filepath"
"strings"
"vocat/internal/config"
"vocat/internal/vowifi"
)
func runCarrier(args []string, stdout io.Writer) error {
if len(args) == 0 {
return errors.New("usage: vocat carrier import-ipcc [flags] FILE.ipcc")
}
switch args[0] {
case "import-ipcc":
return runCarrierImportIPCC(args[1:], stdout)
default:
return fmt.Errorf("unknown carrier subcommand %q", args[0])
}
}
func runCarrierImportIPCC(args []string, stdout io.Writer) error {
flags := flag.NewFlagSet("carrier import-ipcc", flag.ContinueOnError)
flags.SetOutput(io.Discard)
var bundle string
var profileID string
var profileDir string
var install bool
var documentOnly bool
flags.StringVar(&bundle, "bundle", "", "bundle name when an IPCC contains more than one carrier bundle")
flags.StringVar(&profileID, "id", "", "override the generated carrier profile ID")
flags.StringVar(&profileDir, "profile-dir", "", "installation directory (default: next to the VoCat database)")
flags.BoolVar(&install, "install", false, "atomically install the reviewed generated profile")
flags.BoolVar(&documentOnly, "document-only", false, "print only the generated carrier profile document")
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() != 1 {
return errors.New("usage: vocat carrier import-ipcc [--bundle NAME] [--id ID] [--document-only] [--install] [--profile-dir DIR] FILE.ipcc")
}
if documentOnly && install {
return errors.New("--document-only and --install cannot be used together")
}
if strings.TrimSpace(profileDir) != "" && !install {
return errors.New("--profile-dir requires --install")
}
result, err := vowifi.ImportCarrierIPCC(flags.Arg(0), vowifi.IPCCImportOptions{
Bundle: bundle,
ProfileID: profileID,
})
if err != nil {
return err
}
if documentOnly {
_, err := stdout.Write(result.Document)
return err
}
installedPath := ""
if install {
profileDir = strings.TrimSpace(profileDir)
if profileDir == "" {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("load configuration for carrier profile directory: %w", err)
}
profileDir = filepath.Join(filepath.Dir(cfg.DatabasePath), "carrier-profiles.d")
}
installedPath, err = vowifi.InstallCarrierIPCCResult(result, profileDir)
if err != nil {
return err
}
if absolute, absoluteErr := filepath.Abs(installedPath); absoluteErr == nil {
installedPath = absolute
}
}
output := struct {
vowifi.IPCCImportResult
InstalledPath string `json:"installed_path,omitempty"`
RestartRequired bool `json:"restart_required,omitempty"`
}{
IPCCImportResult: result,
InstalledPath: installedPath,
RestartRequired: installedPath != "",
}
encoder := json.NewEncoder(stdout)
encoder.SetIndent("", " ")
return encoder.Encode(output)
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"archive/zip"
"bytes"
"encoding/json"
"os"
"path/filepath"
"testing"
"howett.net/plist"
)
func TestRunCarrierImportIPCCPreviewsAndInstallsExplicitly(t *testing.T) {
archivePath := filepath.Join(t.TempDir(), "test.ipcc")
file, err := os.Create(archivePath)
if err != nil {
t.Fatal(err)
}
archive := zip.NewWriter(file)
entry, err := archive.Create("Payload/Test.bundle/carrier.plist")
if err != nil {
t.Fatal(err)
}
var encoded bytes.Buffer
if err := plist.NewEncoder(&encoded).Encode(map[string]any{
"CarrierName": "Test Carrier",
"SupportedSIMs": []any{"99901"},
"SupportedPLMNs": []any{"99901"},
}); err != nil {
t.Fatal(err)
}
if _, err := entry.Write(encoded.Bytes()); err != nil {
t.Fatal(err)
}
if err := archive.Close(); err != nil {
t.Fatal(err)
}
if err := file.Close(); err != nil {
t.Fatal(err)
}
var preview bytes.Buffer
if err := runCarrier([]string{"import-ipcc", "--document-only", archivePath}, &preview); err != nil {
t.Fatal(err)
}
var document struct {
Version int `json:"version"`
}
if err := json.Unmarshal(preview.Bytes(), &document); err != nil || document.Version != 1 {
t.Fatalf("preview = %q, version=%d, error=%v", preview.String(), document.Version, err)
}
installDir := t.TempDir()
var output bytes.Buffer
if err := runCarrier([]string{
"import-ipcc", "--id", "cli-test", "--install", "--profile-dir", installDir, archivePath,
}, &output); err != nil {
t.Fatal(err)
}
var installed struct {
InstalledPath string `json:"installed_path"`
RestartRequired bool `json:"restart_required"`
}
if err := json.Unmarshal(output.Bytes(), &installed); err != nil {
t.Fatal(err)
}
if !installed.RestartRequired || filepath.Base(installed.InstalledPath) != "cli-test.json" {
t.Fatalf("install output = %s", output.String())
}
if _, err := os.Stat(filepath.Join(installDir, "cli-test.json")); err != nil {
t.Fatal(err)
}
}
+6
View File
@@ -24,6 +24,12 @@ Usage:
vocat doctor Diagnose USB modem, AT, QMI, PC/SC and proxy UDP paths.
Use --repair-dji-qmi on Linux to safely wake a factory-ID
DJI/Baiwang 2ca3:4006 QMI interface without changing NV.
vocat carrier import-ipcc [flags] FILE.ipcc
Convert an Apple carrier bundle into a reviewable VoCat
profile. Preview is the default; --install writes it to
carrier-profiles.d and takes effect after restart.
Flags: --bundle NAME --id ID --document-only --install
--profile-dir DIR.
vocat update Check GitHub for a newer release and self-update.
Flags:
--check Only report whether an update is available.
+9
View File
@@ -82,6 +82,11 @@ func main() {
logger.Error("doctor failed", "error", err)
os.Exit(1)
}
case "carrier":
if err := runCarrier(rest, os.Stdout); err != nil {
logger.Error("carrier command failed", "error", err)
os.Exit(1)
}
case "menu":
if err := runMenu(logger); err != nil {
logger.Error("menu failed", "error", err)
@@ -125,6 +130,10 @@ func run(logger *slog.Logger, logs *loghub.Hub) error {
if err != nil {
return fmt.Errorf("load configuration: %w", err)
}
carrierProfileDir := filepath.Join(filepath.Dir(cfg.DatabasePath), "carrier-profiles.d")
if err := vowifi.LoadCarrierProfileDirectory(carrierProfileDir); err != nil {
return fmt.Errorf("load installed carrier profiles: %w", err)
}
instanceLock, err := lockServerInstance(cfg.DatabasePath)
if err != nil {
return err