package connector import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "galaxy/model/client" "galaxy/model/report" ) // Connector is a main interface to provide connectivity with app's server. type Connector interface { UIConnector // CheckConnection is called asynchronously every 5 seconds and tests is connection available with a specific backend server endpoint. // There is guaranteed jittered backoff with caps 5s -> 15s -> 30s -> 60s when no connection is available. CheckConnection() bool // CheckVersion is called asynchronously every 30 minutes and receives from backend server information about currently available app versions. CheckVersion() ([]VersionInfo, error) // DownloadVersion asynchronously retrieves from a specific string URL a binary artifact from backend server. DownloadVersion(string) ([]byte, error) } // UIConnector contains only funcs are needed for the client app to be functional. type UIConnector interface { // FetchReport asynchronously requests from backend server a [report.Report] for a given [model.GameID] and turn number. // Passed callback func will will accept non-nil error in case of I/O or decoding errors occuried, // otherwise callback func accepts loaded [report.Report]. FetchReport(client.GameID, uint, func(report.Report, error)) } type VersionInfo struct { OS string `json:"os"` // Operating System name (unix, darwin, windows, etc.) Version string `json:"version"` // Semver format: X.Y.Z URL string `json:"url"` // Artifact download URL for this version Checksum SHA256Digest `json:"sha256"` // Base64 SHA-256 checksum for artifact binary data } // SHA256Digest represents a SHA-256 digest in raw binary form. // // Internally it stores the exact 32-byte digest. // In JSON it is encoded as a lowercase hexadecimal string of 64 characters. type SHA256Digest [32]byte // NewSHA256Digest calculates SHA-256 for the provided byte slice // and returns the digest as SHA256Digest. // // The function does not modify the input data. func NewSHA256Digest(data []byte) SHA256Digest { sum := sha256.Sum256(data) return SHA256Digest(sum) } // String returns the lowercase hexadecimal representation // of the digest. // // The returned string always contains exactly 64 characters. func (d SHA256Digest) String() string { return hex.EncodeToString(d[:]) } // MarshalJSON encodes the digest as a JSON string containing // the lowercase hexadecimal SHA-256 value. // // Example JSON value: // // "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" func (d SHA256Digest) MarshalJSON() ([]byte, error) { return json.Marshal(d.String()) } // UnmarshalJSON decodes a JSON string containing a lowercase or uppercase // hexadecimal SHA-256 value into the digest. // // The input must be a JSON string with exactly 64 hexadecimal characters. func (d *SHA256Digest) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return fmt.Errorf("sha256 digest must be a JSON string: %w", err) } if len(s) != hex.EncodedLen(len(d)) { return fmt.Errorf("invalid SHA-256 hex length: got %d, want %d", len(s), hex.EncodedLen(len(d))) } decoded, err := hex.DecodeString(s) if err != nil { return fmt.Errorf("invalid SHA-256 hex value: %w", err) } copy(d[:], decoded) return nil } // Equal returns true when both digests are identical. // // Since SHA256Digest is based on a fixed-size array, direct value comparison // is efficient and idiomatic for non-constant-time equality checks. func (d SHA256Digest) Equal(other SHA256Digest) bool { return d == other }