Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
--health-retries 10
env:
# The oldest iyi this release supports (README, Requirements).
IYI_VERSION: 0.20.0
IYI_VERSION: 0.21.0
DATABASE_URL: postgres://postgres:postgres@127.0.0.1/iyi_pg_test
steps:
- uses: actions/checkout@v4
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
## Unreleased

Requires iyi 0.21.0 and iyi-db 0.5.0. iyi 0.21.0 refuses an operation on
raw memory in a module that is not marked `@[Unsafe("why")]` (SPEC.md
III.13), and 0.20.x refuses the mark above a `module` header, so no source
of this driver compiles on both.

* The four modules that handle raw memory say so, each with why:
`pg/pq/stream` (the socket's read and write buffers),
`pg/pq/connection` (`read_direct`'s pointer into the caller's buffer),
`pg/copy_result` (a copy read straight into the caller's slice) and
`pg/pq/scram` (the HMAC pads and client proof written into new
strings).
* `pg/core`, `pg/pq/frame`, `pg/pq/param` and `pg/replication/messages`
no longer touch raw memory: a string is made from bytes with
`String.new(bytes)` and a string's bytes are read through `Bytes.of`
or `byte_at`, so the compiler checks them. The tests and
`examples/simple_query` do the same.

## v0.4.0

A statement cache, and the driver's module state made safe to reach from
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ are closed, and tasks are structured.

## Requirements

iyi 0.20.0 or later, and iyi-db 0.4.0 (required by this package's `iyi.mod`).
iyi 0.21.0 or later, and iyi-db 0.5.0 (required by this package's `iyi.mod`).
0.21.0 refuses raw memory in a module that does not say it handles some
(SPEC.md III.13), and a module says so with `@[Unsafe("why")]` above its
`module` header, which 0.20.x refuses.
Tested against PostgreSQL 16.

## Installation
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_query.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class SimpleConnection
end

private def text(bytes : Bytes) : String
String.new(bytes.size) { |buffer| buffer.copy_from(bytes.to_unsafe, bytes.size) }
String.new(bytes)
end
end

Expand Down
2 changes: 1 addition & 1 deletion iyi.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `iyi get example.com/someone/lib` adds a dependency here.
module github.com/iyilang/pg

require github.com/iyilang/db v0.4.0 as db
require github.com/iyilang/db v0.5.0 as db
2 changes: 1 addition & 1 deletion iyi.sum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github.com/iyilang/db v0.4.0 s1:eb713fe8bccc5b89be8d2efa16e515ae96667a15
github.com/iyilang/db v0.5.0 s1:a435c49b63b1fa7cbfbc97d6d36d5f83636f4675
1 change: 1 addition & 0 deletions pg/copy_result.iyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# The data of a `COPY ... TO STDOUT`, or the sink of a `COPY ... FROM
# STDIN`: what `PGConnection#exec_copy` answers.
@[Unsafe("a copy is read straight into the caller's slice through its pointer, and gets_to_end writes each chunk from its buffer's pointer")]
module pg/copy_result

import std/int
Expand Down
4 changes: 1 addition & 3 deletions pg/core.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,7 @@ pub class PGResultSet < ResultSet
end

def self.bytes_string(bytes : Bytes) : String
String.new(bytes.size) do |buffer|
buffer.copy_from(bytes.to_unsafe, bytes.size)
end
String.new(bytes)
end

private def field(index : Int32) : Field
Expand Down
2 changes: 1 addition & 1 deletion pg/escape_test.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import pg/testing/spec_helper::*

def escape_literal_bytes(s : String) : String
result = ""
with_connection { |conn| result = conn.escape_literal(Bytes.new(s.to_unsafe, s.bytesize, read_only: true)) }
with_connection { |conn| result = conn.escape_literal(Bytes.of(s)) }
result
end

Expand Down
1 change: 1 addition & 0 deletions pg/pq/connection.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Every read answers its failure: `StreamError` when the socket fails,
# `PQError` when the server answers an `ErrorResponse`, `ConnectionError`
# when the session cannot go on.
@[Unsafe("read_direct hands the stream a raw pointer into the caller's buffer to fill")]
module pg/pq/connection

import std/int
Expand Down
6 changes: 2 additions & 4 deletions pg/pq/frame.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,7 @@ pub abstract class Frame

# *count* bytes of *bytes* from *start*, as a string.
def self.string_of(bytes : Bytes, start : Int32, count : Int32) : String
String.new(count) do |buffer|
buffer.copy_from(bytes.to_unsafe + start, count)
end
String.new(bytes[start, count])
end

# A message this library does not know.
Expand Down Expand Up @@ -372,7 +370,7 @@ pub abstract class Frame
private def tag_starts_with?(prefix : String) : Bool
size = prefix.bytesize
return false if @bytes.size <= size
bytes = prefix.to_unsafe
bytes = Bytes.of(prefix)
i = 0
while i < size
return false if @bytes[i] != bytes[i]
Expand Down
5 changes: 2 additions & 3 deletions pg/pq/param.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub struct Param
end

def self.text(string : String) : Param
new(Bytes.new(string.to_unsafe, string.bytesize, read_only: true), string.bytesize, 0_i16, string)
new(Bytes.of(string), string.bytesize, 0_i16, string)
end

# The array literal of *array*: `{1,2,3}`, `{"a","b"}`, `{NULL}`.
Expand Down Expand Up @@ -165,9 +165,8 @@ pub struct Param
end

private def self.write_hex(io : String::Builder, bytes : Bytes) : Nil
digits = HEX.to_unsafe
bytes.each do |byte|
io << digits[byte >> 4].unsafe_chr << digits[byte & 0x0F].unsafe_chr
io << HEX.byte_at(byte >> 4).unsafe_chr << HEX.byte_at(byte & 0x0F).unsafe_chr
end
end

Expand Down
2 changes: 1 addition & 1 deletion pg/pq/param_test.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ end

# Crystal's `String#to_slice`, which iyi's prelude lacks.
def to_slice(s : String) : Bytes
Bytes.new(s.to_unsafe, s.bytesize, read_only: true)
Bytes.of(s)
end

describe "PQ::Param encoders #encode_array" do
Expand Down
1 change: 1 addition & 0 deletions pg/pq/scram.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# SCRAM-SHA-256-PLUS binds the exchange to the TLS channel under it
# (`tls-server-end-point`, RFC 5929, the binding PostgreSQL takes): a man
# in the middle who ended TLS on both sides cannot relay it.
@[Unsafe("the HMAC pads, PBKDF2's block number and the client proof are written a byte at a time straight into new strings' buffers")]
module pg/pq/scram

import std/int
Expand Down
1 change: 1 addition & 0 deletions pg/pq/stream.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#
# Every read answers `StreamError` when the bytes asked for are not there:
# the socket failed, or the source ended first.
@[Unsafe("the socket's reads and writes go through read and write buffers it indexes by pointer, copying frames in and out of them and reading floats by reinterpreting their bits")]
module pg/pq/stream

import std/int
Expand Down
4 changes: 2 additions & 2 deletions pg/pq/stream_test.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ end
def read_message(bytes : Array(UInt8)) : String
answer = ""
socket_pair do |client, server|
server.write(String.new(bytes.size) { |b| bytes.each_with_index { |byte, i| b[i] = byte } })
server.write(String.new(bytes))
pq = PQConnection.new(Stream.new(client), ConnInfo.new("127.0.0.1", "db", "user", "pass", 1))
frame = pq.read
answer = frame.is_a?(StreamError) ? "StreamError: #{frame.message}" : frame.to_s
Expand Down Expand Up @@ -86,7 +86,7 @@ describe "PQ::Stream#flush" do
end
assert received.size == big + small.bytesize
assert received[0, big].all? { |byte| byte == 'a'.ord.to_u8 }
tail = String.new(small.bytesize) { |b| b.copy_from(received.to_unsafe + big, small.bytesize) }
tail = String.new(received[big, small.bytesize])
assert tail == small, tail
assert stream.pending == 0
end
Expand Down
4 changes: 1 addition & 3 deletions pg/replication/messages.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ pub class Read
break if byte == 0_u8
bytes << byte
end
String.new(bytes.size) do |buffer|
bytes.each_with_index { |b, i| buffer[i] = b }
end
String.new(bytes)
end

# An Int32 length, then that many bytes.
Expand Down
4 changes: 2 additions & 2 deletions pg/replication_test.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class TestMessageHandler < Handler
joined = Bytes.new(size)
offset = 0
parts.each do |part|
(joined.to_unsafe + offset).copy_from(part.to_unsafe, part.size)
joined[offset, part.size].copy_from(part)
offset += part.size
end
joined
Expand Down Expand Up @@ -202,7 +202,7 @@ end
# --------------------------------------------------------------- the bytes

def slice_of(text : String) : Bytes
Bytes.new(text.bytesize) { |i| text.to_unsafe[i] }
Bytes.of(text).dup
end

# Protocol bytes, big-endian, built up a value at a time.
Expand Down
Loading