package pack import ( "bytes" "encoding/json" "errors" "fmt" "io" "strings" "unicode" "unicode/utf8" ) // Format is the pack format this package reads. const Format = 1 // Ratings. const ( General = "general" Adult = "adult" ) // Manifest limits. const ( MaxIDLength = 64 MaxNameLength = 64 // characters MaxCreatorLength = 64 // characters MaxDescriptionLength = 20000 ) // Manifest is t5de-product.json. type Manifest struct { Format int `json:"format"` ID string `json:"id"` Revision int `json:"revision"` Name string `json:"name"` Creator string `json:"creator,omitempty"` Description string `json:"description,omitempty"` // HTML, cleaned by package safehtml when shown Rating string `json:"rating"` } // ParseManifest parses and checks t5de-product.json. Name and Creator come back trimmed. func ParseManifest(data []byte) (*Manifest, error) { m, err := parseManifest(data) if err != nil { return nil, fmt.Errorf("%s: %w", ManifestName, err) } return m, nil } func parseManifest(data []byte) (*Manifest, error) { d := json.NewDecoder(bytes.NewReader(data)) d.DisallowUnknownFields() var m Manifest if err := d.Decode(&m); err != nil { return nil, err } if _, err := d.Token(); err != io.EOF { return nil, errors.New("data after the object") } m.Name = strings.TrimSpace(m.Name) m.Creator = strings.TrimSpace(m.Creator) switch { case m.Format != Format: return nil, fmt.Errorf("format: is %d, must be %d", m.Format, Format) case !validID(m.ID): return nil, fmt.Errorf("id: %q must be 1-%d of a-z, 0-9 and -, not starting or ending with -", m.ID, MaxIDLength) case m.Revision < 1: return nil, fmt.Errorf("revision: is %d, must be at least 1", m.Revision) case m.Name == "": return nil, errors.New("name: missing") case utf8.RuneCountInString(m.Name) > MaxNameLength: return nil, fmt.Errorf("name: over %d characters", MaxNameLength) case hasControl(m.Name): return nil, errors.New("name: has control characters") case utf8.RuneCountInString(m.Creator) > MaxCreatorLength: return nil, fmt.Errorf("creator: over %d characters", MaxCreatorLength) case hasControl(m.Creator): return nil, errors.New("creator: has control characters") case len(m.Description) > MaxDescriptionLength: return nil, fmt.Errorf("description: over %d bytes", MaxDescriptionLength) case m.Rating != General && m.Rating != Adult: return nil, fmt.Errorf("rating: %q must be %q or %q", m.Rating, General, Adult) } return &m, nil } func validID(id string) bool { if id == "" || len(id) > MaxIDLength || id[0] == '-' || id[len(id)-1] == '-' { return false } for _, c := range []byte(id) { if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') { return false } } return true } func hasControl(s string) bool { return strings.ContainsFunc(s, unicode.IsControl) }