2
0

param_binary.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. package pgserver
  2. import (
  3. "encoding/binary"
  4. "fmt"
  5. "math"
  6. "strconv"
  7. "strings"
  8. "github.com/danfragoso/pizzasql-next/pkg/executor"
  9. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  10. )
  11. // binaryArrayLiteral decodes a PostgreSQL binary array parameter into the typed
  12. // array literal the executor consumes. Element text is re-encoded into array
  13. // input text and canonicalized, so a malformed element fails at Bind with
  14. // 22P02 exactly like a malformed text-format array.
  15. func binaryArrayLiteral(elementType string, value []byte) (string, error) {
  16. text, err := binaryArrayText(elementType, value)
  17. if err != nil {
  18. return "", err
  19. }
  20. arrayType := elementType + "[]"
  21. canonical, err := executor.CanonicalizeArrayText(text, arrayType)
  22. if err != nil {
  23. return "", err
  24. }
  25. return "'" + strings.ReplaceAll(canonical, "'", "''") + "'::" + arrayType, nil
  26. }
  27. // binaryArrayText converts a binary array parameter to PostgreSQL array text.
  28. // The envelope is ndim, hasnull, element OID, one dimension header, then
  29. // length-prefixed elements; every read is bounds-checked and any structural
  30. // problem is reported as invalid text input rather than a partial decode.
  31. func binaryArrayText(elementType string, value []byte) (string, error) {
  32. if len(value) < 12 {
  33. return "", pgerror.InvalidText("binary array parameter is truncated")
  34. }
  35. dimensions := int32(binary.BigEndian.Uint32(value[0:4]))
  36. hasNull := int32(binary.BigEndian.Uint32(value[4:8]))
  37. elementOID := binary.BigEndian.Uint32(value[8:12])
  38. if dimensions < 0 || hasNull < 0 || hasNull > 1 {
  39. return "", pgerror.InvalidText("invalid binary array header")
  40. }
  41. if dimensions > 1 {
  42. return "", pgerror.InvalidText("multidimensional arrays are not supported")
  43. }
  44. expectedOID, ok := binaryArrayElementOID(elementType)
  45. if !ok {
  46. return "", fmt.Errorf("%w: binary array element type %s", pgerror.ErrFeatureNotSupported, elementType)
  47. }
  48. if elementOID != expectedOID {
  49. return "", pgerror.InvalidText("binary array element OID %d does not match %s[]", elementOID, elementType)
  50. }
  51. position := 12
  52. count := 0
  53. if dimensions == 1 {
  54. if position+8 > len(value) {
  55. return "", pgerror.InvalidText("binary array parameter is truncated")
  56. }
  57. length := int32(binary.BigEndian.Uint32(value[position : position+4]))
  58. if length < 0 {
  59. return "", pgerror.InvalidText("invalid binary array dimension length")
  60. }
  61. count = int(length)
  62. position += 8 // length and lower bound; elements are read in order
  63. }
  64. // Every element has at least its 4-byte length prefix, so a corrupt count
  65. // cannot drive a long loop over bytes that are not there.
  66. if count > (len(value)-position)/4 {
  67. return "", pgerror.InvalidText("binary array parameter is truncated")
  68. }
  69. var builder strings.Builder
  70. builder.WriteByte('{')
  71. for i := 0; i < count; i++ {
  72. if position+4 > len(value) {
  73. return "", pgerror.InvalidText("binary array parameter is truncated")
  74. }
  75. length := int32(binary.BigEndian.Uint32(value[position : position+4]))
  76. position += 4
  77. if i > 0 {
  78. builder.WriteByte(',')
  79. }
  80. if length == -1 {
  81. if hasNull == 0 {
  82. return "", pgerror.InvalidText("binary array has a NULL element without the null flag")
  83. }
  84. builder.WriteString("NULL")
  85. continue
  86. }
  87. if length < 0 || position+int(length) > len(value) {
  88. return "", pgerror.InvalidText("binary array parameter is truncated")
  89. }
  90. text, err := binaryArrayElementText(elementOID, value[position:position+int(length)])
  91. if err != nil {
  92. return "", err
  93. }
  94. position += int(length)
  95. builder.WriteString(arrayElementLiteral(text))
  96. }
  97. if position != len(value) {
  98. return "", pgerror.InvalidText("binary array parameter has trailing bytes")
  99. }
  100. builder.WriteByte('}')
  101. return builder.String(), nil
  102. }
  103. // binaryArrayElementOID reports the PostgreSQL element OID a supported array
  104. // parameter type must carry in its binary envelope.
  105. func binaryArrayElementOID(elementType string) (uint32, bool) {
  106. switch elementType {
  107. case "TEXT":
  108. return 25, true
  109. case "VARCHAR":
  110. return 1043, true
  111. case "INTEGER":
  112. return 23, true
  113. case "BIGINT":
  114. return 20, true
  115. case "SMALLINT":
  116. return 21, true
  117. case "BOOLEAN":
  118. return 16, true
  119. case "REAL":
  120. return 700, true
  121. case "DOUBLE":
  122. return 701, true
  123. case "NUMERIC":
  124. return 1700, true
  125. default:
  126. return 0, false
  127. }
  128. }
  129. // binaryArrayElementText decodes one element to its PostgreSQL text spelling.
  130. func binaryArrayElementText(elementOID uint32, data []byte) (string, error) {
  131. switch elementOID {
  132. case 25, 1043: // text, varchar
  133. return string(data), nil
  134. case 16: // boolean
  135. if len(data) != 1 || data[0] > 1 {
  136. return "", pgerror.InvalidText("invalid binary boolean array element")
  137. }
  138. if data[0] == 0 {
  139. return "f", nil
  140. }
  141. return "t", nil
  142. case 21: // int2
  143. if len(data) != 2 {
  144. return "", pgerror.InvalidText("invalid binary int2 array element")
  145. }
  146. return strconv.FormatInt(int64(int16(binary.BigEndian.Uint16(data))), 10), nil
  147. case 23: // int4
  148. if len(data) != 4 {
  149. return "", pgerror.InvalidText("invalid binary int4 array element")
  150. }
  151. return strconv.FormatInt(int64(int32(binary.BigEndian.Uint32(data))), 10), nil
  152. case 20: // int8
  153. if len(data) != 8 {
  154. return "", pgerror.InvalidText("invalid binary int8 array element")
  155. }
  156. return strconv.FormatInt(int64(binary.BigEndian.Uint64(data)), 10), nil
  157. case 700: // float4
  158. if len(data) != 4 {
  159. return "", pgerror.InvalidText("invalid binary float4 array element")
  160. }
  161. return strconv.FormatFloat(float64(math.Float32frombits(binary.BigEndian.Uint32(data))), 'g', -1, 32), nil
  162. case 701: // float8
  163. if len(data) != 8 {
  164. return "", pgerror.InvalidText("invalid binary float8 array element")
  165. }
  166. return strconv.FormatFloat(math.Float64frombits(binary.BigEndian.Uint64(data)), 'g', -1, 64), nil
  167. case 1700: // numeric
  168. return binaryNumericText(data)
  169. default:
  170. return "", fmt.Errorf("%w: binary array element OID %d", pgerror.ErrFeatureNotSupported, elementOID)
  171. }
  172. }
  173. // arrayElementLiteral quotes one element for array input text. The result is
  174. // still validated by CanonicalizeArrayText, so this only has to be parseable.
  175. func arrayElementLiteral(text string) string {
  176. needsQuote := text == "" || strings.EqualFold(text, "NULL")
  177. for i := 0; i < len(text) && !needsQuote; i++ {
  178. switch text[i] {
  179. case '{', '}', ',', '"', '\\', ' ', '\t', '\n', '\r':
  180. needsQuote = true
  181. }
  182. }
  183. if !needsQuote {
  184. return text
  185. }
  186. var builder strings.Builder
  187. builder.WriteByte('"')
  188. for i := 0; i < len(text); i++ {
  189. if text[i] == '"' || text[i] == '\\' {
  190. builder.WriteByte('\\')
  191. }
  192. builder.WriteByte(text[i])
  193. }
  194. builder.WriteByte('"')
  195. return builder.String()
  196. }
  197. // binaryNumericText converts PostgreSQL's binary numeric representation to its
  198. // exact decimal text. NUMERIC elements stay text in the engine, so the digits
  199. // must not pass through float64: the representation is base-10000 digits, a
  200. // base-10000 weight, a sign, and a display scale.
  201. func binaryNumericText(data []byte) (string, error) {
  202. if len(data) < 8 {
  203. return "", pgerror.InvalidText("binary numeric parameter is truncated")
  204. }
  205. ndigits := int(binary.BigEndian.Uint16(data[0:2]))
  206. weight := int(int16(binary.BigEndian.Uint16(data[2:4])))
  207. sign := binary.BigEndian.Uint16(data[4:6])
  208. scale := int(binary.BigEndian.Uint16(data[6:8]))
  209. if len(data) != 8+ndigits*2 {
  210. return "", pgerror.InvalidText("binary numeric parameter has an inconsistent digit count")
  211. }
  212. if scale > 16383 {
  213. return "", pgerror.InvalidText("binary numeric parameter has an unsupported scale")
  214. }
  215. digits := make([]int, ndigits)
  216. for i := range digits {
  217. digits[i] = int(binary.BigEndian.Uint16(data[8+i*2:]))
  218. if digits[i] > 9999 {
  219. return "", pgerror.InvalidText("binary numeric parameter has an invalid digit")
  220. }
  221. }
  222. switch sign {
  223. case 0x0000, 0x4000:
  224. case 0xC000:
  225. return "NaN", nil
  226. case 0xD000:
  227. return "Infinity", nil
  228. case 0xF000:
  229. return "-Infinity", nil
  230. default:
  231. return "", pgerror.InvalidText("binary numeric parameter has an invalid sign")
  232. }
  233. var integer strings.Builder
  234. if weight >= 0 {
  235. for position := weight; position >= 0; position-- {
  236. index := weight - position
  237. digit := 0
  238. if index < ndigits {
  239. digit = digits[index]
  240. }
  241. if position == weight {
  242. integer.WriteString(strconv.Itoa(digit))
  243. } else {
  244. fmt.Fprintf(&integer, "%04d", digit)
  245. }
  246. }
  247. }
  248. integerPart := strings.TrimLeft(integer.String(), "0")
  249. if integerPart == "" {
  250. integerPart = "0"
  251. }
  252. var fractional strings.Builder
  253. group := 1
  254. for fractional.Len() < scale {
  255. digit := 0
  256. if index := weight + group; index >= 0 && index < ndigits {
  257. digit = digits[index]
  258. }
  259. fmt.Fprintf(&fractional, "%04d", digit)
  260. group++
  261. }
  262. fractionPart := fractional.String()[:scale]
  263. var result strings.Builder
  264. if sign == 0x4000 {
  265. result.WriteByte('-')
  266. }
  267. result.WriteString(integerPart)
  268. if fractionPart != "" {
  269. result.WriteByte('.')
  270. result.WriteString(fractionPart)
  271. }
  272. return result.String(), nil
  273. }
  274. // binaryFloatText decodes a scalar binary float4 or float8 parameter to the
  275. // decimal text the executor parses.
  276. func binaryFloatText(oid int32, value []byte) (string, error) {
  277. switch oid {
  278. case 700:
  279. if len(value) != 4 {
  280. return "", pgerror.InvalidText("invalid binary float4 parameter")
  281. }
  282. return strconv.FormatFloat(float64(math.Float32frombits(binary.BigEndian.Uint32(value))), 'g', -1, 32), nil
  283. case 701:
  284. if len(value) != 8 {
  285. return "", pgerror.InvalidText("invalid binary float8 parameter")
  286. }
  287. return strconv.FormatFloat(math.Float64frombits(binary.BigEndian.Uint64(value)), 'g', -1, 64), nil
  288. default:
  289. return "", pgerror.InvalidText("unsupported binary floating-point OID %d", oid)
  290. }
  291. }