5 Universal information
Dither Dude edited this page 2025-09-11 12:17:25 +10:00

Note

All the information below is not unique to this specific repo, but concepts and laws I structure my code around, or methods that are standardised across my Github projects. As a result, this page will likely move outside of this repo, as its concepts can apply to virtually all of my Rust projects.

Toward the end of 2024, I completely migrated from being a Windows "power-user" to a Linux (Kubuntu & Arch) daily-driver, and have since migrated from C# to Rust. Therefore, the below does not apply to any of my C# projects.

Rust Language-Specific Ethics

This is a small list of laws I will always oblige by when using the Rust language, and by extension will advocate strongly that all contributions to any of my code follow these guidelines.

expect()*

This one ties in strongly with the next law. Using .expect() in code shows careless error handling, as this always results in a panic - even if the error could've been preempted and safely handled. For example, if the user is requested to type in a username, and the readline has .expect("Missing Username!");, the program will panic, and the user will have to restart the program all over again - which could be very unfortunate if the user accidentally hit the Return key. Wrapping this in a match statement (or a loop) can instead check for a Err(_) value, and re-request the username, without needing the panic the program.

unwrap()*

I swear you are trying to kill me. For example, even if there is a malicious user who sends a packet with no data, the server can still send a graceful termination message, without bloating the program output with called unwrap() on None.

*Tests

The aforementioned laws are only true in regards to executable code, to ensure errors are handled gracefully. However, the exact opposite is true with regards to tests - tests should panic when something goes wrong, as gracefully handling such cases usually defeats the purpose of code testing.

package.workspace = true

I NEVER want to have to deal with an issue where "it is working in server but then not working in client... even though the exact same code!? Oh right the important-package is version 2.0.3 in server and 1.18.4 in client..."

ALL packages are to be declared in the workspace's Cargo.toml file. If client needs to use say random, in the workspace Cargo.toml it will have random = "0.14.0", and in client's Cargo.toml, it will read random.workspace = true.

Utilities

These functions are generally found in ./utils/src/lib.rs, and are usually the same across projects. Newer projects may contain extended implementations of these functions, but are still backward-compatible (i.e. you can replace these functions in older projects with their implementation in newer projects, and they should work flawlessly).

How To Write A Packet

Server-and-Client applications need a uniform method to send and receive data - one that allows for virtually unlimited data to be sent in a single 'packet', with as little additional overhead as possible. All data transfers use TCP. The utils crate is the only place in my repos that use the read and write methods on my TcpStream objects, as all data must be transferred through this protocol.

A 'hello' packet preempts every block of data that is sent. This special packet tells the receiver how many bytes to expect to read in the packet. This helps mitigate programs hanging while expecting data that will never come. This packet is 2b long, and has three variants:

Value Implication
0x0000 Null terminator. This is sent after a 0xFFFF if the data exactly fills the buffer to indicate to stop receiving data.
0x0001..0xFFFF Expect packet of this specified length, and return read data.
0xFFFF Expect packet of length 65,535b (or 65.535KB), and await another packet to arrive.

Directly afterward, the sender sends a block of data to fill the length it announced in the 'hello' packet. Therefore, if a server wishes to communicate Hello, World! to a client, the packet will look as such:

\x0D\x00
\x48\x65\x6C\x6C\x6F\x2C\x20\x57\x6F\x72\x6C\x64\x21

This translates to:

  • Expect 13b
  • Read string Hello, World!

Obviously, the data transmision protocol will not convert the data to a string, and will instead return it as a vector of Bytes.

Sending Error Codes

If and when something goes wrong, server should notify client that an irrecoverable state has been reached, and thus the connection needs to be terminated. As Status Codes are u32, client knows server has faulted if the received data is of length 4b. An error response could look as such:

\xA5\x01\x00\x00

This translates to:

  • server has received the data successfully, but is unsure what to do with the data (See Status Codes for a full breakdown)

Once an error packet is sent to client, the connection is instantly terminated.

Status Codes

Whenever server sends a response to client, client needs to what happened if there was an error, or how something succeeded - as sometimes even when the request is completed successfully, some data was mutated. Status codes have three variants: Error, Informative, and Local.

Warning

While these are based off HTTP status codes, some functionality may differ from IANA's implementation, or some codes may exist here that are not real HTTP status codes.

Errors

These indicate that server reached an invalid state. server is now acknowledging that is cannot continue with the request, and thus is gracefully terminating the handshake.

Informative

These indicate that server successfully processed the data. These are usually (but not bound to be) attached to a response. These can also indicate that while the request completed, the response may not reflect the response client was expecting.

Local

These are not sent over the network, but rather baked into either the server or the client to indicate that something internal went wrong.

Caution

As of writing, Local statuses are in groups 4xx and 5xx, respectively. As this subset of statuses is not sent with a data packet, they will likely be moved to their own status group.


While in this documentation status codes are referred to by their name, each status code corresponds to a u32 integer value. Though the code for the utils crate has a basic explanation for each error code, This table attempt to elaborate here on the underlying meaning of each status code.

