txt - leer salto de linea c++
¿Cómo leer una línea de forma segura desde std:: istream? (4)
Quiero leer con seguridad una línea de un std::istream
. La transmisión podría ser cualquier cosa, por ejemplo, una conexión en un servidor web o algo que procese archivos enviados por fuentes desconocidas. Hay muchas respuestas que comienzan a hacer el equivalente moral de este código:
void read(std::istream& in) {
std::string line;
if (std::getline(in, line)) {
// process the line
}
}
Dada la fuente posiblemente dudosa de in
, el uso del código anterior llevaría a una vulnerabilidad: un agente malicioso puede montar un ataque de denegación de servicio contra este código usando una línea enorme. Por lo tanto, me gustaría limitar la longitud de la línea a un valor bastante alto, digamos 4 millones de char
. Aunque se pueden encontrar algunas líneas grandes, no es viable asignar un búfer para cada archivo y usar std::istream::getline()
.
¿Cómo se puede limitar el tamaño máximo de la línea, idealmente sin distorsionar demasiado el código y sin asignar grandes porciones de memoria al frente?
En base a los comentarios y respuestas, parece haber tres enfoques:
- Escriba una versión personalizada de
getline()
posiblemente usando internamente el miembrostd::istream::getline()
para obtener los caracteres reales. - Use un buffer de flujo de filtrado para limitar la cantidad de datos potencialmente recibidos.
- En lugar de leer una
std::string
, use una instanciación de cadenas con un asignador personalizado que limite la cantidad de memoria almacenada en la cadena.
No todas las sugerencias vienen con código. Esta respuesta proporciona un código para todos los enfoques y un poco de discusión de los tres enfoques. Antes de entrar en detalles de implementación, primero vale la pena señalar que hay múltiples opciones de lo que debería suceder si se recibe una entrada excesivamente larga:
- Leer una línea demasiado larga podría resultar en una lectura exitosa de una línea parcial, es decir, la cadena resultante contiene el contenido leído y la secuencia no tiene ningún indicador de error establecido. Hacer eso significa, sin embargo, que no es posible distinguir entre una línea que llega exactamente al límite o que es demasiado larga. Sin embargo, dado que el límite es un tanto arbitrario, probablemente no importe.
- Leer una línea demasiado larga podría considerarse una falla (es decir, establecer
std::ios_base::failbit
y / ostd::ios_base::bad_bit
) y, dado que la lectura falló, generar una cadena vacía. Ceder una cadena vacía, obviamente, evita potencialmente mirar la cadena leída hasta ahora para posiblemente ver lo que está pasando. - Leer una línea demasiado larga podría proporcionar la lectura de línea parcial y también establecer indicadores de error en la transmisión. Esto parece un comportamiento razonable tanto para detectar que hay algo como para proporcionar la entrada para una posible inspección.
Aunque hay varios ejemplos de código que implementan una versión limitada de getline()
, ¡aquí hay otro! Creo que es más simple (aunque posiblemente más lento, el rendimiento se puede tratar cuando es necesario) que también conserva la interfaz std::getline()
: utiliza el width()
la width()
para comunicar un límite (tal vez teniendo en cuenta el width()
una extensión razonable para std::getline()
):
template <typename cT, typename Traits, typename Alloc>
std::basic_istream<cT, Traits>&
safe_getline(std::basic_istream<cT, Traits>& in,
std::basic_string<cT, Traits, Alloc>& value,
cT delim)
{
typedef std::basic_string<cT, Traits, Alloc> string_type;
typedef typename string_type::size_type size_type;
typename std::basic_istream<cT, Traits>::sentry cerberos(in);
if (cerberos) {
value.clear();
size_type width(in.width(0));
if (width == 0) {
width = std::numeric_limits<size_type>::max();
}
std::istreambuf_iterator<char> it(in), end;
for (; value.size() != width && it != end; ++it) {
if (!Traits::eq(delim, *it)) {
value.push_back(*it);
}
else {
++it;
break;
}
}
if (value.size() == width) {
in.setstate(std::ios_base::failbit);
}
}
return in;
}
Esta versión de getline()
se usa igual que std::getline()
pero cuando parece razonable limitar la cantidad de datos leídos, se establece el width()
, por ejemplo:
std::string line;
if (safe_getline(in >> std::setw(max_characters), line)) {
// do something with the input
}
Otro enfoque es simplemente usar un buffer de flujo de filtrado para limitar la cantidad de entrada: el filtro solo contará el número de caracteres procesados y limitará la cantidad a un número adecuado de caracteres. Este enfoque es realmente más fácil de aplicar a una secuencia completa que a una línea individual: al procesar solo una línea, el filtro no puede obtener almacenamientos intermedios llenos de caracteres de la transmisión subyacente porque no hay una manera confiable de devolver los caracteres. La implementación de una versión sin búferes sigue siendo simple, pero probablemente no sea particularmente eficiente:
template <typename cT, typename Traits = std::char_traits<char> >
class basic_limitbuf
: std::basic_streambuf <cT, Traits> {
public:
typedef Traits traits_type;
typedef typename Traits::int_type int_type;
private:
std::streamsize size;
std::streamsize max;
std::basic_istream<cT, Traits>* stream;
std::basic_streambuf<cT, Traits>* sbuf;
int_type underflow() {
if (this->size < this->max) {
return this->sbuf->sgetc();
}
else {
this->stream->setstate(std::ios_base::failbit);
return traits_type::eof();
}
}
int_type uflow() {
if (this->size < this->max) {
++this->size;
return this->sbuf->sbumpc();
}
else {
this->stream->setstate(std::ios_base::failbit);
return traits_type::eof();
}
}
public:
basic_limitbuf(std::streamsize max,
std::basic_istream<cT, Traits>& stream)
: size()
, max(max)
, stream(&stream)
, sbuf(this->stream->rdbuf(this)) {
}
~basic_limitbuf() {
std::ios_base::iostate state = this->stream->rdstate();
this->stream->rdbuf(this->sbuf);
this->stream->setstate(state);
}
};
Este buffer de flujo ya está configurado para insertarse en la construcción y eliminarse luego de la destrucción. Es decir, puede usarse simplemente así:
std::string line;
basic_limitbuf<char> sbuf(max_characters, in);
if (std::getline(in, line)) {
// do something with the input
}
También sería fácil agregar un manipulador configurando el límite. Una ventaja de este enfoque es que no es necesario tocar ninguno de los códigos de lectura si el tamaño total de la transmisión puede ser limitado: el filtro podría configurarse inmediatamente después de crear la transmisión. Cuando no hay necesidad de retroceder el filtro, el filtro también podría usar un buffer que mejoraría en gran medida el rendimiento.
El tercer enfoque sugerido es usar std::basic_string
con un asignador personalizado. Hay dos aspectos que son un poco incómodos con el enfoque del asignador:
- La cadena que se lee en realidad tiene un tipo que no es inmediatamente convertible a
std::string
(aunque tampoco es difícil hacer la conversión). - El tamaño máximo de la matriz puede limitarse fácilmente, pero la cadena tendrá un tamaño más o menos aleatorio más pequeño que eso: cuando la secuencia falla, se asigna la asignación de una excepción y no se intenta hacer crecer la cadena por un tamaño menor.
Aquí está el código necesario para un asignador que limita el tamaño asignado:
template <typename T>
struct limit_alloc
{
private:
std::size_t max_;
public:
typedef T value_type;
limit_alloc(std::size_t max): max_(max) {}
template <typename S>
limit_alloc(limit_alloc<S> const& other): max_(other.max()) {}
std::size_t max() const { return this->max_; }
T* allocate(std::size_t size) {
return size <= max_
? static_cast<T*>(operator new[](size))
: throw std::bad_alloc();
}
void deallocate(void* ptr, std::size_t) {
return operator delete[](ptr);
}
};
template <typename T0, typename T1>
bool operator== (limit_alloc<T0> const& a0, limit_alloc<T1> const& a1) {
return a0.max() == a1.max();
}
template <typename T0, typename T1>
bool operator!= (limit_alloc<T0> const& a0, limit_alloc<T1> const& a1) {
return !(a0 == a1);
}
El asignador se usaría algo como esto (el código compila OK con una versión reciente de clang pero no con gcc ):
std::basic_string<char, std::char_traits<char>, limit_alloc<char> >
tmp(limit_alloc<char>(max_chars));
if (std::getline(in, tmp)) {
std::string(tmp.begin(), tmp.end());
// do something with the input
}
En resumen, hay múltiples enfoques, cada uno con su propio pequeño inconveniente pero cada uno razonablemente viable para el objetivo declarado de limitar los ataques de denegación de servicio basados en líneas demasiado largas:
- Usar una versión personalizada de
getline()
significa que el código de lectura debe ser cambiado. - El uso de un búfer de transmisión personalizado es lento a menos que el tamaño de la secuencia completa se pueda limitar.
- El uso de un asignador personalizado ofrece menos control y requiere algunos cambios en el código de lectura.
Podría escribir su propia versión de std::getline
con un número máximo de parámetros de lectura de caracteres, algo llamado getline_n
o algo así.
#include <string>
#include <iostream>
template<typename CharT, typename Traits, typename Alloc>
auto getline_n(std::basic_istream<CharT, Traits>& in, std::basic_string<CharT, Traits, Alloc>& str, std::streamsize n) -> decltype(in) {
std::ios_base::iostate state = std::ios_base::goodbit;
bool extracted = false;
const typename std::basic_istream<CharT, Traits>::sentry s(in, true);
if(s) {
try {
str.erase();
typename Traits::int_type ch = in.rdbuf()->sgetc();
for(; ; ch = in.rdbuf()->snextc()) {
if(Traits::eq_int_type(ch, Traits::eof())) {
// eof spotted, quit
state |= std::ios_base::eofbit;
break;
}
else if(str.size() == n) {
// maximum number of characters met, quit
extracted = true;
in.rdbuf()->sbumpc();
break;
}
else if(str.max_size() <= str.size()) {
// string too big
state |= std::ios_base::failbit;
break;
}
else {
// character valid
str += Traits::to_char_type(ch);
extracted = true;
}
}
}
catch(...) {
in.setstate(std::ios_base::badbit);
}
}
if(!extracted) {
state |= std::ios_base::failbit;
}
in.setstate(state);
return in;
}
int main() {
std::string s;
getline_n(std::cin, s, 10); // maximum of 10 characters
std::cout << s << ''/n'';
}
Podría ser exagerado sin embargo.
Reemplazar std::getline creando un contenedor alrededor de std::istream::getline :
std::istream& my::getline( std::istream& is, std::streamsize n, std::string& str, char delim )
{
try
{
str.resize(n);
is.getline(&str[0],n,delim);
str.resize(is.gcount());
return is;
}
catch(...) { str.resize(0); throw; }
}
Si desea evitar asignaciones excesivas de memoria temporal, puede usar un bucle que aumente la asignación según sea necesario (probablemente duplicando el tamaño de cada pase). No olvide que las excepciones pueden o no estar habilitadas en el objeto istream.
Aquí hay una versión con la estrategia de asignación más eficiente:
std::istream& my::getline( std::istream& is, std::streamsize n, std::string& str, char delim )
{
std::streamsize base=0;
do {
try
{
is.clear();
std::streamsize chunk=std::min(n-base,std::max(static_cast<std::streamsize>(2),base));
if ( chunk == 0 ) break;
str.resize(base+chunk);
is.getline(&str[base],chunk,delim);
}
catch( std::ios_base::failure ) { if ( !is.gcount () ) str.resize(0), throw; }
base += is.gcount();
} while ( is.fail() && is.gcount() );
str.resize(base);
return is;
}
Ya existe una función getline
como función miembro de istream
, solo tiene que envolverla para la administración del búfer.
#include <assert.h>
#include <istream>
#include <stddef.h> // ptrdiff_t
#include <string> // std::string, std::char_traits
typedef ptrdiff_t Size;
namespace my {
using std::istream;
using std::string;
using std::char_traits;
istream& getline(
istream& stream, string& s, Size const buf_size, char const delimiter = ''/n''
)
{
s.resize( buf_size ); assert( s.size() > 1 );
stream.getline( &s[0], buf_size, delimiter );
if( !stream.fail() )
{
Size const n = char_traits<char>::length( &s[0] );
s.resize( n ); // Downsizing.
}
return stream;
}
} // namespace my