package gopher import ( "bytes" "fmt" "io" "mime" "path" "strings" "sync" "tildegit.org/tjp/sliderule/internal/types" ) // The Canonical gopher item types. const ( TextFileType types.Status = '0' MenuType types.Status = '1' CSOPhoneBookType types.Status = '2' ErrorType types.Status = '3' MacBinHexType types.Status = '4' DosBinType types.Status = '5' UuencodedType types.Status = '6' SearchType types.Status = '7' TelnetSessionType types.Status = '8' BinaryFileType types.Status = '9' MirrorServerType types.Status = '+' GifFileType types.Status = 'g' ImageFileType types.Status = 'I' Telnet3270Type types.Status = 'T' ) // The gopher+ types. const ( BitmapType types.Status = ':' MovieFileType types.Status = ';' SoundFileType types.Status = '<' ) // The various non-canonical gopher types. const ( DocumentType types.Status = 'd' HTMLType types.Status = 'h' InfoMessageType types.Status = 'i' PngImageFileType types.Status = 'p' RtfDocumentType types.Status = 'r' WavSoundFileType types.Status = 's' PdfDocumentType types.Status = 'P' XmlDocumentType types.Status = 'X' ) // MapItem is a single item in a gophermap. type MapItem struct { Type types.Status Display string Selector string Hostname string Port string } // String serializes the item into a gophermap CRLF-terminated text line. func (mi MapItem) String() string { return fmt.Sprintf( "%s%s\t%s\t%s\t%s\r\n", []byte{byte(mi.Type)}, mi.Display, mi.Selector, mi.Hostname, mi.Port, ) } // Response builds a response which contains just this single MapItem. // // Meta in the response will be a pointer to the MapItem. func (mi *MapItem) Response() *types.Response { return &types.Response{ Status: mi.Type, Meta: &mi, Body: bytes.NewBufferString(mi.String() + ".\r\n"), } } // MapDocument is a list of map items which can print out a full gophermap document. type MapDocument []MapItem // String serializes the document into gophermap format. func (md MapDocument) String() string { return md.serialize().String() } // Response builds a gopher response containing the gophermap. // // Meta will be the MapDocument itself. func (md MapDocument) Response() *types.Response { return &types.Response{ Status: MenuType, Meta: md, Body: md.serialize(), } } func (md MapDocument) serialize() *bytes.Buffer { buf := &bytes.Buffer{} for _, mi := range md { _, _ = buf.WriteString(mi.String()) } _, _ = buf.WriteString(".\r\n") return buf } // Error builds an error message MapItem. func Error(err error) *MapItem { return &MapItem{ Type: ErrorType, Display: err.Error(), Hostname: "none", Port: "0", } } // File builds a minimal response delivering a file's contents. // // Meta is nil in this response. func File(status types.Status, contents io.Reader) *types.Response { return &types.Response{Status: status, Body: contents} } // NewResponseReader produces a reader which supports reading gopher protocol responses. func NewResponseReader(response *types.Response) types.ResponseReader { return &responseReader{ Response: response, once: &sync.Once{}, } } type responseReader struct { *types.Response reader io.Reader once *sync.Once } func (rdr *responseReader) Read(b []byte) (int, error) { rdr.ensureReader() return rdr.reader.Read(b) } func (rdr *responseReader) WriteTo(dst io.Writer) (int64, error) { rdr.ensureReader() return rdr.reader.(io.WriterTo).WriteTo(dst) } func (rdr *responseReader) ensureReader() { rdr.once.Do(func() { if _, ok := rdr.Body.(io.WriterTo); ok { rdr.reader = rdr.Body return } // rdr.reader needs to implement WriterTo, so in this case // we borrow an implementation in terms of io.Reader from // io.MultiReader. rdr.reader = io.MultiReader(rdr.Body) }) } // GuessItemType attempts to find the best gopher item type for a file based on its name. func GuessItemType(filepath string) types.Status { ext := path.Ext(filepath) switch ext { case ".gophermap": return MenuType case ".txt", ".gmi", ".md": return TextFileType case ".gif": return GifFileType case ".png": return PngImageFileType case ".jpg", ".jpeg", ".tif", ".tiff": return ImageFileType case ".mp4", ".mov": return MovieFileType case ".pcm", ".mp3", ".aiff", ".aif", ".aac", ".ogg", ".flac", ".alac", ".wma": return SoundFileType case ".bmp": return BitmapType case ".doc", ".docx", ".odt", ".fodt": return DocumentType case ".html", ".htm": return HTMLType case ".rtf": return RtfDocumentType case ".wav": return WavSoundFileType case ".pdf": return PdfDocumentType case ".xml", ".atom": return XmlDocumentType case ".exe", ".bin", ".out", ".dylib", ".dll", ".so", ".a", ".o": return BinaryFileType } mtype := mime.TypeByExtension(ext) if strings.HasPrefix(mtype, "text/") { return TextFileType } return BinaryFileType }