Name Value Type Implication
TEST_NOT_IMPLEMENTED 0u32 - Spare testing error code, used as placeholders for WIP protocols.
SUCCESS 200u32 Info server was able to successfully complete client's request.
NON_AUTHORITATIVE 203u32 Info Request completed, but something is likely incorrect with the response.
PERMANENT_REDIRECT 301u32 Error server has relocated. Please re-route all future requests to the new location (usually specified in the response).
FOUND 302u32 Info While client expects no more data to be received in a recursive request, server affirms that there is an additional followup.
BAD_REQUEST 400u32 Error This type of request is not implemented for this server. Likely cause of client expecting server to be of different type.
TOO_SMALL 402u32 Error server expected a larger packet.
FORBIDDEN 403u32 Error client is not allowed to access this resource.
NOT_FOUND 404u32 Error server cannot find the file client has requested.
GONE 410u32 Info While client expects additional responses in a recursive request, server affirms that there is no more data to be received. This is the opposite of FOUND.
MISDIRECTED 421u32 Error server tried to complete request, but exhausted all attempts to do so.
UNPROCESSABLE 422u32 Error While the request was correctly formatted, server does not support the protcol client expects.
UPGRADE_REQUIRED 426u32 Error client is too old for server, and incompatible. server advises client to upgrade API versions.
DOWNGRADE_REQUIRED 427u32 Error client is too new for server, and incompatible. server advises client to downgrade API versions.
HOST_UNREACHABLE 432u32 Local server does not exist, or cannot be reached. client or server is likely offline.
SHAT_THE_BED 433u32 Local client reached an invalid state.
NOT_IMPLEMENTED 501u32 Error {Unused}
LOOP_DETECTED 508u32 Local A recursive request (see FOUND and GONE) has looped. Client is terminating lookup to avoid panic (like stackoverflow or hang).
BAD_RESPONSE 512u32 Local server responded with something client did not expect.

Compatibility

Before server and client negotiate information, they should both ensure that they are using the correct API version to communicate. If client's API version is newer than server's and is not backward-compatible, server sends Status Code DOWNGRADE_REQUIRED. Likewise, if client is too old, server sends UPGRADE_REQUIRED. In an example version 7.5.2, 7 is the "Major", 5 is the "Minor", and 2 is the "Patch" ("Tiny").

The following rules are used to determine whether client and server are compatible:

  • Is the Major of client or server equal to 0?
    • Yes: Compare client's Minor to server's
      • client's is greater: return error DOWNGRADE_REQUIRED.
      • client's is less: return error UPGRADE REQUIRED.
      • equal:
        • Is the Tiny of client greater than server's?
          • Yes: return error DOWNGRADE_REQUIRED.
          • No: APIs compatible, resume action.
    • No: Compare client's Major to server's
      • client's is greater: return error DOWNGRADE_REQUIRED.
      • client's is less: return error UPGRADE REQUIRED.
      • equal:
        • Is the Minor of client greater than server's?
          • Yes: return error DOWNGRADE_REQUIRED.
          • No: APIs compatible, resume action.

General Programming Ethics

These aren't necessarily related to Rust, but more a universal set of rules that help with continuity across my projects.

Byte Conversion

There are two ways to convert bytes to numbers: Little Endian and Big Endian. All my projects use Little Endian to en/decode bytestreams. There was no special reason for this decision, aside for that is what was used in the tutorial I followed to understand how to use Rust's TcpStream.

Binary Flags

Note

Binary as in compiled executable, not as in there are two types of flags.

...but actually, there are two types of flags. Usually, each Long-Named flag will have an identical Short-Named flag that performs the exact same functionalilty.

Short-Named Flags

Short-Named flags will always start with a hyphen ("-") character, followed by a single, case-sensitive character, like -v or -R. As all Short-Named flags are only one character long, they can be concatenated, so -a -B -c becomes -aBc.

Long-Named Flags

Long-Named flags will always start with a double-hyphen ("--"), and usually spell out their functionality to make them easier to remember. As such, Long-Named flags cannot be concatenated like Short-Named flags. A sample Long-Named flag may look like --enable-feature. Long-Named flags and Short-Named flags can be used in conjuction with each other on the same binary.

Arguments

Some flags take arguments to be passed into the program. Unless otherwise specified, the last declaration of a flag value takes precedence over prior declarations. A sample flag is --port, where this flag takes a port number as an argument to be passed to the program.

# Sets the port to "5"
./program --port 5

# Sets the port to "5", using Short-Named flags
./program -p 5

# Sets the port to "4", and then overrides the port to "5"
./program --port 4 --port 5

With concatenated Short-Named flags, each flag that requires an argument consumes the next argument of the command-line.

# 'p' gets "Arg1", 'q' gets "Arg2 Arg2.5", and 'r' gets "Arg3"
./program -pqr Arg1 "Arg2 Arg2.5" Arg3

However, this only applies for flags that take arguments:

# 'p' => consumes an argument (u16)
# 'v' => no consumption
# 'b' => no consumption
# 'i' => consumes an argument (FilePath)
# 'o' => consumes an argument (FilePath)
./program -pvpbio 5 3 /file/in /file/out
# 'p' set to "5" but overwritten to '3', 'v' and 'b' are both enabled, 'i' is set to "/file/in" and 'o' is set to "/file/out".

Incrementors

These flags don't take values, but rather update a value for each instance of the flag. This is currently only employed in the --verbose/-v flag.

# Increase verbosity from "INFO" (default) to "DEBUG" (one after INFO):
./program --verbose

# Increase verbosity to "TRACE" (one after DEBUG):
./program -vv

Red Herrings

Any argument that does not start with - or -- is ignored. It is therefore possible to write sentences in the command line arguments, as long as the Red Herrings do not interfere with any argumentative flags.

./program dont forget to change the -pov 15 /file.out option!
# Assuming same variables as prior example, 'p' consumes "15", and 'o' consumes "/file.out".