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  }