mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-21 07:13:43 +08:00
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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user