github.com/Ptt-official-app/go-bbs@v0.12.0/pttbbs/fav.go (about) 1 // Copyright 2020 Pichu Chen, The PTT APP Authors 2 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 7 // http://www.apache.org/licenses/LICENSE-2.0 8 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // This file is for favorite function. 16 17 package pttbbs 18 19 import ( 20 "github.com/Ptt-official-app/go-bbs" 21 22 "bytes" 23 "encoding" 24 "encoding/binary" 25 "errors" 26 "fmt" 27 "io/ioutil" 28 "time" 29 ) 30 31 // For Current PTT 32 // Please see https://github.com/ptt/pttbbs/blob/master/include/fav.h 33 // https://github.com/ptt/pttbbs/blob/af507e0029c4e6b3a564ec98328ffe7cd7fd16be/mbbsd/fav.c 34 // 35 // This Fav parser parses favorites file. The favorites file contains the following 36 // 1. 2 bytes for FavFolder.Version 37 // 38 // Followed by at least 1 FavFolder, each FavFolder contains 39 // 2. 2 bytes for FavFolder.NBoards, how many boards in fav 40 // 3. 1 byte for FavFolder.NLines, how many line separator in fav 41 // 4. 1 byte for FavFolder.NFolders, how many folder in fav 42 // FavItemTypeBoard / FavItemTypeFolder / FavItemTypeLine is wrapped inside FavItem.Item and can be cast later. 43 // So the total items in this file will be (countOfItems = FavFolder.NBoards + FavFolder.NFolders + FavFolder.NLines) 44 // Followed by a list of FavItem, each FavItem pre-allocates: 45 // FavItem itself takes 2 bytes for FavItemType and FavAttr 46 // FavItemTypeBoard pre-allocates 12 bytes 47 // FavItemTypeLine pre-allocates 1 bytes 48 // FavItemTypeFolder pre-allocates 50 bytes 49 // Lastly followed by folders as another FavFolder 50 51 const ( 52 TIME4TBytes = 4 // Bytes for time4_t 53 favPreAlloc = 8 54 sizeOfPttFavBoardBytes = 12 // Each FavBoardItem takes this many bytes 55 sizeOfPttFavFolderBytes = 50 // Each FavFolderItem takes bytes 56 sizeOfPttFavLineBytes = 1 // Each FavLineItem takes bytes 57 ) 58 59 var ( 60 ErrInvalidFavType = errors.New("invalid Favorite type") 61 ErrIndexOutOfBound = errors.New("index out of range, file format invalid") 62 ) 63 64 type FavItemType uint8 65 66 const ( 67 FavItemTypeBoard FavItemType = 1 // FAVT_BOARD 68 FavItemTypeFolder FavItemType = 2 // FAVT_FOLDER 69 FavItemTypeLine FavItemType = 3 // FAVT_LINE 70 ) 71 72 // FavAttr represents fav attr 73 type FavAttr uint8 74 75 const ( 76 FavhFav FavAttr = 0x00000001 // FAVH_FAV 77 FavhTag FavAttr = 0x00000002 // FAVH_TAG 78 FavhUnread FavAttr = 0x00000004 // FAVH_UNREAD 79 FavhAdmTag FavAttr = 0x00000008 // FAVH_ADM_TAG 80 ) 81 82 // FavItem represents 1 Item in FavFolder 83 type FavItem struct { 84 FavType FavItemType 85 FavAttr uint8 86 Item interface{} // This could be either FavBoardItem / FavFolderItem / FavLineItem 87 } 88 89 func (favi *FavItem) BoardID() string { 90 if favi.FavType != FavItemTypeBoard { 91 return "" 92 } 93 return favi.Item.(*FavBoardItem).boardID 94 } 95 96 func (favi *FavItem) Title() string { 97 if favi.FavType == FavItemTypeLine { 98 return "------------------------------------------" 99 } 100 if favi.FavType == FavItemTypeFolder { 101 return favi.Item.(*FavFolderItem).Title 102 } 103 return "" 104 } 105 106 func (favi *FavItem) Type() bbs.FavoriteType { 107 switch favi.FavType { 108 case FavItemTypeBoard: 109 return bbs.FavoriteTypeBoard 110 case FavItemTypeFolder: 111 return bbs.FavoriteTypeFolder 112 case FavItemTypeLine: 113 return bbs.FavoriteTypeLine 114 } 115 return bbs.FavoriteTypeBoard 116 117 } 118 119 func (favi *FavItem) Records() []bbs.FavoriteRecord { 120 if favi.FavType != FavItemTypeFolder { 121 return nil 122 } 123 rec := favi.Item.(*FavFolderItem).ThisFolder.FavItems 124 ret := make([]bbs.FavoriteRecord, len(rec)) 125 for i, v := range rec { 126 ret[i] = v 127 } 128 return ret 129 } 130 131 // GetBoard tries to cast Item to FavBoardItem; return nil if it is not 132 func (favi *FavItem) GetBoard() *FavBoardItem { 133 if ret, ok := favi.Item.(*FavBoardItem); ok { 134 return ret 135 } 136 return nil 137 } 138 139 // GetFolder tries to cast Item to FavFolderItem; return nil if it is not 140 func (favi *FavItem) GetFolder() *FavFolderItem { 141 if ret, ok := favi.Item.(*FavFolderItem); ok { 142 return ret 143 } 144 return nil 145 } 146 147 // GetLine tries to cast Item to FavLineItem; return nil if it is not 148 func (favi *FavItem) GetLine() *FavLineItem { 149 if ret, ok := favi.Item.(*FavLineItem); ok { 150 return ret 151 } 152 return nil 153 } 154 155 // FavFile represents the entire fav file. Starts with 2 bytes of Version and at most 1 FavFolder. 156 type FavFile struct { 157 Version uint16 158 Folder *FavFolder 159 } 160 161 // FavFolder represents a folder in .fav file. Each folder could contain NBoards of board, NLines of lines 162 // and NFolders of sub-folders. 163 type FavFolder struct { 164 NAlloc uint16 165 DataTail uint16 166 NBoards uint16 167 NLines uint8 168 NFolders uint8 169 LineID uint8 170 FolderID uint8 171 FavItems []*FavItem 172 } 173 174 // FavBoardItem represents a Board in FavFolder. FavBoardItem takes 12 bytes 175 type FavBoardItem struct { 176 BoardID uint32 177 LastVisit time.Time 178 Attr uint32 179 boardID string 180 } 181 182 // FavFolderItem represents a Folder in FavFolder. FavFolderItem takes 50 bytes 183 type FavFolderItem struct { 184 FolderID uint8 185 Title string 186 ThisFolder *FavFolder 187 } 188 189 // FavLineItem represents a Line in FavFolder. FavLineItem takes 1 byte 190 type FavLineItem struct { 191 LineID uint8 192 } 193 194 // OpenFavFile reads a fav file 195 func OpenFavFile(filename string) (*FavFile, error) { 196 data, err := ioutil.ReadFile(filename) 197 if err != nil { 198 return nil, err 199 } 200 return UnmarshalFavFile(data) 201 } 202 203 // UnmarshalFavFile parse data and return FavFile 204 func UnmarshalFavFile(data []byte) (*FavFile, error) { 205 ret := &FavFile{} 206 size := 2 207 ret.Version = binary.LittleEndian.Uint16(data[0:size]) 208 209 var err error 210 ret.Folder, _, err = UnmarshalFavFolder(data, size) 211 if err != nil { 212 return nil, err 213 } 214 return ret, err 215 } 216 217 // getDataNumber returns the count of total items in FavFolder 218 func (favf *FavFolder) getDataNumber() uint16 { 219 return favf.NBoards + uint16(favf.NFolders) + uint16(favf.NLines) 220 } 221 222 // UnmarshalFavFolder takes a []byte, parse it starting with startIndex, return an instance of FavFolder, endIndex 223 // and error. 224 func UnmarshalFavFolder(data []byte, startIndex int) (*FavFolder, int, error) { 225 // data must at least has 4 bytes for a new FavFolder 226 if len(data) < startIndex+4 { 227 return nil, startIndex, ErrIndexOutOfBound 228 } 229 ret := &FavFolder{} 230 c := startIndex // current index 231 232 size := 2 233 ret.NBoards = binary.LittleEndian.Uint16(data[c : c+size]) 234 c += size 235 236 size = 1 237 ret.NLines = data[c] 238 c += size 239 240 size = 1 241 ret.NFolders = data[c] 242 c += size 243 244 ret.DataTail = ret.getDataNumber() 245 ret.NAlloc = ret.DataTail + favPreAlloc 246 ret.LineID = 0 247 ret.FolderID = 0 248 249 itemCount := ret.DataTail 250 ret.FavItems = make([]*FavItem, itemCount) 251 var err error 252 253 // There are itemCount items, parse and insert them one by one 254 for itemCount > 0 { 255 n := len(ret.FavItems) - int(itemCount) // calculate index 256 ret.FavItems[n], c, err = UnmarshalFavItem(data, c) 257 if err != nil { 258 return nil, 0, err 259 } 260 itemCount-- 261 } 262 263 // Parse and insert next folder, if any 264 for _, item := range ret.FavItems { 265 if f, ok := item.Item.(*FavFolderItem); ok { 266 var nextFolder *FavFolder 267 nextFolder, c, err = UnmarshalFavFolder(data, c) 268 if err != nil { 269 return nil, c, err 270 } 271 ret.FolderID++ 272 f.FolderID = ret.FolderID 273 f.ThisFolder = nextFolder 274 } 275 if f, ok := item.Item.(*FavLineItem); ok { 276 ret.LineID++ 277 f.LineID = ret.LineID 278 } 279 } 280 281 return ret, c, nil 282 } 283 284 // UnmarshalFavItem parse data starting from startIndex and return FavItem. FavItem.Item might be either FavBoardItem, 285 // FavFolderItem or FavLineItem 286 func UnmarshalFavItem(data []byte, startIndex int) (*FavItem, int, error) { 287 // data at least must have 2 bytes for a new FavItem 288 if len(data) < startIndex+2 { 289 return nil, startIndex, ErrIndexOutOfBound 290 } 291 ret := &FavItem{} 292 c := startIndex // current index 293 294 size := 1 295 ret.FavType = FavItemType(data[c]) 296 c += size 297 298 size = 1 299 ret.FavAttr = data[c] 300 c += size 301 302 var err error 303 var item interface{} 304 305 switch ret.FavType { 306 case FavItemTypeBoard: 307 item, c, err = UnmarshalFavBoardItem(data, c) 308 case FavItemTypeLine: 309 item, c, err = UnmarshalFavLineItem(data, c) 310 case FavItemTypeFolder: 311 item, c, err = UnmarshalFavFolderItem(data, c) 312 default: 313 err = ErrInvalidFavType 314 } 315 if err != nil { 316 return nil, c, err 317 } 318 ret.Item = item 319 320 return ret, c, err 321 } 322 323 // UnmarshalFavBoardItem takes a []byte and parse it starting from startIndex, return FavBoardItem, end index and error 324 func UnmarshalFavBoardItem(data []byte, startIndex int) (*FavBoardItem, int, error) { 325 if len(data) < startIndex+sizeOfPttFavBoardBytes { 326 return nil, startIndex, ErrIndexOutOfBound 327 } 328 ret := &FavBoardItem{} 329 c := startIndex 330 331 size := 4 332 ret.BoardID = binary.LittleEndian.Uint32(data[c : c+size]) 333 c += size 334 335 size = TIME4TBytes // use 4 bytes for time.Time 336 ret.LastVisit = time.Unix(int64(binary.LittleEndian.Uint32(data[c:c+size])), 0) 337 c += size 338 339 // This attr is a char in fav.h which should have been 1 byte. However, from the sample file 340 // we can see a Board takes 12 bytes, 4 bytes for BoardID, 4 bytes for LastVisit, so allocate the remaining 341 // 4 byte to attr. May need double check on this. 342 size = 4 343 ret.Attr = binary.LittleEndian.Uint32(data[c : c+size]) 344 c += size 345 346 return ret, c, nil 347 } 348 349 // UnmarshalFavFolderItem takes a []byte and parse it starting from startIndex, return FavFolderItem, end index and error 350 func UnmarshalFavFolderItem(data []byte, startIndex int) (*FavFolderItem, int, error) { 351 if len(data) < startIndex+sizeOfPttFavFolderBytes { 352 return nil, startIndex, ErrIndexOutOfBound 353 } 354 ret := &FavFolderItem{} 355 c := startIndex 356 357 size := 1 358 ret.FolderID = data[c] 359 c += size 360 361 size = BoardTitleLength + 1 362 ret.Title = big5uaoToUTF8String(bytes.Split(data[c:c+size], []byte("\x00"))[0]) 363 c += size 364 365 return ret, c, nil 366 } 367 368 // UnmarshalFavLineItem takes a []byte and parse it starting from startIndex, return FavLineItem, end index and error 369 func UnmarshalFavLineItem(data []byte, startIndex int) (*FavLineItem, int, error) { 370 if len(data) < startIndex+sizeOfPttFavLineBytes { 371 return nil, startIndex, ErrIndexOutOfBound 372 } 373 ret := &FavLineItem{} 374 c := startIndex 375 376 ret.LineID = data[c] 377 c++ 378 return ret, c, nil 379 } 380 381 func (favf *FavFile) MarshalBinary() ([]byte, error) { 382 ret := make([]byte, 2) 383 384 binary.LittleEndian.PutUint16(ret[0:2], favf.Version) 385 folderInBytes, err := favf.Folder.MarshalBinary() 386 if err != nil { 387 return nil, err 388 } 389 ret = append(ret, folderInBytes...) 390 391 return ret, nil 392 } 393 394 func (favf *FavFolder) MarshalBinary() ([]byte, error) { 395 ret := make([]byte, 4) 396 c := 0 397 398 size := 2 399 binary.LittleEndian.PutUint16(ret[c:c+size], favf.NBoards) 400 c += size 401 402 ret[c] = favf.NLines 403 c++ 404 405 ret[c] = favf.NFolders 406 407 for _, item := range favf.FavItems { 408 encoded, err := item.MarshalBinary() 409 if err != nil { 410 return nil, err 411 } 412 ret = append(ret, encoded...) 413 } 414 415 for _, item := range favf.FavItems { 416 if f, ok := item.Item.(*FavFolderItem); ok { 417 encoded, err := f.ThisFolder.MarshalBinary() 418 if err != nil { 419 return nil, err 420 } 421 ret = append(ret, encoded...) 422 } 423 } 424 425 return ret, nil 426 } 427 428 func (favi *FavItem) MarshalBinary() ([]byte, error) { 429 ret := make([]byte, 2) 430 431 ret[0] = uint8(favi.FavType) 432 ret[1] = favi.FavAttr 433 favim, ok := favi.Item.(encoding.BinaryMarshaler) 434 if !ok { 435 return nil, fmt.Errorf("FavItem.Item must implement encoding.BinaryMarshaler") 436 } 437 encoded, err := favim.MarshalBinary() 438 if err != nil { 439 return nil, err 440 } 441 ret = append(ret, encoded...) 442 443 return ret, nil 444 } 445 446 func (favbi *FavBoardItem) MarshalBinary() ([]byte, error) { 447 ret := make([]byte, sizeOfPttFavBoardBytes) 448 c := 0 449 450 size := 4 451 binary.LittleEndian.PutUint32(ret[c:c+size], favbi.BoardID) 452 c += size 453 454 binary.LittleEndian.PutUint32(ret[c:c+size], uint32(favbi.LastVisit.Unix())) 455 c += size 456 457 binary.LittleEndian.PutUint32(ret[c:c+size], favbi.Attr) 458 459 return ret, nil 460 } 461 462 func (favfi *FavFolderItem) MarshalBinary() ([]byte, error) { 463 ret := make([]byte, sizeOfPttFavFolderBytes) 464 ret[0] = favfi.FolderID 465 466 size := BoardTitleLength + 1 467 copy(ret[1:1+size], utf8ToBig5UAOString(favfi.Title)) 468 469 return ret, nil 470 } 471 472 func (favli *FavLineItem) MarshalBinary() ([]byte, error) { 473 ret := make([]byte, sizeOfPttFavLineBytes) 474 ret[0] = favli.LineID 475 return ret, nil 476 }