Livt packages
Livt.Net
Version 1.4.1
Ethernet, IPv4, TCP, UDP, ARP, TFTP, and Telnet networking components for Livt hardware.
Published files (163)
src/tftp/TftpWriteServer.lvt
namespace Livt.Net
using Livt.IO
/**
* Single-transfer RFC 1350 octet upload engine, independent of Ethernet and storage.
* Borrows a datagram source, transactional sink and shared upload buffer; owns
* a packet snapshot. The network owner validates UDP, dispatches port 69 and TRANSFER_PORT,
* serializes Receive/Poll/Release, and sends this published payload to responseIp/
* responsePort from TRANSFER_PORT. Hold it through transmit completion before Release.
* nowMs is a wrapping uint millisecond clock. Poll regularly even without traffic.
* Reset abandons protocol state; the sink must recover unpublished writes separately.
* No RRQ, option negotiation, authentication or block-number rollover. Uploads must
* terminate by block 65535. Use on a trusted network or gate uploads externally.
* WRITE_CHUNK_BYTES bounds each staged sink write; DATA packets remain 512 bytes.
* Each block is acknowledged only after every chunk succeeds.
* STYLE selects default snapshot RAM; B accepts an alternative owned IRam with
* at least 516 bytes, including ArrayRam. Shared upload storage is selected by
* the composition root and bound to both this engine and its sink.
*/
component TftpWriteServer<P: IPacketData, S: ITftpWriteSink,
TRANSFER_PORT: int = 1069, RETRY_MS: uint = 1000, MAX_RETRIES: int = 5, WRITE_CHUNK_BYTES: int = 64, STYLE: MemoryStyle = MemoryStyle.Block,
B: IRam<byte> & new() = Ram<byte, 516, STYLE>> : IPacketData
{
static assert TRANSFER_PORT > 0 && TRANSFER_PORT <= 65535 && TRANSFER_PORT != 69, "Transfer port must differ from port 69"
static assert RETRY_MS > 0 && RETRY_MS < 2147483648, "Retry interval must fit wrapping time comparison"
static assert MAX_RETRIES > 0, "At least one retry is required"
static assert WRITE_CHUNK_BYTES > 0 && WRITE_CHUNK_BYTES <= 512, "Sink chunks must fit one TFTP block"
// Baseline opcodes, ports and payload bound.
const WRQ: int = 2
const DATA: int = 3
const ACK: byte = 4
const ERROR: byte = 5
const LISTEN_PORT: int = 69
const BLOCK_SIZE: int = 512
// Owned decoder and borrowed transactional destination/shared staging.
request: TftpRequest<P, STYLE, B>
sink: S
upload: ITftpUploadBuffer
nameLength: int
// Active peer and last successfully accepted block; completed retains final ACK.
active: bool
completed: bool
peerIp: uint
peerPort: int
lastBlock: int
lastActivity: uint
retries: int
// Stable response publication and its destination, valid until Release.
ready: bool
responseOpcode: byte
responseCode: int
public responseIp: uint
public responsePort: int
/**
* Starts idle; borrows source, sink and their shared upload buffer in one context.
*/
new(source: P, sink: S, upload: ITftpUploadBuffer)
{
request = new TftpRequest<P, STYLE, B>(source)
this.sink = sink
this.upload = upload
}
/**
* Publishes a response; caller has already established its destination.
*/
fn Respond(opcode: byte, code: int)
{
responseOpcode = opcode
responseCode = code
ready = true
}
/**
* Handles a validated datagram for either listening or transfer port. Returns
* false while a response is borrowed or for unrelated ports. A true result means
* consumed, not necessarily a reply. Peer IP is a stable network-order uint.
*/
public fn Receive(ip: uint, port: int, destination: int, nowMs: uint) bool
{
if (ready || (destination != LISTEN_PORT && destination != TRANSFER_PORT)) {
return false
}
responseIp = ip
responsePort = port
if (destination == TRANSFER_PORT && (!active || ip != peerIp || port != peerPort)) {
Respond(ERROR, 5)
return true
}
if (!request.Load()) {
if (destination == TRANSFER_PORT) {
Cancel()
}
Respond(ERROR, 4)
return true
}
if (destination == LISTEN_PORT) {
if (request.GetOpcode() == WRQ) {
AcceptRequest(ip, port, nowMs)
} else {
Respond(ERROR, 4)
}
return true
}
if (request.GetOpcode() == (ERROR as int)) {
if (!completed) {
sink.Abort()
}
active = false
return true
}
if (request.GetOpcode() != DATA || request.GetLength() < 4) {
Cancel()
Respond(ERROR, 4)
return true
}
AcceptData(nowMs)
return true
}
/**
* Accepts a new WRQ or repeats ACK zero for an identical initial request.
*/
fn AcceptRequest(ip: uint, port: int, nowMs: uint)
{
var count = request.GetWriteNameLength()
if (count == 0) {
Respond(ERROR, 4)
return
}
if (active) {
if (completed || lastBlock != 0 || ip != peerIp || port != peerPort || count != nameLength) {
Respond(ERROR, 0)
return
}
var same = true
for (var index = 0; index < count; index++) {
if (upload.GetNameByte(index) != request.GetByte(index + 2)) {
same = false
}
}
if (same) {
Respond(ACK, 0)
} else {
Respond(ERROR, 0)
}
return
}
for (var index = 0; index < count; index++) {
upload.SetNameByte(index, request.GetByte(index + 2))
}
if (!sink.Begin(count)) {
Respond(ERROR, 2)
return
}
nameLength = count
peerIp = ip
peerPort = port
lastBlock = 0
lastActivity = nowMs
retries = 0
active = true
completed = false
Respond(ACK, 0)
}
/**
* Appends only the next block, acknowledging duplicates without rewriting.
* Final ACK is published only after successful storage validation and commit.
*/
fn AcceptData(nowMs: uint)
{
var block = request.GetBlock()
if (block == lastBlock && lastBlock != 0) {
Respond(ACK, lastBlock)
return
}
if (completed || block != lastBlock + 1) {
return
}
var length = request.GetLength() - 4
if (block == 65535 && length == BLOCK_SIZE) {
sink.Abort()
active = false
Respond(ERROR, 3)
return
}
var accepted = true
var offset = 0
while (offset < length && accepted) {
var count = length - offset
if (count > WRITE_CHUNK_BYTES) {
count = WRITE_CHUNK_BYTES
}
for (var index = 0; index < count; index++) {
upload.SetDataByte(index, request.GetByte(offset + index + 4))
}
accepted = sink.Write(count)
offset = offset + count
}
if (length == 0) {
accepted = sink.Write(0)
}
if (accepted && length < BLOCK_SIZE) {
accepted = sink.Commit()
}
if (!accepted) {
sink.Abort()
active = false
Respond(ERROR, 3)
return
}
lastBlock = block
lastActivity = nowMs
retries = 0
completed = length < BLOCK_SIZE
Respond(ACK, lastBlock)
}
/**
* Retries the last ACK on timeout, aborting an incomplete upload after the retry
* budget. Completed transfers retain a bounded final-ACK grace period.
*/
public fn Poll(nowMs: uint)
{
if (ready || !active || nowMs - lastActivity < RETRY_MS) {
return
}
if (retries >= MAX_RETRIES) {
if (!completed) {
sink.Abort()
}
active = false
return
}
lastActivity = nowMs
retries++
responseIp = peerIp
responsePort = peerPort
Respond(ACK, lastBlock)
}
/**
* Cancels an incomplete upload and clears protocol state. Requires no outstanding
* network borrow; committed data survives cancellation and completed-session expiry.
*/
public fn Cancel()
{
if (active && !completed) {
sink.Abort()
}
active = false
completed = false
ready = false
}
/**
* Reports whether the retained transfer belongs to this endpoint, including final-ACK grace.
*/
public fn IsPeer(ip: uint, port: int) bool
{
return active && peerIp == ip && peerPort == port
}
/**
* Releases the response only after all network reads have completed.
*/
public fn Release()
{
ready = false
}
/**
* Reports whether a response payload is published.
*/
override fn IsReady() bool
{
return ready
}
/**
* Returns four ACK bytes or five ERROR bytes including an empty NUL message.
*/
override fn GetAvailableLength() int
{
if (!ready) {
return 0
}
if (responseOpcode == ERROR) {
return 5
}
return 4
}
/**
* Reads opcode at bytes 0..1, block/error code at 2..3, and ERROR NUL at 4.
*/
override fn TryRead(index: int, value: out byte) PacketDataResult
{
value = 0
if (!ready) {
return PacketDataResult.NotReady
}
if (index < 0 || index >= GetAvailableLength()) {
return PacketDataResult.Invalid
}
match (index)
{
1 =>
{
value = responseOpcode
}
2 =>
{
value = (responseCode >> 8) as byte
}
3 =>
{
value = responseCode as byte
}
_ =>
{
}
}
return PacketDataResult.Success
}
}