package car //go:generate go tool stringer -type=renditionAttributeType,renditionLayoutType,resourceID,compressionType -output=car_string.go . import ( "bytes" "encoding/binary" "encoding/json" "fmt" "image" "strings" "time" "github.com/apex/log" "github.com/blacktop/go-termimg" "github.com/blacktop/ipsw/internal/utils" "github.com/dustin/go-humanize" "github.com/fatih/color" ) var ( colorTitle = color.New(color.Bold, color.FgHiMagenta).SprintFunc() colorField = color.New(color.Bold, color.FgHiBlue).SprintFunc() colorSubField = color.New(color.Bold, color.FgHiCyan).SprintFunc() ) func (a *Asset) String() string { var out strings.Builder out.WriteString(colorTitle("Asset\n") + "=====\n") // title out.WriteString(colorField("Header") + ":\n") out.WriteString(fmt.Sprintf( colorSubField(" Version")+": %s"+ colorSubField(" CoreUI Version")+": %d\n"+ colorSubField(" Storage Version")+": %d\n"+ colorSubField(" Storage Timestamp")+": %s\n"+ colorSubField(" Rendition Count")+": %d\n"+ colorSubField(" UUID")+": %s\n"+ colorSubField(" Associated Checksum")+": %#x\n"+ colorSubField(" Schema Version")+": %d\n"+ colorSubField(" ColorSpaceID")+": %s\n"+ colorSubField(" Key Semantics")+": %d\n", string(bytes.Trim(a.MainVersionString[:], "\x00")), a.CoreUiVersion, a.StorageVersion, time.Unix(int64(a.StorageTimestamp), 0).String(), a.RenditionCount, a.UUID.String(), a.AssociatedChecksum, a.SchemaVersion, a.ColorSpaceID, a.KeySemantics, )) out.WriteString(colorField("Metadata") + ":\n") out.WriteString(fmt.Sprintf( " Authoring Tool: %s"+ " Thinning Args: %s\n"+ " Deployment Platform: %s %s\n", string(bytes.Trim(a.Metadata.AuthoringTool[:], "\x00")), strings.ReplaceAll(string(bytes.Trim(a.Metadata.ThinningArguments[:], "\x00")), "<", "\n <"), string(bytes.Trim(a.Metadata.DeploymentPlatform[:], "\x00")), string(bytes.Trim(a.Metadata.DeploymentPlatformVersion[:], "\x00")), )) if len(a.KeyFormat) > 0 { out.WriteString(colorField("KeyFormats") + ":\n") for _, k := range a.KeyFormat { out.WriteString(fmt.Sprintf(" - %s\n", k)) } } if len(a.AppearanceDB) > 0 { out.WriteString(colorField("Appearances") + ":\n") for k, v := range a.AppearanceDB { out.WriteString(fmt.Sprintf(" %s: %d\n", colorSubField(k), v)) } } if len(a.ColorDB) > 0 { out.WriteString(colorField("Colors") + ":\n") for k, v := range a.ColorDB { if a.conf.Verbose { if tout, err := colorInTerminal(v); err == nil { out.WriteString(fmt.Sprintf("- %s:\n\n%s\n\n", k, tout)) } } else { out.WriteString(fmt.Sprintf(" %s: %#v\n", colorSubField(k), v)) } } } if len(a.Localizations) > 0 { out.WriteString(colorField("Localizations") + ":\n") for k, v := range a.Localizations { out.WriteString(fmt.Sprintf(" %s: %d\n", colorSubField(k), v)) } } if len(a.ImageDB) > 0 { out.WriteString(fmt.Sprintf(colorTitle("Assets")+": (%d)\n", len(a.ImageDB))) for _, ass := range a.ImageDB { out.WriteString(" ╭╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴\n") var asset strings.Builder switch t := ass.Asset.(type) { case csiColor: asset.WriteString(fmt.Sprintf(colorField("Colorspace")+": %s\n", t.Info.ColorSpaceID())) if len(t.Components) > 0 { asset.WriteString(" " + colorSubField("Components") + ":\n") for _, c := range t.Components { asset.WriteString(fmt.Sprintf(" - %v\n", c)) } if a.conf.Verbose { if tout, err := t.ToTerminal(); err == nil { asset.WriteString(fmt.Sprintf(colorField("Color Preview")+":\n%s\n", tout)) } } } case *BGRA: asset.WriteString(fmt.Sprintf(colorField("Image Size")+": %dx%d\n", t.Rect.Max.X, t.Rect.Max.Y)) if a.conf.Verbose { if tout, err := termimg.New(t).Render(); err == nil { asset.WriteString(fmt.Sprintf("Image Preview:\n%s\n", tout)) } } case *GA8: asset.WriteString(fmt.Sprintf(colorField("Image Size")+": %dx%d\n", t.Rect.Max.X, t.Rect.Max.Y)) if a.conf.Verbose { if tout, err := termimg.New(t).Render(); err == nil { asset.WriteString(fmt.Sprintf("Image Preview:\n%s\n", tout)) } } case *image.RGBA: asset.WriteString(fmt.Sprintf(colorField("Image Size")+": %dx%d\n", t.Rect.Max.X, t.Rect.Max.Y)) if a.conf.Verbose { if tout, err := termimg.New(t).Render(); err == nil { asset.WriteString(fmt.Sprintf("Image Preview:\n%s\n", tout)) } } case csiNamedGradient: asset.WriteString(colorField("Gradient") + ":\n") for i := range t.ColorCount { asset.WriteString(fmt.Sprintf(" - %s (%.3f, %.3f)\n", bytes.Trim(t.Stops[i].Name, "\x00"), float32(t.StartStops[i].Start), float32(t.StartStops[i].Stop))) } case csiMultisizeImageSet: asset.WriteString(colorField("MultiSized") + ":\n") for _, size := range t.ImageSizes { asset.WriteString(fmt.Sprintf(" - index %d: %dx%d\n", size.Index, size.Width, size.Height)) } case []byte: // Raw data (PDF, JPEG, HEIF, etc.) asset.WriteString(fmt.Sprintf(colorField("Data Size")+": %s (%d bytes)\n", humanize.Bytes(uint64(len(t))), len(t))) default: log.Debugf("%s has unknown asset type: %T", ass.RenditionName, t) } var attrs strings.Builder if len(ass.Attributes) > 0 { attrs.WriteString(colorField("Attributes") + ":\n") for _, kf := range a.KeyFormat { if value, ok := ass.Attributes[kf.String()]; ok { attrs.WriteString(fmt.Sprintf(" %s%d\n", colorSubField(fmt.Sprintf("%-20s", kf.String())), value)) } } } var rscs strings.Builder if len(ass.Resources) > 0 { rscs.WriteString(colorField("Resources") + ":\n") for _, rsc := range ass.Resources { switch rsc.ID { case SliceID: var slice sliceResource if err := slice.UnmarshalBinary(rsc.Data); err != nil { rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s: (%d)\n", colorSubField(rsc.ID), slice.NumSlices)) for _, s := range slice.Slices { rscs.WriteString(fmt.Sprintf(" - pos(%03d,%03d) size(%03d,%03d)\n", s.X, s.Y, s.Width, s.Height)) } case MetricsID: var metrics metricsResource if err := metrics.UnmarshalBinary(rsc.Data); err != nil { rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s: (%d)\n", colorSubField(rsc.ID), metrics.NumMetrics)) for _, m := range metrics.Metrics { rscs.WriteString(fmt.Sprintf(" - %s(%d,%d,%d,%d) %s(%03d,%03d)\n", colorField("insets"), m.LeftInset, m.TopInset, m.RightInset, m.BottomInset, colorField("size"), m.Width, m.Height)) } case LayerReferenceID: layer := new(layerResource) if err := layer.UnmarshalBinary(rsc.Data); err != nil { rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s: (%d):\n", colorSubField("Layers"), layer.NumLayers)) for _, layer := range layer.Layers { rscs.WriteString(fmt.Sprintf(" %s(%03d,%03d) %s(%03d,%03d) %s=%d %s=%.2f\n", colorField("pos"), layer.Frame.X, layer.Frame.Y, colorField("size"), layer.Frame.Width, layer.Frame.Height, colorField("blend"), layer.BlendMode, colorField("opacity"), layer.Opacity)) rscs.WriteString(fmt.Sprintf(" %s", utils.HexDump(layer.Data, 0))) } case InternalLinkID: var link csiInternalLinkData if err := link.UnmarshalBinary(bytes.NewReader(rsc.Data)); err != nil { rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s: %s(%d,%d) %s(%d)\n", colorSubField(rsc.ID), colorSubField("frame"), link.Frame.X, link.Frame.Y, colorSubField("layout"), link.Layout)) for _, ref := range link.Reference { rscs.WriteString(fmt.Sprintf(" %s: %d\n", colorSubField(renditionAttributeType(ref.Name)), ref.Value)) } case CompositingOptionsID: var comp compositingResource if err := binary.Read(bytes.NewReader(rsc.Data), binary.LittleEndian, &comp); err != nil { rscs.WriteString(fmt.Sprintf(" %s:\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s:\n %s: %d\n %s: %.2f\n", colorSubField(rsc.ID), colorField("BlendMode"), comp.BlendMode, colorField("Opacity"), comp.Opacity)) case MetaDataID: var meta metadataResource if err := meta.UnmarshalBinary(rsc.Data); err != nil { rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s: %s\n", colorSubField(rsc.ID), bytes.Trim(meta.Data[:], "\x00"))) case MetaDataEXIFOrientationID: var orient uint32 if err := binary.Read(bytes.NewReader(rsc.Data), binary.LittleEndian, &orient); err != nil { rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s: %d\n", colorSubField(rsc.ID), orient)) case ImageRowBytesID: var rowBytes uint32 if err := binary.Read(bytes.NewReader(rsc.Data), binary.LittleEndian, &rowBytes); err != nil { rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } rscs.WriteString(fmt.Sprintf(" %s: %s (%d)\n", colorSubField(rsc.ID), humanize.Bytes(uint64(rowBytes)), rowBytes)) default: rscs.WriteString(fmt.Sprintf(" %s\n%s", colorSubField(rsc.ID), utils.HexDump(rsc.Data, 0))) } } } var nameStr string if name := a.GetName(ass.ID()); len(name) > 0 { nameStr = colorField("Name") + fmt.Sprintf(": %s\n", name) if name != ass.RenditionName { nameStr += colorField("Rendition") + fmt.Sprintf(": %s\n", ass.RenditionName) } } else if len(ass.RenditionName) > 0 { nameStr = colorField("Rendition") + fmt.Sprintf(": %s\n", ass.RenditionName) } out.WriteString(fmt.Sprintf( "%s"+ colorField("Type")+": %s\n"+ colorField("Size")+": %s (%d)\n"+ "%s%s%s", // "%s%s%s", nameStr, ass.Type, humanize.Bytes(uint64(ass.Size)), ass.Size, asset.String(), attrs.String(), rscs.String(), )) out.WriteString(" ╰╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴\n") } } return out.String() } // KeyFormatName converts renditionAttributeType to its Apple name func (r renditionAttributeType) KeyFormatName() string { switch r { case ThemeLook: return "kCRThemeLookName" case Element: return "kCRThemeElementName" case Part: return "kCRThemePartName" case Size: return "kCRThemeSizeName" case Direction: return "kCRThemeDirectionName" case Placeholder: return "kCRThemePlaceholderName" case Value: return "kCRThemeValueName" case ThemeAppearance: return "kCRThemeAppearanceName" case Dimension1: return "kCRThemeDimension1Name" case Dimension2: return "kCRThemeDimension2Name" case State: return "kCRThemeStateName" case Layer: return "kCRThemeLayerName" case Scale: return "kCRThemeScaleName" case Localization: return "kCRThemeLocalizationName" case PresentationState: return "kCRThemePresentationStateName" case Idiom: return "kCRThemeIdiomName" case Subtype: return "kCRThemeSubtypeName" case Identifier: return "kCRThemeIdentifierName" case PreviousValue: return "kCRThemePreviousValueName" case PreviousState: return "kCRThemePreviousStateName" case HorizontalSizeClass: return "kCRThemeHorizontalSizeClassName" case VerticalSizeClass: return "kCRThemeVerticalSizeClassName" case MemoryLevelClass: return "kCRThemeMemoryLevelClassName" case GraphicsFeatureSetClass: return "kCRThemeGraphicsFeatureSetClassName" case DisplayGamut: return "kCRThemeDisplayGamutName" case DeploymentTarget: return "kCRThemeDeploymentTargetName" default: return fmt.Sprintf("renditionAttributeType(%d)", r) } } // ToJSON converts Asset to JSON format matching assetutil output func (a *Asset) ToJSON() ([]byte, error) { output := []map[string]any{} // Add header information as first element header := map[string]any{ "CoreUIVersion": a.Header.CoreUiVersion, "StorageVersion": a.Header.StorageVersion, "Timestamp": a.Header.StorageTimestamp, "SchemaVersion": a.Header.SchemaVersion, "MainVersion": strings.TrimSpace(string(bytes.Trim(a.Header.MainVersionString[:], "\x00"))), "Authoring Tool": strings.TrimSpace(string(bytes.Trim(a.Metadata.AuthoringTool[:], "\x00"))), "ThinningParameters": string(bytes.Trim(a.Metadata.ThinningArguments[:], "\x00")), "Platform": string(bytes.Trim(a.Metadata.DeploymentPlatform[:], "\x00")), "PlatformVersion": string(bytes.Trim(a.Metadata.DeploymentPlatformVersion[:], "\x00")), } // Add key format if len(a.KeyFormat) > 0 { keyFormats := []string{} for _, k := range a.KeyFormat { keyFormats = append(keyFormats, k.KeyFormatName()) } header["Key Format"] = keyFormats } // Add appearances if len(a.AppearanceDB) > 0 { appearances := map[string]int{} for k, v := range a.AppearanceDB { appearances[k] = int(v) } header["Appearances"] = appearances } output = append(output, header) // Add renditions for _, rend := range a.ImageDB { rendition := map[string]any{ "Name": rend.RenditionName, "Type": rend.Type, } // Add attributes for _, kf := range a.KeyFormat { if value, ok := rend.Attributes[kf.String()]; ok { switch kf { case Scale: rendition["Scale"] = value case Idiom: rendition["Idiom"] = getIdiomName(value) case Subtype: if value > 0 { rendition["Subtype"] = value } case Identifier: if value > 0 { rendition["NameIdentifier"] = value } } } } if rend.Size > 0 { rendition["SizeOnDisk"] = rend.Size } output = append(output, rendition) } return json.MarshalIndent(output, "", " ") } func getIdiomName(value uint16) string { switch coreThemeIdiom(value) { case Universal: return "universal" case Phone: return "phone" case Tablet: return "pad" case Desktop: return "desktop" case Tv: return "tv" case Car: return "car" case Watch: return "watch" case Marketing: return "marketing" default: return fmt.Sprintf("idiom_%d", value) } }