gemini.go (3781B)
1 package gemini 2 3 import ( 4 "fmt" 5 "net/url" 6 "strings" 7 ) 8 9 const ( 10 URLMaxLength = 1024 11 MetaMaxLength = 1024 12 ) 13 14 // Gemini status codes as defined in the Gemini spec Appendix 1. 15 const ( 16 StatusInput = 10 17 StatusSensitiveInput = 11 18 19 StatusSuccess = 20 20 21 StatusRedirect = 30 22 StatusRedirectTemporary = 30 23 StatusRedirectPermanent = 31 24 25 StatusTemporaryFailure = 40 26 StatusUnavailable = 41 27 StatusCGIError = 42 28 StatusProxyError = 43 29 StatusSlowDown = 44 30 31 StatusPermanentFailure = 50 32 StatusNotFound = 51 33 StatusGone = 52 34 StatusProxyRequestRefused = 53 35 StatusBadRequest = 59 36 37 StatusClientCertificateRequired = 60 38 StatusCertificateNotAuthorised = 61 39 StatusCertificateNotValid = 62 40 ) 41 42 var statusText = map[int]string{ 43 StatusInput: "Input", 44 StatusSensitiveInput: "Sensitive Input", 45 46 StatusSuccess: "Success", 47 48 // StatusRedirect: "Redirect - Temporary" 49 StatusRedirectTemporary: "Redirect - Temporary", 50 StatusRedirectPermanent: "Redirect - Permanent", 51 52 StatusTemporaryFailure: "Temporary Failure", 53 StatusUnavailable: "Server Unavailable", 54 StatusCGIError: "CGI Error", 55 StatusProxyError: "Proxy Error", 56 StatusSlowDown: "Slow Down", 57 58 StatusPermanentFailure: "Permanent Failure", 59 StatusNotFound: "Not Found", 60 StatusGone: "Gone", 61 StatusProxyRequestRefused: "Proxy Request Refused", 62 StatusBadRequest: "Bad Request", 63 64 StatusClientCertificateRequired: "Client Certificate Required", 65 StatusCertificateNotAuthorised: "Certificate Not Authorised", 66 StatusCertificateNotValid: "Certificate Not Valid", 67 } 68 69 // StatusText returns a text for the Gemini status code. It returns the empty 70 // string if the code is unknown. 71 func StatusText(code int) string { 72 return statusText[code] 73 } 74 75 // SimplifyStatus simplify the response status by ommiting the detailed second digit of the status code. 76 func SimplifyStatus(status int) int { 77 return (status / 10) * 10 78 } 79 80 // IsStatusValid checks whether an int status is covered by the spec. 81 // Note that: 82 // A client SHOULD deal with undefined status codes 83 // between '10' and '69' per the default action of the initial digit. 84 func IsStatusValid(status int) bool { 85 _, found := statusText[status] 86 return found 87 } 88 89 // StatusInRange returns true if the status has a valid first digit. 90 // This means it can be handled even if it's not defined by the spec, 91 // because it has a known category 92 func StatusInRange(status int) bool { 93 if status < 10 || status > 69 { 94 return false 95 } 96 return true 97 } 98 99 // CleanStatus returns the status code as is, unless it's invalid but still in range 100 // Then it returns the status code with the second digit zeroed. So 51 returns 51, 101 // but 22 returns 20. 102 // 103 // This corresponds with the spec: 104 // A client SHOULD deal with undefined status codes 105 // between '10' and '69' per the default action of the initial digit. 106 func CleanStatus(status int) int { 107 // All the functions come together! 108 if !IsStatusValid(status) && StatusInRange(status) { 109 return SimplifyStatus(status) 110 } 111 return status 112 } 113 114 // QueryEscape provides URL query escaping in a way that follows the Gemini spec. 115 // It is the same as url.PathEscape, but it also replaces the +, because Gemini 116 // requires percent-escaping for queries. 117 func QueryEscape(query string) string { 118 return strings.ReplaceAll(url.PathEscape(query), "+", "%2B") 119 } 120 121 // QueryUnescape is the same as url.PathUnescape 122 func QueryUnescape(query string) (string, error) { 123 return url.PathUnescape(query) 124 } 125 126 type Error struct { 127 Err error 128 Status int 129 } 130 131 func (e Error) Error() string { 132 return fmt.Sprintf("Status %d: %v", e.Status, e.Err) 133 } 134 135 func (e Error) Unwrap() error { 136 return e.Err 137 }