github.com/la5nta/wl2k-go@v0.11.8/fbb/proposal.go (about)

     1  // Copyright 2015 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
     2  // Use of this source code is governed by the MIT-license that can be
     3  // found in the LICENSE file.
     4  
     5  package fbb
     6  
     7  import (
     8  	"bytes"
     9  	"compress/gzip"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/la5nta/wl2k-go/lzhuf"
    17  )
    18  
    19  type PropCode byte
    20  
    21  const (
    22  	BasicProposal PropCode = 'B' // Basic ASCII proposal (or compressed binary in v0/1)
    23  	AsciiProposal          = 'A' // Compressed v0/1 ASCII proposal
    24  	Wl2kProposal           = 'C' // Compressed v2 proposal (winlink extension)
    25  	GzipProposal           = 'D' // Gzip compressed v2 proposal
    26  )
    27  
    28  type ProposalAnswer byte
    29  
    30  const (
    31  	Accept ProposalAnswer = '+'
    32  	Reject                = '-'
    33  	Defer                 = '='
    34  
    35  	// Offset not supported yet
    36  )
    37  
    38  // Proposal is the type representing a inbound or outbound proposal.
    39  type Proposal struct {
    40  	code           PropCode
    41  	msgType        string
    42  	mid            string
    43  	answer         ProposalAnswer
    44  	title          string
    45  	offset         int
    46  	sent           bool
    47  	size           int
    48  	compressedData []byte
    49  	compressedSize int
    50  }
    51  
    52  // Constructor for a new Proposal given a Winlink Message.
    53  //
    54  // Reads the Winlink Message given and constructs a new proposal
    55  // based on what's read and prepares for outbound delivery, returning
    56  // a Proposal with the given data.
    57  //
    58  func NewProposal(MID, title string, code PropCode, data []byte) *Proposal {
    59  	prop := &Proposal{
    60  		mid:     MID,
    61  		code:    code,
    62  		msgType: "EM",
    63  		title:   title,
    64  		size:    len(data),
    65  	}
    66  
    67  	if prop.title == `` {
    68  		prop.title = `No title`
    69  	}
    70  
    71  	var (
    72  		z   io.WriteCloser
    73  		buf bytes.Buffer
    74  	)
    75  	switch prop.code {
    76  	case GzipProposal:
    77  		z, _ = gzip.NewWriterLevel(&buf, gzip.BestCompression)
    78  	default:
    79  		z = lzhuf.NewB2Writer(&buf)
    80  	}
    81  
    82  	z.Write(data)
    83  	if err := z.Close(); err != nil {
    84  		panic(err)
    85  	}
    86  
    87  	prop.compressedData = buf.Bytes()
    88  	prop.compressedSize = len(prop.compressedData)
    89  
    90  	return prop
    91  }
    92  
    93  // Method for checking if the Proposal is completely
    94  // downloaded/loaded and ready to be read/sent.
    95  //
    96  // Typically used to check if the whole message was
    97  // successfully downloaded from the CMS.
    98  //
    99  func (p *Proposal) DataIsComplete() bool {
   100  	return len(p.compressedData) == p.compressedSize
   101  }
   102  
   103  // Returns the uniqe Message ID
   104  func (p *Proposal) MID() string {
   105  	return p.mid
   106  }
   107  
   108  // Returns the title of this proposal
   109  func (p *Proposal) Title() string {
   110  	return p.title
   111  }
   112  
   113  func (p *Proposal) Message() (*Message, error) {
   114  	buf := bytes.NewBuffer(p.Data())
   115  	m := new(Message)
   116  	err := m.ReadFrom(buf)
   117  	return m, err
   118  }
   119  
   120  // Data returns the decompressed raw message
   121  func (p *Proposal) Data() []byte {
   122  	var r io.ReadCloser
   123  	var err error
   124  
   125  	switch p.code {
   126  	case GzipProposal:
   127  		r, err = gzip.NewReader(bytes.NewBuffer(p.compressedData))
   128  	default:
   129  		r, err = lzhuf.NewB2Reader(bytes.NewBuffer(p.compressedData))
   130  	}
   131  
   132  	if err != nil {
   133  		panic(err) //TODO: Should return error
   134  	}
   135  
   136  	var buf bytes.Buffer
   137  	if _, err := io.Copy(&buf, r); err != nil {
   138  		panic(err) //TODO
   139  	}
   140  
   141  	return buf.Bytes()
   142  }
   143  
   144  func parseProposal(line string, prop *Proposal) (err error) {
   145  	if len(line) < 1 {
   146  		return
   147  	} else if line[0] != 'F' {
   148  		return
   149  	}
   150  
   151  	prop.code = PropCode(line[1])
   152  
   153  	switch prop.code {
   154  	case BasicProposal, AsciiProposal: // TODO: implement
   155  	case Wl2kProposal, GzipProposal:
   156  		err = parseB2Proposal(line, prop)
   157  	default:
   158  		err = fmt.Errorf("Unsupported proposal code '%c'", prop.code)
   159  	}
   160  	return
   161  }
   162  
   163  func parseB2Proposal(line string, prop *Proposal) (err error) {
   164  	if len(line) < 4 {
   165  		return errors.New("Unexpected end of proposal line")
   166  	}
   167  
   168  	if !(line[1] == Wl2kProposal || line[1] == GzipProposal) {
   169  		return errors.New("Not a type C or D proposal")
   170  	}
   171  
   172  	// FC EM TJKYEIMMHSRB 527 123 0
   173  	parts := strings.Split(line[3:], " ")
   174  	if len(parts) < 5 {
   175  		return errors.New(`Malformed proposal: ` + line[2:])
   176  	}
   177  
   178  	for i, part := range parts {
   179  		switch i {
   180  		case 0:
   181  			if len(part) < 1 || len(part) > 2 {
   182  				return errors.New(`Malformed proposal 0`)
   183  			} else if part != "EM" && part != "CM" {
   184  				return fmt.Errorf(`Expected message type CM or EM, but found %s`, part)
   185  			}
   186  			prop.msgType = part
   187  		case 1:
   188  			prop.mid = part
   189  		case 2:
   190  			prop.size, _ = strconv.Atoi(part)
   191  		case 3:
   192  			prop.compressedSize, _ = strconv.Atoi(part)
   193  		case 4:
   194  		default:
   195  			return errors.New(fmt.Sprintf(`Too many parts in proposal: %+v`, parts))
   196  		}
   197  	}
   198  	return
   199  }
   200  
   201  // precedence returns the priority level of the message. Lower precedence value is more important
   202  // and should be handled sooner.
   203  //
   204  // See https://www.winlink.org/content/how_use_message_precedence_precedence.
   205  func (p *Proposal) precedence() int {
   206  	const (
   207  		Flash = iota
   208  		Immediate
   209  		Priority
   210  		Routine
   211  	)
   212  	switch {
   213  	case strings.Contains(p.title, "//WL2K Z/"):
   214  		return Flash
   215  	case strings.Contains(p.title, "//WL2K O/"):
   216  		return Immediate
   217  	case strings.Contains(p.title, "//WL2K P/"):
   218  		return Priority
   219  	default:
   220  		return Routine
   221  	}
   222  }