takane
Validators for ArtifactDB file formats
Loading...
Searching...
No Matches
sequence_string_set.hpp
Go to the documentation of this file.
1#ifndef TAKANE_SEQUENCE_STRING_SET_HPP
2#define TAKANE_SEQUENCE_STRING_SET_HPP
3
4#include "byteme/byteme.hpp"
5#include "ritsuko/ritsuko.hpp"
6#include "sanisizer/sanisizer.hpp"
7
8#include <array>
9#include <algorithm>
10#include <stdexcept>
11#include <limits>
12#include <cstddef>
13#include <filesystem>
14#include <exception>
15
16#include "utils_other.hpp"
17
23namespace takane {
24
28inline constexpr int char2int(char val) {
29 return static_cast<int>(val) - static_cast<int>(std::numeric_limits<char>::min());
30}
31
32typedef std::array<bool, char2int(std::numeric_limits<char>::max())> AllowedCharacterMap;
33
34template<bool has_quality_>
35std::size_t parse_sequence_contents(const std::filesystem::path& path, AllowedCharacterMap allowed, char lowest_quality, bool parallel) {
36 auto gzreader = open_reader<byteme::GzipFileReader>(path, byteme::GzipFileReaderOptions());
37 auto pb = wrap_reader_for_bytes<char>(std::move(gzreader), parallel);
38
39 std::size_t nseq = 0;
40 unsigned long long line_count = 1;
41 auto advance_and_check = [&]() -> char {
42 if (!pb->advance()) {
43 throw std::runtime_error("premature end of the file at line " + ritsuko::itoa(line_count));
44 }
45 return pb->get();
46 };
47
48 while (pb->valid()) {
49 // Chomp through any empty lines.
50 while (pb->get() == '\n') {
51 ++line_count;
52 if (!pb->advance()) {
53 break;
54 }
55 }
56 if (!pb->valid()) {
57 break;
58 }
59
60 // Processing the name.
61 {
62 if constexpr(!has_quality_) {
63 if (pb->get() != '>') {
64 throw std::runtime_error("sequence name should start with '>' at line " + ritsuko::itoa(line_count));
65 }
66 } else {
67 if (pb->get() != '@') {
68 throw std::runtime_error("sequence name should start with '@' at line " + ritsuko::itoa(line_count));
69 }
70 }
71
72 const std::size_t mult_limit = nseq / 10;
73 const std::size_t add_limit = nseq % 10;
74 std::size_t proposed = 0;
75 bool empty = true;
76 while (true) {
77 char val = advance_and_check();
78 if (val == '\n') {
79 break;
80 }
81
82 empty = false;
83 if (val < '0' || val > '9') {
84 throw std::runtime_error("sequence name should be a non-negative integer at line " + ritsuko::itoa(line_count));
85 }
86
87 const auto delta = val - '0';
88 if (proposed < mult_limit || (proposed == mult_limit && sanisizer::is_less_than_or_equal(delta, add_limit))) {
89 proposed *= 10;
90 proposed += delta;
91 } else {
92 throw std::runtime_error("sequence name is out of bounds at line " + ritsuko::itoa(line_count));
93 }
94 }
95 if (empty || proposed != nseq) {
96 throw std::runtime_error("sequence name should be its index at line " + ritsuko::itoa(line_count));
97 }
98 ++line_count;
99 }
100
101 if constexpr(!has_quality_) {
102 // Processing the sequence itself until we get to a '>' or we run out of bytes.
103 char val = advance_and_check();
104 while (true) {
105 if (val == '\n') {
106 ++line_count;
107 if (!pb->advance()) {
108 break;
109 }
110 val = pb->get();
111 if (val == '>') {
112 break;
113 }
114 } else if (allowed[char2int(val)]) {
115 val = advance_and_check();
116 } else {
117 throw std::runtime_error("forbidden character '" + std::string(1, val) + "' in sequence at line " + ritsuko::itoa(line_count));
118 }
119 }
120
121 } else {
122 // Processing the sequence itself until we get to a '+'.
123 std::size_t seq_length = 0;
124 {
125 char val = advance_and_check();
126 while (true) {
127 if (val == '\n') {
128 ++line_count;
129 val = advance_and_check();
130 if (val == '+') {
131 break;
132 }
133 } else if (allowed[char2int(val)]) {
134 seq_length = sanisizer::sum<std::size_t>(seq_length, 1); // Protect against overflow as this is important for the quality string checks.
135 val = advance_and_check();
136 } else {
137 throw std::runtime_error("forbidden character '" + std::string(1, val) + "' in sequence at line " + ritsuko::itoa(line_count));
138 }
139 }
140 }
141
142 // Next should be a single line; starting with '+' is implicit from above.
143 while (true) {
144 const char val = advance_and_check();
145 if (val == '\n') {
146 ++line_count;
147 break;
148 }
149 }
150
151 // Processing the qualities. Note that we can't check for '@' as a
152 // delimitor, as this can be a valid score, so instead we check at each
153 // newline whether we've reached the specified length, and quit if so.
154 std::size_t qual_length = 0;
155 while (true) {
156 const char val = advance_and_check();
157 if (val == '\n') {
158 ++line_count;
159 if (qual_length >= seq_length) {
160 break;
161 }
162 } else if (val >= lowest_quality && val <= '~') {
163 qual_length = sanisizer::sum<std::size_t>(qual_length, 1);
164 } else {
165 throw std::runtime_error("out-of-range quality score '" + std::string(1, val) + "' detected at line " + ritsuko::itoa(line_count));
166 }
167 }
168
169 if (qual_length != seq_length) {
170 throw std::runtime_error("unequal lengths for quality and sequence strings at line " + ritsuko::itoa(line_count));
171 }
172 }
173
174 nseq = sanisizer::sum<std::size_t>(nseq, 1);
175 }
176
177 return nseq;
178}
179
180inline std::size_t parse_sequence_names(const std::filesystem::path& path, bool parallel) {
181 auto gzreader = open_reader<byteme::GzipFileReader>(path, byteme::GzipFileReaderOptions());
182 auto pb = wrap_reader_for_bytes<char>(std::move(gzreader), parallel);
183
184 std::size_t nseq = 0;
185 unsigned long long line_count = 1;
186 auto advance_and_check = [&]() -> char {
187 if (!pb->advance()) {
188 throw std::runtime_error("premature end of the file at line " + ritsuko::itoa(line_count));
189 }
190 return pb->get();
191 };
192
193 while (pb->valid()) {
194 char val = pb->get();
195 if (val != '"') {
196 throw std::runtime_error("name should start with a quote at line " + ritsuko::itoa(line_count));
197 }
198
199 while (true) {
200 val = advance_and_check();
201 if (val == '"') {
202 val = advance_and_check();
203 if (val == '\n') {
204 ++nseq;
205 ++line_count;
206 pb->advance();
207 break;
208 } else if (val != '"') {
209 throw std::runtime_error("characters present after end quote at line " + ritsuko::itoa(line_count));
210 }
211 } else if (val == '\n') {
212 ++line_count;
213 }
214 }
215 }
216
217 return nseq;
218}
228inline void validate_sequence_string_set(const std::filesystem::path& path, const ObjectMetadata& metadata, const Options& options) {
229 const std::string type_name = "sequence_string_set"; // use a separate variable to avoid dangling reference warnings from GCC.
230
231 std::size_t expected_nseq = 0;
232 AllowedCharacterMap allowed{};
233 bool has_qualities = false;
234 char lowest_quality = 0;
235
236 try {
237 const auto& obj = extract_json_object(metadata.other, type_name);
238 try {
239 const auto& vstring = extract_json_version_string(obj);
240 auto version = ritsuko::parse_version_string(vstring.c_str(), vstring.size(), /* skip_patch = */ true);
241 if (version.major != 1) {
242 throw std::runtime_error("unsupported version string '" + vstring + "'");
243 }
244 } catch (...) {
245 std::throw_with_nested(std::runtime_error("failed to read 'version'"));
246 }
247
248 try {
249 auto lIt = obj.find("length");
250 if (lIt == obj.end()) {
251 throw std::runtime_error("no such property");
252 }
253
254 const auto& val = lIt->second;
255 if (val->type() != millijson::NUMBER) {
256 throw std::runtime_error("property should be a JSON number");
257 }
258
259 auto num = reinterpret_cast<const millijson::Number*>(val.get())->value();
260 if (num < 0 || std::floor(num) != num) {
261 throw std::runtime_error("property should be a non-negative integer");
262 }
263
264 expected_nseq = sanisizer::from_float<std::size_t>(num);
265 } catch (...) {
266 std::throw_with_nested(std::runtime_error("failed to read 'length'"));
267 }
268
269 try {
270 const std::string stype_name = "sequence_type"; // avoid dangling reference again.
271 const auto& stype = extract_json_string(obj, stype_name);
272
273 std::string allowable;
274 if (stype == "DNA" || stype == "RNA") {
275 allowable = "ACGRYSWKMBDHVN";
276 if (stype == "DNA") {
277 allowable += "T";
278 } else {
279 allowable += "U";
280 }
281 } else if (stype == "AA") {
282 allowable = "ACDEFGHIKLMNPQRSTVWY";
283 } else if (stype == "custom") {
284 std::fill(allowed.begin() + char2int('!'), allowed.begin() + char2int('~') + 1, true);
285 } else {
286 throw std::runtime_error("unknown sequence type '" + stype + "'");
287 }
288
289 const auto lower_shift = 'A' - 'a';
290 for (auto a : allowable) {
291 allowed[char2int(a)] = true;
292 allowed[char2int(a) - lower_shift] = true; // don't trust std::tolower(), as this is locale dependent.
293 }
294 allowed[char2int('.')] = true;
295 allowed[char2int('-')] = true;
296
297 } catch (...) {
298 std::throw_with_nested(std::runtime_error("failed to extract 'sequence_type'"));
299 }
300
301 bool offset_failed = false;
302 try {
303 auto xIt = obj.find("quality_type");
304 if (xIt != obj.end()) {
305 const auto& val = xIt->second;
306 if (val->type() != millijson::STRING) {
307 throw std::runtime_error("expected a JSON string");
308 }
309
310 const auto& qtype = reinterpret_cast<const millijson::String*>(val.get())->value();
311
312 if (qtype == "phred") {
313 has_qualities = true;
314 offset_failed = true;
315 try {
316 auto oIt = obj.find("quality_offset");
317 if (oIt == obj.end()) {
318 throw std::runtime_error("no such property");
319 }
320
321 const auto& val = oIt->second;
322 if (val->type() != millijson::NUMBER) {
323 throw std::runtime_error("property should be a JSON number");
324 }
325
326 double offset = reinterpret_cast<const millijson::Number*>(val.get())->value();
327 if (offset == 33) {
328 lowest_quality = '!';
329 } else if (offset == 64) {
330 lowest_quality = '@';
331 } else {
332 throw std::runtime_error("property should be either 33 or 64");
333 }
334 } catch (...) {
335 std::throw_with_nested(std::runtime_error("failed to extract 'quality_offset'"));
336 }
337
338 } else if (qtype == "solexa") {
339 has_qualities = true;
340 lowest_quality = ';';
341
342 } else if (qtype != "none") {
343 throw std::runtime_error("unknown quality type '" + qtype + "'");
344 }
345 }
346 } catch (...) {
347 if (offset_failed) {
348 throw;
349 } else {
350 std::throw_with_nested(std::runtime_error("failed to extract 'quality_type'"));
351 }
352 }
353 } catch (...) {
354 std::throw_with_nested(std::runtime_error("failed to read '" + type_name + "' in the object metadata"));
355 }
356
357 std::string fname;
358 try {
359 std::size_t nseq = 0;
360 if (has_qualities) {
361 fname = "sequences.fastq.gz";
362 nseq = parse_sequence_contents<true>(path / fname, allowed, lowest_quality, options.parallel_reads);
363 } else {
364 fname = "sequences.fasta.gz";
365 nseq = parse_sequence_contents<false>(path / fname, allowed, lowest_quality, options.parallel_reads);
366 }
367 if (nseq != expected_nseq) {
368 throw std::runtime_error("number of sequences is different from the expected length (" + ritsuko::itoa(nseq) + " versus " + ritsuko::itoa(expected_nseq) + ")");
369 }
370 } catch (...) {
371 std::throw_with_nested(std::runtime_error("failed to validate '" + fname + "'"));
372 }
373
374 auto npath = path / "names.txt.gz";
375 if (std::filesystem::exists(npath)) {
376 try {
377 std::size_t nnames = parse_sequence_names(npath, options.parallel_reads);
378 if (nnames != expected_nseq) {
379 throw std::runtime_error("number of names is different from the number of sequences (" + ritsuko::itoa(nnames) + " versus " + ritsuko::itoa(expected_nseq) + ")");
380 }
381 } catch (...) {
382 std::throw_with_nested(std::runtime_error("failed to validate '" + npath.filename().string() + "'"));
383 }
384 }
385
386 validate_mcols(path, "sequence_annotations", expected_nseq, options);
387 validate_metadata(path, "other_annotations", options);
388}
389
396inline std::size_t height_of_sequence_string_set([[maybe_unused]] const std::filesystem::path& path, const ObjectMetadata& metadata, [[maybe_unused]] const Options& options) {
397 const std::string type_name = "sequence_string_set"; // use a separate variable to avoid dangling reference warnings from GCC.
398 const auto& obj = extract_json_object(metadata.other, type_name);
399 auto lIt = obj.find("length");
400 const auto& val = lIt->second;
401 return sanisizer::from_float<std::size_t>(reinterpret_cast<const millijson::Number*>(val.get())->value());
402}
403
404}
405
406#endif
takane validation functions.
Definition _derived_from.hpp:15
void validate_sequence_string_set(const std::filesystem::path &path, const ObjectMetadata &metadata, const Options &options)
Definition sequence_string_set.hpp:228
std::size_t height_of_sequence_string_set(const std::filesystem::path &path, const ObjectMetadata &metadata, const Options &options)
Definition sequence_string_set.hpp:396
Object metadata, including the type and other fields.
Definition utils_public.hpp:29
std::unordered_map< std::string, std::shared_ptr< millijson::Base > > other
Definition utils_public.hpp:38
Validation options.
Definition utils_public.hpp:99
bool parallel_reads
Definition utils_public.hpp:103