1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
use crateopen_regular_file;
use crate::;
use ;
use Path;
/// Inspects a 1-based line of a regular file.
///
/// Returns `Some` with its raw byte length, including any terminating LF, or
/// `None` beyond EOF. CR is ordinary content; a trailing LF does not create an
/// additional empty line. Invalid UTF-8 is accepted.
///
/// Reopens the path and requires [file stability](crate#file-stability).
///
/// # Errors
///
/// Returns [`Error::InvalidLineNumber`] for line zero, [`Error::Io`] if
/// metadata lookup, opening, or reading fails, or [`Error::NotRegularFile`]
/// for a non-regular input.
///
/// # Examples
///
/// See [`fingerprint_line_prefix`] for an example inspecting and hashing a line.
/// Hashes only the first `byte_count` raw bytes of a 1-based line.
///
/// Zero bytes or an absent line hashes the empty sequence. Requests beyond
/// the line include its entire content, including LF if present, without
/// entering the next line or adding an EOF marker. Invalid UTF-8 is accepted.
/// The path must identify a readable regular file even when `byte_count` is
/// zero.
///
/// Reopens the path and requires [file stability](crate#file-stability).
///
/// # Errors
///
/// Returns [`Error::InvalidLineNumber`] for line zero, [`Error::Io`] if
/// metadata lookup, opening, or reading fails, or [`Error::NotRegularFile`]
/// for a non-regular input.
///
/// # Examples
///
/// A line-local prefix excludes preceding lines and may end inside a UTF-8
/// code point. Longer requests stop at the selected line's LF.
///
/// ```
/// use paircomp_core::{fingerprint_line_prefix, fingerprint_through_line, inspect_line};
/// use std::fs;
///
/// let directory = std::env::temp_dir()
/// .join(format!("paircomp-prefix-example-{}", std::process::id()));
/// fs::create_dir(&directory)?;
/// let path = directory.join("sample.txt");
/// fs::write(&path, "header\ncafé\nnext\n")?;
///
/// assert_eq!(inspect_line(&path, 2)?.map(|line| line.byte_len), Some(6));
/// let prefix = fingerprint_line_prefix(&path, 2, 4)?;
/// assert_eq!(prefix.as_bytes(), blake3::hash(b"caf\xc3").as_bytes());
/// let whole_line = fingerprint_line_prefix(&path, 2, 100)?;
/// assert_eq!(whole_line.as_bytes(), blake3::hash("café\n".as_bytes()).as_bytes());
///
/// // A file prefix also includes every preceding line.
/// let file_prefix = fingerprint_through_line(&path, 2)?;
/// assert_eq!(file_prefix.as_bytes(), blake3::hash("header\ncafé\n".as_bytes()).as_bytes());
///
/// // Zero bytes and an absent line both hash the empty sequence.
/// assert_eq!(fingerprint_line_prefix(&path, 2, 0)?, fingerprint_line_prefix(&path, 4, 100)?);
/// fs::remove_dir_all(&directory)?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
/// Maps a 1-based byte position to a 1-based Unicode code-point position.
///
/// Every byte of a multibyte character maps to the same character. Immediately
/// after an existing line, returns the next character position. CR and LF each
/// count as a code point; these positions are not visual editor columns.
///
/// Validates the entire selected line, returning `None` if any part is invalid
/// UTF-8, even after the requested byte. An absent line permits byte position
/// 1 and returns `None`. Other lines' encodings do not affect the result.
///
/// Reopens the path and requires [file stability](crate#file-stability).
///
/// # Errors
///
/// Returns [`Error::InvalidLineNumber`] for line zero and
/// [`Error::InvalidBytePosition`] for byte zero or a position more than one
/// past the line. Coordinate validation also applies to invalid UTF-8 lines.
/// Returns [`Error::Io`] if metadata lookup, opening, or reading fails,
/// [`Error::NotRegularFile`] for a non-regular input, or [`Error::FileTooLarge`]
/// if the next character position cannot fit in `u64`.
///
/// # Examples
///
/// Both bytes of `é` map to character 4. The terminating LF counts as a
/// separate code point, and the position immediately after it is also valid.
///
/// ```
/// use paircomp_core::utf8_character_position;
/// use std::fs;
///
/// let directory = std::env::temp_dir()
/// .join(format!("paircomp-utf8-example-{}", std::process::id()));
/// fs::create_dir(&directory)?;
/// let path = directory.join("sample.txt");
/// fs::write(&path, b"caf\xc3\xa9\nvalid prefix\xff\n")?;
///
/// assert_eq!(utf8_character_position(&path, 1, 4)?, Some(4));
/// assert_eq!(utf8_character_position(&path, 1, 5)?, Some(4));
/// assert_eq!(utf8_character_position(&path, 1, 6)?, Some(5)); // LF
/// assert_eq!(utf8_character_position(&path, 1, 7)?, Some(6)); // After LF
///
/// // Invalid UTF-8 later in line 2 suppresses even its first position.
/// assert_eq!(utf8_character_position(&path, 2, 1)?, None);
/// // A trailing LF does not create a third line.
/// assert_eq!(utf8_character_position(&path, 3, 1)?, None);
/// fs::remove_dir_all(&directory)?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
/// Maps a byte position using a reader positioned at the start of the file.
///
/// The caller must reject byte zero before calling this helper.
/// Visits bounded chunks from a single line, without retaining its contents.
///
/// The reader must start at the beginning of the file. Visits at most
/// `byte_limit` bytes, including the terminating LF if reached, and returns the
/// number visited. An absent line or a zero limit visits nothing and returns
/// zero. Line zero is invalid even when the limit is zero.