Skip to content

Commit a9b783d

Browse files
committed
Merge branch 'copy-packetline'
2 parents 68d1a29 + ebb6ef5 commit a9b783d

22 files changed

+2286
-3
lines changed

.gitattributes

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
**/generated-archives/*.tar.xz filter=lfs-disabled diff=lfs merge=lfs -text
22

3-
# assure line feeds don't interfere with our working copy hash
3+
# assure line feeds don't interfere with our working copy hash
44
**/tests/fixtures/**/*.sh text crlf=input eol=lf
55
/justfile text crlf=input eol=lf
6+
7+
# have GitHub treat the gix-packetline-blocking src copy as auto-generated
8+
gix-packetline-blocking/src/ linguist-generated=true

.github/workflows/ci.yml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ jobs:
176176
# Let's not fail CI for this, it will fail locally often enough, and a crate a little bigger
177177
# than allows is no problem either if it comes to that.
178178
just check-size || true
179-
179+
180180
cargo-deny:
181181
runs-on: ubuntu-latest
182182
strategy:
@@ -193,6 +193,7 @@ jobs:
193193
- uses: EmbarkStudios/cargo-deny-action@v1
194194
with:
195195
command: check ${{ matrix.checks }}
196+
196197
wasm:
197198
name: WebAssembly
198199
runs-on: ubuntu-latest
@@ -213,3 +214,32 @@ jobs:
213214
name: crates with 'wasm' feature
214215
- run: cd gix-pack && cargo build --all-features --target ${{ matrix.target }}
215216
name: gix-pack with all features (including wasm)
217+
218+
check-packetline:
219+
strategy:
220+
fail-fast: false
221+
matrix:
222+
os:
223+
- ubuntu-latest
224+
# We consider this script read-only and its effect is the same everywhere.
225+
# However, when changes are made to `etc/copy-packetline.sh`, re-enable the other platforms for testing.
226+
# - macos-latest
227+
# - windows-latest
228+
runs-on: ${{ matrix.os }}
229+
defaults:
230+
run:
231+
shell: bash
232+
steps:
233+
- uses: actions/checkout@v4
234+
- name: Check that working tree is initially clean
235+
run: |
236+
set -x
237+
git status
238+
git diff --exit-code
239+
- name: Regenerate gix-packetline-blocking/src
240+
run: etc/copy-packetline.sh
241+
- name: Check that gix-packetline-blocking/src was already up to date
242+
run: |
243+
set -x
244+
git status
245+
git diff --exit-code

etc/copy-packetline.sh

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/bin/bash
2+
3+
set -euC -o pipefail
4+
5+
readonly input_dir='gix-packetline/src'
6+
readonly output_parent_dir='gix-packetline-blocking'
7+
readonly output_dir="$output_parent_dir/src"
8+
9+
function fail () {
10+
printf '%s: error: %s\n' "$0" "$1" >&2
11+
exit 1
12+
}
13+
14+
function chdir_toplevel () {
15+
local root_padded root
16+
17+
# Find the working tree's root. (Padding covers the trailing-newline case.)
18+
root_padded="$(git rev-parse --show-toplevel && echo -n .)" ||
19+
fail 'git-rev-parse failed to find top-level dir'
20+
root="${root_padded%$'\n.'}"
21+
22+
cd -- "$root"
23+
}
24+
25+
function merging () {
26+
local git_dir_padded git_dir
27+
28+
# Find the .git directory. (Padding covers the trailing-newline case.)
29+
git_dir_padded="$(git rev-parse --git-dir && echo -n .)" ||
30+
fail 'git-rev-parse failed to find git dir'
31+
git_dir="${git_dir_padded%$'\n.'}"
32+
33+
test -e "$git_dir/MERGE_HEAD"
34+
}
35+
36+
function output_dir_status () {
37+
git status --porcelain --ignored=traditional -- "$output_dir" ||
38+
fail 'git-status failed'
39+
}
40+
41+
function check_output_dir () {
42+
if ! test -e "$output_dir"; then
43+
# The destination does not exist on disk, so nothing will be lost. Proceed.
44+
return
45+
fi
46+
47+
if merging; then
48+
# In a merge, it would be confusing to replace anything at the destination.
49+
if output_dir_status | grep -q '^'; then
50+
fail 'output location exists, and a merge is in progress'
51+
fi
52+
else
53+
# We can lose data if anything of value at the destination is not in the
54+
# index. (This includes unstaged deletions, for two reasons. We could lose
55+
# track of which files had been deleted. More importantly, replacing a
56+
# staged symlink or regular file with an unstaged directory is shown by
57+
# git-status as only a deletion, even if the directory is non-empty.)
58+
if output_dir_status | grep -q '^.[^ ]'; then
59+
fail 'output location exists, with unstaged changes or ignored files'
60+
fi
61+
fi
62+
}
63+
64+
function first_line_ends_crlf () {
65+
# This is tricky to check portably. In Cygwin-like environments including
66+
# MSYS2 and Git Bash, most text processing tools, including awk, sed, and
67+
# grep, automatically ignore \r before \n. Some ignore \r everywhere. Some
68+
# can be told to keep \r, but in non-portable ways that may affect other
69+
# implementations. Bash ignores \r in some places even without "-o igncr",
70+
# and ignores \r even more with it, including in all text from command
71+
# substitution. Simple checks may be non-portable to other OSes. Fortunately,
72+
# tools that treat input as binary data are exempt (even cat, but "-v" is
73+
# non-portable, and unreliable in general because lines can end in "^M").
74+
# This may be doable without od, by using tr more heavily, but it could be
75+
# hard to avoid false positives with unexpected characters or \r without \n.
76+
77+
head -n 1 -- "$1" | # Get the longest prefix with no non-trailing \n byte.
78+
od -An -ta | # Represent all bytes symbolically, without addresses.
79+
tr -sd '\n' ' ' | # Scrunch into one line, so "cr nl" appears as such.
80+
grep -q 'cr nl$' # Check if the result signifies a \r\n line ending.
81+
}
82+
83+
function make_header () {
84+
local input_file endline
85+
86+
input_file="$1"
87+
endline="$2"
88+
89+
# shellcheck disable=SC2016 # The backticks are intentionally literal.
90+
printf '// DO NOT EDIT - this is a copy of %s. Run `just copy-packetline` to update it.%s%s' \
91+
"$input_file" "$endline" "$endline"
92+
}
93+
94+
function copy_with_header () {
95+
local input_file output_file endline
96+
97+
input_file="$1"
98+
output_file="$2"
99+
100+
if first_line_ends_crlf "$input_file"; then
101+
endline=$'\r\n'
102+
else
103+
endline=$'\n'
104+
fi
105+
106+
make_header "$input_file" "$endline" | cat -- - "$input_file" >"$output_file"
107+
}
108+
109+
function generate_one () {
110+
local input_file output_file
111+
112+
input_file="$1"
113+
output_file="$output_dir${input_file#"$input_dir"}"
114+
115+
if test -d "$input_file"; then
116+
mkdir -p -- "$output_file"
117+
elif test -L "$input_file"; then
118+
# Cover this case separately, for more useful error messages.
119+
fail "input file is symbolic link: $input_file"
120+
elif ! test -f "$input_file"; then
121+
# This covers less common kinds of files we can't or shouldn't process.
122+
fail "input file neither regular file nor directory: $input_file"
123+
elif [[ "$input_file" =~ \.rs$ ]]; then
124+
copy_with_header "$input_file" "$output_file"
125+
else
126+
fail "input file not named as Rust source code: $input_file"
127+
fi
128+
}
129+
130+
function generate_all () {
131+
local input_file
132+
133+
if ! test -d "$input_dir"; then
134+
fail "no input directory: $input_dir"
135+
fi
136+
if ! test -d "$output_parent_dir"; then
137+
fail "no output parent directory: $output_parent_dir"
138+
fi
139+
check_output_dir
140+
141+
rm -rf -- "$output_dir" # It may be a directory, symlink, or regular file.
142+
if test -e "$output_dir"; then
143+
fail 'unable to remove output location'
144+
fi
145+
146+
find "$input_dir" -print0 | while IFS= read -r -d '' input_file; do
147+
generate_one "$input_file"
148+
done
149+
}
150+
151+
chdir_toplevel
152+
generate_all

