mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
318 lines
8.7 KiB
Go
318 lines
8.7 KiB
Go
package resources
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/blacktop/go-macho"
|
|
"github.com/blacktop/go-plist"
|
|
pl "github.com/blacktop/ipsw/pkg/plist"
|
|
)
|
|
|
|
const libraryDirectory = "Contents/Library"
|
|
const resourcesDirectory = "Contents/Resources"
|
|
const frameworksDirectory = "Contents/Frameworks"
|
|
const CodeResourcesPath = "Contents/_CodeSignature/CodeResources"
|
|
|
|
type hash2 struct {
|
|
CDHash []byte `plist:"cdhash,omitempty" xml:"cdhash,omitempty"`
|
|
Requirement string `plist:"requirement,omitempty" xml:"requirement,omitempty"`
|
|
Hash2 []byte `plist:"hash2,omitempty" xml:"hash2,omitempty"`
|
|
Symlink string `plist:"symlink,omitempty" xml:"symlink,omitempty"`
|
|
Optional bool `plist:"optional,omitempty" xml:"optional,omitempty"`
|
|
}
|
|
|
|
type CodeResources struct {
|
|
Files map[string][]byte `plist:"files,omitempty" xml:"files,omitempty"`
|
|
Files2 map[string]hash2 `plist:"files2,omitempty" xml:"files2,omitempty"`
|
|
Rules map[string]any `plist:"rules,omitempty" xml:"rules,omitempty"`
|
|
Rules2 map[string]any `plist:"rules2,omitempty" xml:"rules2,omitempty"`
|
|
}
|
|
|
|
func CreateCodeResources(dir string) error {
|
|
cr := &CodeResources{
|
|
Files: make(map[string][]byte),
|
|
Files2: make(map[string]hash2),
|
|
Rules: make(map[string]any),
|
|
Rules2: make(map[string]any),
|
|
}
|
|
// rules
|
|
cr.Rules["^Resources/"] = true
|
|
cr.Rules["^Resources/.*\\.lproj/"] = struct {
|
|
Optional bool `plist:"optional,omitempty" xml:"optional,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Optional: true,
|
|
Weight: 1000,
|
|
}
|
|
cr.Rules["^Resources/.*\\.lproj/locversion.plist$"] = struct {
|
|
Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Omit: true,
|
|
Weight: 1100,
|
|
}
|
|
cr.Rules["^Resources/Base\\.lproj/"] = struct {
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Weight: 1010,
|
|
}
|
|
cr.Rules["^version.plist$"] = true
|
|
// rules2
|
|
cr.Rules2[".*\\.dSYM($|/)"] = struct {
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Weight: 11,
|
|
}
|
|
cr.Rules2["^(.*/)?\\.DS_Store$"] = struct {
|
|
Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Omit: true,
|
|
Weight: 2000,
|
|
}
|
|
cr.Rules2["^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/"] = struct {
|
|
Nested bool `plist:"nested,omitempty" xml:"nested,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Nested: true,
|
|
Weight: 10,
|
|
}
|
|
cr.Rules2["^.*"] = true
|
|
cr.Rules2["^Info\\.plist$"] = struct {
|
|
Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Omit: true,
|
|
Weight: 20,
|
|
}
|
|
cr.Rules2["^PkgInfo$"] = struct {
|
|
Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Omit: true,
|
|
Weight: 20,
|
|
}
|
|
cr.Rules2["^Resources/"] = struct {
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Weight: 20,
|
|
}
|
|
cr.Rules2["^Resources/.*\\.lproj/"] = struct {
|
|
Optional bool `plist:"optional,omitempty" xml:"optional,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Optional: true,
|
|
Weight: 1000,
|
|
}
|
|
cr.Rules2["^Resources/.*\\.lproj/locversion.plist$"] = struct {
|
|
Omit bool `plist:"omit,omitempty" xml:"omit,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Omit: true,
|
|
Weight: 1100,
|
|
}
|
|
cr.Rules2["^Resources/Base\\.lproj/"] = struct {
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Weight: 1010,
|
|
}
|
|
cr.Rules2["^[^/]+$"] = struct {
|
|
Nested bool `plist:"nested,omitempty" xml:"nested,omitempty"`
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Nested: true,
|
|
Weight: 10,
|
|
}
|
|
cr.Rules2["^embedded\\.provisionprofile$"] = struct {
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Weight: 20,
|
|
}
|
|
cr.Rules2["^version\\.plist$"] = struct {
|
|
Weight float64 `plist:"weight,omitempty" xml:"weight,omitempty"`
|
|
}{
|
|
Weight: 20,
|
|
}
|
|
|
|
if err := filepath.WalkDir(filepath.Join(dir, frameworksDirectory), func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
relPath, err := filepath.Rel(filepath.Join(dir, "Contents"), path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var fwpath string
|
|
if regexp.MustCompile(`^Frameworks/[^/]+\.framework/.+Info\.plist$`).MatchString(relPath) {
|
|
dat, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %s: %w", path, err)
|
|
}
|
|
ainfo, err := pl.ParseAppInfo(dat)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse %s: %w", path, err)
|
|
}
|
|
if ainfo.CFBundleExecutable != "" {
|
|
before, _, ok := strings.Cut(path, ".framework/")
|
|
if ok {
|
|
fwpath = filepath.Join(before+".framework/", ainfo.CFBundleExecutable)
|
|
}
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
if _, ok := cr.Files2[fwpath]; ok {
|
|
return nil // already added
|
|
}
|
|
|
|
// files
|
|
var m *macho.File
|
|
if fat, err := macho.OpenFat(fwpath); err == nil { // UNIVERSAL MACHO
|
|
defer fat.Close()
|
|
m = fat.Arches[len(fat.Arches)-1].File
|
|
} else { // SINGLE MACHO ARCH
|
|
if errors.Is(err, macho.ErrNotFat) {
|
|
m, err = macho.Open(fwpath)
|
|
if err != nil {
|
|
return nil
|
|
// return err
|
|
}
|
|
defer m.Close()
|
|
} else {
|
|
return nil // not a macho file
|
|
}
|
|
}
|
|
cs := m.CodeSignature()
|
|
if cs == nil {
|
|
return fmt.Errorf("no code signature in %s", path)
|
|
}
|
|
if len(cs.CodeDirectories) == 0 {
|
|
return fmt.Errorf("no code directory in %s", path)
|
|
}
|
|
cdhashBytes, err := hex.DecodeString(cs.CodeDirectories[len(cs.CodeDirectories)-1].CDHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var requirement string
|
|
if len(cs.Requirements) > 0 {
|
|
requirement = cs.Requirements[0].Detail
|
|
}
|
|
relPath, err = filepath.Rel(filepath.Join(dir, "Contents"), fwpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cr.Files2[filepath.Dir(relPath)] = hash2{
|
|
CDHash: cdhashBytes[:20],
|
|
Requirement: requirement,
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to walk %s: %w", filepath.Join(dir, "Resources"), err)
|
|
}
|
|
// TODO: walk pluginsDirectory ??
|
|
// TODO: walk xpcServicesDirectory ??
|
|
// TODO: walk helpersDirectory ??
|
|
if err := filepath.WalkDir(filepath.Join(dir, libraryDirectory), func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relPath, err := filepath.Rel(filepath.Join(dir, "Contents"), path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fi, err := os.Lstat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("file %s does not exist", path)
|
|
}
|
|
var symlink string
|
|
if (fi.Mode() & os.ModeSymlink) != 0 {
|
|
symlink, err = os.Readlink(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to eval symlink %s: %w", path, err)
|
|
}
|
|
}
|
|
if symlink != "" {
|
|
cr.Files2[relPath] = hash2{Symlink: symlink}
|
|
} else {
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
h := sha256.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return err
|
|
}
|
|
cr.Files2[relPath] = hash2{Hash2: h.Sum(nil)}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to walk %s: %w", filepath.Join(dir, "Resources"), err)
|
|
}
|
|
if err := filepath.WalkDir(filepath.Join(dir, resourcesDirectory), func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
relPath, err := filepath.Rel(filepath.Join(dir, "Contents"), path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// files
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h := sha1.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return err
|
|
}
|
|
cr.Files[relPath] = h.Sum(nil)
|
|
f.Seek(0, io.SeekStart) // rewind
|
|
// files2
|
|
h = sha256.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return err
|
|
}
|
|
cr.Files2[relPath] = hash2{Hash2: h.Sum(nil)}
|
|
if err := f.Close(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to walk %s: %w", filepath.Join(dir, "Resources"), err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, CodeResourcesPath)), 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(filepath.Join(dir, CodeResourcesPath)), err)
|
|
}
|
|
f, err := os.Create(filepath.Join(dir, CodeResourcesPath))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create %s: %w", CodeResourcesPath, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
enc := plist.NewEncoder(f)
|
|
enc.Indent("\t")
|
|
return enc.Encode(cr)
|
|
}
|