media: never project zero image/video dimensions (crashes Telegram Desktop on reactions)
This commit is contained in:
parent
d4fe056854
commit
57b7829a9e
3 changed files with 122 additions and 4 deletions
|
|
@ -304,6 +304,10 @@ func (s *Service) prepareStickerSetDocument(ctx context.Context, doc domain.Docu
|
||||||
|
|
||||||
func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Document) (domain.Document, error) {
|
func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Document) (domain.Document, error) {
|
||||||
mimeType := canonicalStickerMaterialMime(doc.StickerSetMaterialMime())
|
mimeType := canonicalStickerMaterialMime(doc.StickerSetMaterialMime())
|
||||||
|
// A stored documentAttributeImageSize with a zero dimension is worse than a
|
||||||
|
// missing one: clients divide by it and crash. Strip any such attribute here
|
||||||
|
// so the branches below re-derive a real 512x512 (or decoded) size.
|
||||||
|
doc.Attributes = dropZeroImageSizeAttributes(doc.Attributes)
|
||||||
hasImageSize := false
|
hasImageSize := false
|
||||||
hasVideo := false
|
hasVideo := false
|
||||||
for _, attr := range doc.Attributes {
|
for _, attr := range doc.Attributes {
|
||||||
|
|
@ -311,7 +315,9 @@ func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Doc
|
||||||
case domain.DocAttrImageSize:
|
case domain.DocAttrImageSize:
|
||||||
hasImageSize = true
|
hasImageSize = true
|
||||||
case domain.DocAttrVideo:
|
case domain.DocAttrVideo:
|
||||||
hasVideo = true
|
if attr.W > 0 && attr.H > 0 {
|
||||||
|
hasVideo = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch mimeType {
|
switch mimeType {
|
||||||
|
|
@ -444,6 +450,20 @@ func (s *Service) rewriteStickerMaterialBlob(ctx context.Context, docID int64, d
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dropZeroImageSizeAttributes removes documentAttributeImageSize entries whose
|
||||||
|
// width or height is not positive. Such an attribute reaches clients as
|
||||||
|
// documentAttributeImageSize#0 and is divided by while sizing the render.
|
||||||
|
func dropZeroImageSizeAttributes(attrs []domain.DocumentAttribute) []domain.DocumentAttribute {
|
||||||
|
out := attrs[:0:0]
|
||||||
|
for _, a := range attrs {
|
||||||
|
if a.Kind == domain.DocAttrImageSize && (a.W <= 0 || a.H <= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func replaceStickerMaterialFilename(attrs []domain.DocumentAttribute, fallback string) []domain.DocumentAttribute {
|
func replaceStickerMaterialFilename(attrs []domain.DocumentAttribute, fallback string) []domain.DocumentAttribute {
|
||||||
out := append([]domain.DocumentAttribute(nil), attrs...)
|
out := append([]domain.DocumentAttribute(nil), attrs...)
|
||||||
for i := range out {
|
for i := range out {
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,11 @@ func tgPhotoSizes(sizes []domain.PhotoSize) []tg.PhotoSizeClass {
|
||||||
func tgPhotoSize(s domain.PhotoSize) tg.PhotoSizeClass {
|
func tgPhotoSize(s domain.PhotoSize) tg.PhotoSizeClass {
|
||||||
switch s.Kind {
|
switch s.Kind {
|
||||||
case domain.PhotoSizeKindDefault:
|
case domain.PhotoSizeKindDefault:
|
||||||
|
if s.W <= 0 || s.H <= 0 {
|
||||||
|
// A zero-dimension photoSize is malformed and divides to a crash on
|
||||||
|
// the client; dropping it lets clients fall back to another size.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return &tg.PhotoSize{Type: s.Type, W: s.W, H: s.H, Size: s.Size}
|
return &tg.PhotoSize{Type: s.Type, W: s.W, H: s.H, Size: s.Size}
|
||||||
case domain.PhotoSizeKindStripped:
|
case domain.PhotoSizeKindStripped:
|
||||||
return &tg.PhotoStrippedSize{Type: s.Type, Bytes: s.Bytes}
|
return &tg.PhotoStrippedSize{Type: s.Type, Bytes: s.Bytes}
|
||||||
|
|
@ -450,12 +455,47 @@ func compactPhotoSizeClasses(in []tg.PhotoSizeClass) []tg.PhotoSizeClass {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stickerCanvasSize is the square canvas a sticker / custom emoji is laid out on.
|
||||||
|
// It is the fallback dimension when stored metadata carries a zero, which clients
|
||||||
|
// divide by while sizing the render (a 0 there crashes Telegram Desktop).
|
||||||
|
const stickerCanvasSize = 512
|
||||||
|
|
||||||
|
func stickerLikeMime(mimeType string) bool {
|
||||||
|
switch mimeType {
|
||||||
|
case mimeApplicationXTGSticker, "image/webp", "video/webm":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeMediaDimension replaces a non-positive width/height with a usable value:
|
||||||
|
// the sticker canvas for sticker-like documents, otherwise 1, so no projected
|
||||||
|
// attribute ever carries a zero a client would divide by.
|
||||||
|
func safeMediaDimension(v int, mimeType string) int {
|
||||||
|
if v > 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if stickerLikeMime(mimeType) {
|
||||||
|
return stickerCanvasSize
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
func tgDocumentAttributes(mimeType string, attrs []domain.DocumentAttribute) []tg.DocumentAttributeClass {
|
func tgDocumentAttributes(mimeType string, attrs []domain.DocumentAttribute) []tg.DocumentAttributeClass {
|
||||||
out := make([]tg.DocumentAttributeClass, 0, len(attrs))
|
out := make([]tg.DocumentAttributeClass, 0, len(attrs))
|
||||||
for _, a := range attrs {
|
for _, a := range attrs {
|
||||||
switch a.Kind {
|
switch a.Kind {
|
||||||
case domain.DocAttrImageSize:
|
case domain.DocAttrImageSize:
|
||||||
out = append(out, &tg.DocumentAttributeImageSize{W: a.W, H: a.H})
|
if (a.W <= 0 || a.H <= 0) && !stickerLikeMime(mimeType) {
|
||||||
|
// Malformed size on a plain image: drop it rather than emit a zero.
|
||||||
|
// Clients cope with a missing imageSize; a zero one crashes them.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, &tg.DocumentAttributeImageSize{
|
||||||
|
W: safeMediaDimension(a.W, mimeType),
|
||||||
|
H: safeMediaDimension(a.H, mimeType),
|
||||||
|
})
|
||||||
case domain.DocAttrAnimated:
|
case domain.DocAttrAnimated:
|
||||||
if mimeType == mimeApplicationXTGSticker {
|
if mimeType == mimeApplicationXTGSticker {
|
||||||
continue
|
continue
|
||||||
|
|
@ -473,8 +513,8 @@ func tgDocumentAttributes(mimeType string, attrs []domain.DocumentAttribute) []t
|
||||||
SupportsStreaming: a.SupportsStreaming,
|
SupportsStreaming: a.SupportsStreaming,
|
||||||
Nosound: a.NoSound,
|
Nosound: a.NoSound,
|
||||||
Duration: a.Duration,
|
Duration: a.Duration,
|
||||||
W: a.W,
|
W: safeMediaDimension(a.W, mimeType),
|
||||||
H: a.H,
|
H: safeMediaDimension(a.H, mimeType),
|
||||||
}
|
}
|
||||||
if a.VideoCodec != "" {
|
if a.VideoCodec != "" {
|
||||||
video.SetVideoCodec(a.VideoCodec)
|
video.SetVideoCodec(a.VideoCodec)
|
||||||
|
|
|
||||||
|
|
@ -721,6 +721,64 @@ func TestTGDocumentDoesNotEmitAnimatedAttributeForTGSSticker(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTGDocumentNeverEmitsZeroImageOrVideoDimensions(t *testing.T) {
|
||||||
|
// A sticker / custom emoji whose stored metadata carries a zero dimension
|
||||||
|
// must not reach the client as documentAttributeImageSize#0 -- Telegram
|
||||||
|
// Desktop divides by it and crashes when the emoji is used as a reaction.
|
||||||
|
sticker := tgDocument(domain.Document{
|
||||||
|
ID: 100, AccessHash: 1, DCID: 2, MimeType: "application/x-tgsticker",
|
||||||
|
Attributes: []domain.DocumentAttribute{
|
||||||
|
{Kind: domain.DocAttrImageSize, W: 0, H: 0},
|
||||||
|
{Kind: domain.DocAttrCustomEmoji, Alt: "🙂", StickerSetID: 10, StickerSetAccessHash: 20},
|
||||||
|
},
|
||||||
|
}).(*tg.Document)
|
||||||
|
var gotImage bool
|
||||||
|
for _, attr := range sticker.Attributes {
|
||||||
|
if a, ok := attr.(*tg.DocumentAttributeImageSize); ok {
|
||||||
|
gotImage = true
|
||||||
|
if a.W != 512 || a.H != 512 {
|
||||||
|
t.Fatalf("sticker imageSize = %dx%d, want 512x512 fallback", a.W, a.H)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !gotImage {
|
||||||
|
t.Fatal("sticker lost its imageSize attribute entirely")
|
||||||
|
}
|
||||||
|
|
||||||
|
video := tgDocument(domain.Document{
|
||||||
|
ID: 101, AccessHash: 1, DCID: 2, MimeType: "video/webm",
|
||||||
|
Attributes: []domain.DocumentAttribute{
|
||||||
|
{Kind: domain.DocAttrVideo, W: 0, H: 0, Duration: 3},
|
||||||
|
{Kind: domain.DocAttrSticker, Alt: "😀", StickerSetID: 10, StickerSetAccessHash: 20},
|
||||||
|
},
|
||||||
|
}).(*tg.Document)
|
||||||
|
for _, attr := range video.Attributes {
|
||||||
|
if a, ok := attr.(*tg.DocumentAttributeVideo); ok && (a.W <= 0 || a.H <= 0) {
|
||||||
|
t.Fatalf("video sticker attribute has zero dimension: %+v", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A malformed plain image drops the attribute rather than emitting a zero.
|
||||||
|
plain := tgDocument(domain.Document{
|
||||||
|
ID: 102, AccessHash: 1, DCID: 2, MimeType: "image/jpeg",
|
||||||
|
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrImageSize, W: 0, H: 480}},
|
||||||
|
}).(*tg.Document)
|
||||||
|
for _, attr := range plain.Attributes {
|
||||||
|
if _, ok := attr.(*tg.DocumentAttributeImageSize); ok {
|
||||||
|
t.Fatalf("plain image kept a malformed imageSize: %+v", attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A zero-dimension thumbnail is dropped, not emitted.
|
||||||
|
thumbed := tgDocument(domain.Document{
|
||||||
|
ID: 103, AccessHash: 1, DCID: 2, MimeType: "image/webp",
|
||||||
|
Thumbs: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "s", W: 0, H: 0, Size: 10}},
|
||||||
|
}).(*tg.Document)
|
||||||
|
if len(thumbed.Thumbs) != 0 {
|
||||||
|
t.Fatalf("kept a zero-dimension thumb: %#v", thumbed.Thumbs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTGDocumentUsesDomainDocumentID(t *testing.T) {
|
func TestTGDocumentUsesDomainDocumentID(t *testing.T) {
|
||||||
const documentID int64 = 1382305375846410902
|
const documentID int64 = 1382305375846410902
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue