2013-12-17 2 views
50

Я хочу, чтобы безопасно читать строки от std::istream. Поток может быть любым, например, соединением на веб-сервере или с каким-либо файлом обработки, представленным неизвестными источниками. Есть много ответов, начиная делать моральный эквивалент этого кода:Как безопасно читать строку из std :: istream?

void read(std::istream& in) { 
    std::string line; 
    if (std::getline(in, line)) { 
     // process the line 
    } 
} 

Учитывая, возможно, сомнительный источник in, используя приведенный выше код приведет к уязвимости: злоумышленник агент может установить отказ в обслуживании нападения этот код использует огромную линию. Таким образом, я хотел бы ограничить длину линии некоторым довольно высоким значением, скажем, 4 миллионами char с. Хотя может встречаться несколько больших строк, нецелесообразно выделять буфер для каждого файла и использовать std::istream::getline().

Как можно ограничить максимальный размер линии, в идеале, без искажения кода слишком плохо и без выделения больших блоков памяти спереди?

+8

Как насчет пользовательского распределителя, который будет бросать, если его попросят выделить за пределами вашего порога? Постройте объект 'basic_string' с помощью этого распределителя и зачитайте его. – Praetorian

+0

Может быть, subclassing std :: string и функция 'max_size()', которая выплевывает что-то маленькое? – Collin

+0

@Praetorian: Я думаю, использование распределителя было бы вариантом. К сожалению, он меняет тип 'std :: string'. –

ответ

36

Вы можете написать свою собственную версию std::getline с максимальным количеством параметров чтения символов, что-то вроде getline_n или что-то в этом роде.

#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'; 
} 

Возможно, это будет излишним.

+3

Написание версии 'getline()' может быть вариантом (особенно, поскольку я действительно сделал это в прошлом, внедрив всю библиотеку IOStreams). Я не знаю, почему это не произошло со мной: возможно, я был слишком сосредоточен на двух других решениях (только один из которых упоминался до сих пор). –

+0

+1. Единственное, о чем я сомневаюсь, это вызов «резервировать», поскольку OP говорит о порядке порядка 4 Мбайт в качестве охранника, но может иметь дело только со строками меньшего размера. Возможно, было бы лучше, если бы пользователь выполнил резерв самостоятельно. –

+0

@DaveS Забавно, оригинальная версия, которую я написал, не имела вызова 'reserve', но я добавил ее для хорошей оценки. Если бы это было до меня, я бы тоже этого не сделал. Я полагаю, я просто удалю его. – Rapptz

8

Заменить std::getline пути создания обертки вокруг 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; } 
    } 

Если вы хотите, чтобы избежать чрезмерного распределения временной памяти, можно использовать цикл, который растет распределение по мере необходимости (возможно удвоение размера при каждом проходе) , Не забывайте, что исключения могут или не могут быть включены на объекте istream.

Вот версия с более эффективной стратегии распределения:

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; 
    } 
+0

Как реализовано, оба из них оставят завершающий символ «\ 0» в сгенерированной строке. Это не является нормальным для строки C++, поэтому улучшение должно состоять в том, чтобы выпустить последний символ перед возвратом. Обратите внимание, что изменение размера на основе сканирования строки для «\ 0» может считаться ошибкой, так как «\ 0» может быть допустимым символом внутри строки (это не «C»). Кроме того, я точно не знаю, как это взаимодействует с текстовым режимом Microsoft, где текстовые строки обычно заканчиваются символами * двух *. Если я понимаю документы, в строке будет оставлен «\ r», потому что is.getline() «неформатирован». – nobar

+0

... 'if (! Str.empty()) str.resize (str.size() - 1);' – nobar

+0

'try {my :: getline (is, 4096, s, '\ n'); } catch (std :: ios :: failure const &) {} ' – nobar

16

Существует уже такая getline функция как функция члена istream, вам просто нужно обернуть его для управления буфером.

#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 
+1

' & s [0] 'заставляет меня чувствовать себя неловко – Inverse

+4

@ Inverse: Нечего беспокоиться. Вы можете также чувствовать себя неловко из-за разделения, рассуждая, что он может оказаться делением на ноль. В этом коде соответствующее ограничение на значение (длина строки должно быть> 0) выражается командой 'assert', что обычно является хорошей практикой и делает код более безопасным, чем без него. С 'assert' нужно много работать, чтобы вывести носовые демоны UB, а именно, вызвать функцию с недопустимым аргументом для' buf_size' * и * сделать это с помощью 'NDEBUG', определенного для подавления' assert' , Вот почему вы должны использовать 'assert'. –

+0

О, я имел в виду, что я понимаю, что данные в 'std :: string' не гарантируются непрерывными, это только' .c_str() '. Таким образом, '& v [0]' отлично подходит для 'std :: vector', но не строго для' std :: string' – Inverse

5

На основе комментариев и ответов, кажется, есть три подхода:

  1. Написать собственную версию getline() возможно с помощью std::istream::getline() члена внутри, чтобы получить фактические символы.
  2. Используйте буфер потока фильтрации, чтобы ограничить объем потенциально полученных данных.
  3. Вместо чтения std::string используйте строковое создание экземпляра с помощью специального распределителя, ограничивающего объем памяти, хранящейся в строке.

Не все предложения поставляются с кодом. Этот ответ дает код для всех подходов и немного обсуждения всех трех подходов. Прежде чем вдаваться в подробность реализации это первый стоит отметить, что есть несколько вариантов того, что должно произойти, если чрезмерно длинный вход получен:

  1. Чтение сверхдолгой линии может привести к успешному чтению частичной линии, т.е., результирующая строка содержит прочитанный контент, и в потоке не установлены флаги ошибок. Это означает, однако, что невозможно отличить линию, попадающую точно в лимит или слишком длинную. Так как предел несколько произволен, в любом случае, это, вероятно, не имеет особого значения.
  2. Чтение строки с перекрытием может считаться сбоем (т. Е. Установка std::ios_base::failbit и/или std::ios_base::bad_bit), и, поскольку чтение не выполнено, введите пустую строку. Очевидно, что выход пустой строки предотвращает потенциальный просмотр строки, прочитанной до сих пор, чтобы увидеть, что происходит.
  3. Чтение строки с перекрытием может обеспечить частичную прочтение строки, а также установить флаги ошибок в потоке. Это кажется разумным поведением как для обнаружения того, что что-то есть, так и для ввода информации для потенциального контроля.

Хотя есть несколько примеров кода, в которых реализована ограниченная версия getline(), вот еще одна! Я думаю, что это проще (хотя, возможно, медленнее, производительность может быть решена при необходимости), который также сохраняет это std::getline() с Интерфейс: его использование ручья width() общаться предел (возможно, принимая width() во внимание разумное расширение 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; 
} 

Эта версия getline() используется так же, как std::getline(), но когда это представляется разумным ограничить количество данных прочитан, width() установлен, например:

std::string line; 
if (safe_getline(in >> std::setw(max_characters), line)) { 
    // do something with the input 
} 

Другой подход просто использовать буфер потока фильтрации, чтобы ограничить количество ввода: фильтр будет просто подсчитывать количество обработанных символов и ограничивать количество подходящим количеством символов. Этот подход на самом деле проще применять ко всему потоку, чем к отдельной строке: при обработке только одной строки фильтр не может просто получать буферы, заполненные символами из базового потока, потому что нет надежного способа вернуть символы. Реализация небуферизованных версии по-прежнему просто, но, вероятно, не особенно эффективно:

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); 
    } 
}; 

Этот буфер поток уже настроен для вставки себя на строительство и удалить себя от разрушения. То есть, он может быть использован просто как это:

std::string line; 
basic_limitbuf<char> sbuf(max_characters, in); 
if (std::getline(in, line)) { 
    // do something with the input 
} 

Было бы легко добавить манипулятор настройку до предела, тоже. Одно из преимуществ этого подхода заключается в том, что ни один из кода чтения не должен быть затронут, если общий размер потока может быть ограничен: фильтр можно настроить сразу после создания потока. Когда нет необходимости отпирать фильтр, фильтр может также использовать буфер, который значительно повысит производительность.

Третий подход заключается в использовании std::basic_string с пользовательским распределителем. Есть два аспекта, которые немного неуклюжими о распределителе подходе:

  1. Строка читается на самом деле имеет тип, который не является непосредственно конвертируется в std::string (хотя это и не трудно сделать преобразование).
  2. Максимальный размер массива можно легко ограничить, но строка будет иметь более или менее случайный размер, меньший, чем это: при сбое потока выделение исключения генерируется, и нет попытки вырастить строку меньшим размером.

Вот необходимый код для распределителя ограничения размера выделенного:

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); 
} 

Распределитель будет использоваться что-то вроде этого (кода компилируется нормально с последней версией clang, но не с 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 
} 

В целом, существует несколько подхода каждый со своим собственным небольшим недостатком, но каждый достаточно жизнеспособным для заявленной цели предельного отрицания серва ледовые наложения по линиям с перекрытием:

  1. Использование пользовательской версии getline() означает, что код чтения необходимо изменить.
  2. Использование настраиваемого буфера потока выполняется медленно, если размер всего потока не ограничен.
  3. Использование пользовательского распределителя дает меньше контроля и требует некоторых изменений в чтении кода.
Смежные вопросы