gix-packetline-blocking/src

Lines changed: 0 additions & 1 deletion
This file was deleted.

gix-packetline-blocking/src/decode.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// DO NOT EDIT - this is a copy of gix-packetline/src/decode.rs. Run `just copy-packetline` to update it.
2+
3+
use bstr::BString;
4+
5+
use crate::{PacketLineRef, DELIMITER_LINE, FLUSH_LINE, MAX_DATA_LEN, MAX_LINE_LEN, RESPONSE_END_LINE, U16_HEX_BYTES};
6+
7+
/// The error used in the [`decode`][mod@crate::decode] module
8+
#[derive(Debug, thiserror::Error)]
9+
#[allow(missing_docs)]
10+
pub enum Error {
11+
#[error("Failed to decode the first four hex bytes indicating the line length: {err}")]
12+
HexDecode { err: String },
13+
#[error("The data received claims to be larger than the maximum allowed size: got {length_in_bytes}, exceeds {MAX_DATA_LEN}")]
14+
DataLengthLimitExceeded { length_in_bytes: usize },
15+
#[error("Received an invalid empty line")]
16+
DataIsEmpty,
17+
#[error("Received an invalid line of length 3")]
18+
InvalidLineLength,
19+
#[error("{data:?} - consumed {bytes_consumed} bytes")]
20+
Line { data: BString, bytes_consumed: usize },
21+
#[error("Needing {bytes_needed} additional bytes to decode the line successfully")]
22+
NotEnoughData { bytes_needed: usize },
23+
}
24+
25+
///
26+
#[allow(clippy::empty_docs)]
27+
pub mod band {
28+
/// The error used in [`PacketLineRef::decode_band()`][super::PacketLineRef::decode_band()].
29+
#[derive(Debug, thiserror::Error)]
30+
#[allow(missing_docs)]
31+
pub enum Error {
32+
#[error("attempt to decode a non-side channel line or input was malformed: {band_id}")]
33+
InvalidSideBand { band_id: u8 },
34+
#[error("attempt to decode a non-data line into a side-channel band")]
35+
NonDataLine,
36+
}
37+
}
38+
39+
/// A utility return type to support incremental parsing of packet lines.
40+
#[derive(Debug, Clone)]
41+
pub enum Stream<'a> {
42+
/// Indicate a single packet line was parsed completely
43+
Complete {
44+
/// The parsed packet line
45+
line: PacketLineRef<'a>,
46+
/// The amount of bytes consumed from input
47+
bytes_consumed: usize,
48+
},
49+
/// A packet line could not yet be parsed due to missing bytes
50+
Incomplete {
51+
/// The amount of additional bytes needed for the parsing to complete
52+
bytes_needed: usize,
53+
},
54+
}
55+
56+
/// The result of [`hex_prefix()`] indicating either a special packet line or the amount of wanted bytes
57+
pub enum PacketLineOrWantedSize<'a> {
58+
/// The special kind of packet line decoded from the hex prefix. It never contains actual data.
59+
Line(PacketLineRef<'a>),
60+
/// The amount of bytes indicated by the hex prefix of the packet line.
61+
Wanted(u16),
62+
}
63+
64+
/// Decode the `four_bytes` packet line prefix provided in hexadecimal form and check it for validity.
65+
pub fn hex_prefix(four_bytes: &[u8]) -> Result<PacketLineOrWantedSize<'_>, Error> {
66+
debug_assert_eq!(four_bytes.len(), 4, "need four hex bytes");
67+
for (line_bytes, line_type) in &[
68+
(FLUSH_LINE, PacketLineRef::Flush),
69+
(DELIMITER_LINE, PacketLineRef::Delimiter),
70+
(RESPONSE_END_LINE, PacketLineRef::ResponseEnd),
71+
] {
72+
if four_bytes == *line_bytes {
73+
return Ok(PacketLineOrWantedSize::Line(*line_type));
74+
}
75+
}
76+
77+
let mut buf = [0u8; U16_HEX_BYTES / 2];
78+
faster_hex::hex_decode(four_bytes, &mut buf).map_err(|err| Error::HexDecode { err: err.to_string() })?;
79+
let wanted_bytes = u16::from_be_bytes(buf);
80+
81+
if wanted_bytes == 3 {
82+
return Err(Error::InvalidLineLength);
83+
}
84+
if wanted_bytes == 4 {
85+
return Err(Error::DataIsEmpty);
86+
}
87+
debug_assert!(
88+
wanted_bytes as usize > U16_HEX_BYTES,
89+
"by now there should be more wanted bytes than prefix bytes"
90+
);
91+
Ok(PacketLineOrWantedSize::Wanted(wanted_bytes - U16_HEX_BYTES as u16))
92+
}
93+
94+
/// Obtain a `PacketLine` from `data` after assuring `data` is small enough to fit.
95+
pub fn to_data_line(data: &[u8]) -> Result<PacketLineRef<'_>, Error> {
96+
if data.len() > MAX_LINE_LEN {
97+
return Err(Error::DataLengthLimitExceeded {
98+
length_in_bytes: data.len(),
99+
});
100+
}
101+
102+
Ok(PacketLineRef::Data(data))
103+
}
104+
105+
/// Decode `data` as packet line while reporting whether the data is complete or not using a [`Stream`].
106+
pub fn streaming(data: &[u8]) -> Result<Stream<'_>, Error> {
107+
let data_len = data.len();
108+
if data_len < U16_HEX_BYTES {
109+
return Ok(Stream::Incomplete {
110+
bytes_needed: U16_HEX_BYTES - data_len,
111+
});
112+
}
113+
let wanted_bytes = match hex_prefix(&data[..U16_HEX_BYTES])? {
114+
PacketLineOrWantedSize::Wanted(s) => s as usize,
115+
PacketLineOrWantedSize::Line(line) => {
116+
return Ok(Stream::Complete {
117+
line,
118+
bytes_consumed: 4,
119+
})
120+
}
121+
} + U16_HEX_BYTES;
122+
if wanted_bytes > MAX_LINE_LEN {
123+
return Err(Error::DataLengthLimitExceeded {
124+
length_in_bytes: wanted_bytes,
125+
});
126+
}
127+
if data_len < wanted_bytes {
128+
return Ok(Stream::Incomplete {
129+
bytes_needed: wanted_bytes - data_len,
130+
});
131+
}
132+
133+
Ok(Stream::Complete {
134+
line: to_data_line(&data[U16_HEX_BYTES..wanted_bytes])?,
135+
bytes_consumed: wanted_bytes,
136+
})
137+
}
138+
139+
/// Decode an entire packet line from data or fail.
140+
///
141+
/// Note that failure also happens if there is not enough data to parse a complete packet line, as opposed to [`streaming()`] decoding
142+
/// succeeds in that case, stating how much more bytes are required.
143+
pub fn all_at_once(data: &[u8]) -> Result<PacketLineRef<'_>, Error> {
144+
match streaming(data)? {
145+
Stream::Complete { line, .. } => Ok(line),
146+
Stream::Incomplete { bytes_needed } => Err(Error::NotEnoughData { bytes_needed }),
147+
}
148+
}

0 commit comments

Comments
 (0)