SAUCE (Standard Architecture for Universal Comment Extensions) parser in Go
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sauce.go 5.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // Package sauce contains a SAUCE (Standard Architecture for Universal Comment Extensions) parser.
  2. package sauce
  3. import (
  4. "bytes"
  5. "encoding/binary"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "io/ioutil"
  10. "strconv"
  11. "strings"
  12. "time"
  13. )
  14. const (
  15. // ASCIISub is the SUB ASCII character (or EOF)
  16. ASCIISub = '\x1a'
  17. sauceDate = "19700101"
  18. )
  19. const (
  20. // LetterSpacingLegacy enables legacy letter spacing
  21. LetterSpacingLegacy = iota
  22. // LetterSpacing8Pixel enables 8 pixel letter spacing
  23. LetterSpacing8Pixel
  24. // LetterSpacing9Pixel enables 9 pixel letter spacing
  25. LetterSpacing9Pixel
  26. // LetterSpacingInvalid is unspecified
  27. LetterSpacingInvalid
  28. )
  29. const (
  30. // AspectRatioLegacy enables legacy aspect ratio
  31. AspectRatioLegacy = iota
  32. // AspectRatioStretch enables stretching on displays with square pixels
  33. AspectRatioStretch
  34. // AspectRatioSquare enables optimization for non-square displays
  35. AspectRatioSquare
  36. // AspectRatioInvalid is unspecified
  37. AspectRatioInvalid
  38. )
  39. var (
  40. // ID is the SAUCE header identifier
  41. ID = [5]byte{'S', 'A', 'U', 'C', 'E'}
  42. // Version is the SAUCE version
  43. Version = [2]byte{0, 0}
  44. // No SAUCE record found
  45. ErrNoRecord = errors.New(`sauce: no SAUCE record found`)
  46. )
  47. // SAUCE (Standard Architecture for Universal Comment Extensions) record.
  48. type SAUCE struct {
  49. ID [5]byte
  50. Version [2]byte
  51. Title string
  52. Author string
  53. Group string
  54. Date time.Time
  55. FileSize uint32
  56. DataType uint8
  57. FileType uint8
  58. TInfo [4]uint16
  59. Comments uint8
  60. TFlags TFlags
  61. TInfos []byte
  62. }
  63. // TFlags contains a parsed TFlags structure
  64. type TFlags struct {
  65. NonBlink bool
  66. LetterSpacing uint8
  67. AspectRatio uint8
  68. }
  69. // New creates an empty SAUCE record.
  70. func New() *SAUCE {
  71. return &SAUCE{
  72. ID: ID,
  73. Version: Version,
  74. TInfo: [4]uint16{},
  75. TInfos: []byte{},
  76. TFlags: TFlags{},
  77. }
  78. }
  79. // Parse SAUCE record
  80. func Parse(s *io.SectionReader) (r *SAUCE, err error) {
  81. var n int64
  82. var i int
  83. n, err = s.Seek(-128, 2)
  84. if err != nil {
  85. return
  86. }
  87. if n < 128 {
  88. return nil, io.ErrShortBuffer
  89. }
  90. b := make([]byte, 128)
  91. i, err = s.Read(b)
  92. if err != nil {
  93. return
  94. }
  95. if i != 128 {
  96. return nil, io.ErrShortBuffer
  97. }
  98. return ParseBytes(b)
  99. }
  100. // ParseReader reads the SAUCE header from a stream
  101. func ParseReader(i io.Reader) (r *SAUCE, err error) {
  102. var b []byte
  103. if b, err = ioutil.ReadAll(i); err != nil {
  104. return nil, err
  105. }
  106. if len(b) < 128 {
  107. return nil, io.ErrShortBuffer
  108. }
  109. return ParseBytes(b)
  110. }
  111. // ParseBytes reads the SAUCE header from a slice of bytes
  112. func ParseBytes(b []byte) (r *SAUCE, err error) {
  113. if len(b) < 128 {
  114. return nil, io.ErrShortBuffer
  115. }
  116. o := len(b) - 128
  117. if !bytes.Equal(b[o+0:o+5], ID[:]) {
  118. return nil, ErrNoRecord
  119. }
  120. r = New()
  121. r.Title = strings.TrimSpace(string(b[o+7 : o+41]))
  122. r.Author = strings.TrimSpace(string(b[o+41 : o+61]))
  123. r.Group = strings.TrimSpace(string(b[o+61 : o+81]))
  124. r.Date = r.parseDate(string(b[o+82 : o+90]))
  125. r.FileSize = binary.LittleEndian.Uint32(b[o+91 : o+95])
  126. r.DataType = uint8(b[o+94])
  127. r.FileType = uint8(b[o+95])
  128. r.TInfo[0] = binary.LittleEndian.Uint16(b[o+96 : o+98])
  129. r.TInfo[1] = binary.LittleEndian.Uint16(b[o+98 : o+100])
  130. r.TInfo[2] = binary.LittleEndian.Uint16(b[o+100 : o+102])
  131. r.TInfo[3] = binary.LittleEndian.Uint16(b[o+102 : o+104])
  132. r.Comments = uint8(b[o+104])
  133. r.TInfos = b[106+o:]
  134. tflags := uint8(b[o+105])
  135. r.TFlags.NonBlink = (tflags & 1) == 1
  136. r.TFlags.LetterSpacing = (tflags >> 1) & 3
  137. r.TFlags.AspectRatio = (tflags >> 3) & 3
  138. return r, nil
  139. }
  140. func (r *SAUCE) parseDate(s string) time.Time {
  141. y, _ := strconv.Atoi(s[:4])
  142. m, _ := strconv.Atoi(s[4:6])
  143. d, _ := strconv.Atoi(s[6:8])
  144. return time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC)
  145. }
  146. // Dump the contents of the SAUCE record to stdout.
  147. func (r *SAUCE) Dump() {
  148. fmt.Printf("id......: %s\n", string(r.ID[:]))
  149. fmt.Printf("version.: %d%d\n", r.Version[0], r.Version[1])
  150. fmt.Printf("title...: %s\n", r.Title)
  151. fmt.Printf("author..: %s\n", r.Author)
  152. fmt.Printf("group...: %s\n", r.Group)
  153. fmt.Printf("date....: %s\n", r.Date)
  154. fmt.Printf("filesize: %d\n", r.FileSize)
  155. fmt.Printf("datatype: %d (%s)\n", r.DataType, r.DataTypeString())
  156. if FileType[r.DataType] != nil {
  157. fmt.Printf("filetype: %d (%s)\n", r.FileType, r.FileTypeString())
  158. } else {
  159. fmt.Printf("filetype: %d\n", r.FileType)
  160. }
  161. fmt.Printf("tinfo...: %d, %d, %d, %d\n", r.TInfo[0], r.TInfo[1], r.TInfo[2], r.TInfo[3])
  162. switch r.DataType {
  163. case 1:
  164. switch r.FileType {
  165. case 0, 1, 2, 4, 5, 8:
  166. w := r.TInfo[0]
  167. h := r.TInfo[1]
  168. if w == 0 {
  169. w = 80
  170. }
  171. fmt.Printf("size....: %d x %d characters\n", w, h)
  172. case 3:
  173. fmt.Printf("size....: %d x %d pixels\n", r.TInfo[0], r.TInfo[1])
  174. }
  175. case 2:
  176. fmt.Printf("size....: %d x %d pixels\n", r.TInfo[0], r.TInfo[1])
  177. }
  178. }
  179. // DataTypeString returns the DataType as string.
  180. func (r *SAUCE) DataTypeString() string {
  181. return DataType[r.DataType]
  182. }
  183. // FileTypeString returns the FileType as string.
  184. func (r *SAUCE) FileTypeString() string {
  185. switch FileType[r.DataType] {
  186. case nil:
  187. switch r.DataType {
  188. case DataTypeBinaryText:
  189. return "BinaryText"
  190. case DataTypeXBIN:
  191. return "XBin"
  192. case DataTypeExecutable:
  193. return "Executable"
  194. }
  195. default:
  196. return FileType[r.DataType][r.FileType]
  197. }
  198. return ""
  199. }
  200. // MimeType returns the mime type as string.
  201. func (r *SAUCE) MimeType() (t string) {
  202. switch MimeType[r.DataType] {
  203. case nil:
  204. switch r.DataType {
  205. case DataTypeBinaryText:
  206. t = "text/x-binary"
  207. case DataTypeXBIN:
  208. t = "text/x-xbin"
  209. }
  210. default:
  211. t = MimeType[r.DataType][r.FileType]
  212. }
  213. if t == "" {
  214. t = "application/octet-stream"
  215. }
  216. return
  217. }
  218. // Font returns the font name
  219. func (r *SAUCE) Font() string {
  220. return strings.Trim(string(r.TInfos[:]), "\x00 ")
  221. }