|
Packit |
a4aae4 |
// -*- mode: c++; c-basic-offset:4 -*-
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// This file is part of libdap, A C++ implementation of the OPeNDAP Data
|
|
Packit |
a4aae4 |
// Access Protocol.
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Copyright (c) 2009 OPeNDAP, Inc.
|
|
Packit |
a4aae4 |
// Author: James Gallagher <jgallagher@opendap.org>
|
|
Packit |
a4aae4 |
//
|
|
Packit |
a4aae4 |
// This library is free software; you can redistribute it and/or
|
|
Packit |
a4aae4 |
// modify it under the terms of the GNU Lesser General Public
|
|
Packit |
a4aae4 |
// License as published by the Free Software Foundation; either
|
|
Packit |
a4aae4 |
// version 2.1 of the License, or (at your option) any later version.
|
|
Packit |
a4aae4 |
//
|
|
Packit |
a4aae4 |
// This library is distributed in the hope that it will be useful,
|
|
Packit |
a4aae4 |
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
Packit |
a4aae4 |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Packit |
a4aae4 |
// Lesser General Public License for more details.
|
|
Packit |
a4aae4 |
//
|
|
Packit |
a4aae4 |
// You should have received a copy of the GNU Lesser General Public
|
|
Packit |
a4aae4 |
// License along with this library; if not, write to the Free Software
|
|
Packit |
a4aae4 |
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
Packit |
a4aae4 |
//
|
|
Packit |
a4aae4 |
// You can contact OPeNDAP, Inc. at PO Box 112, Saunderstown, RI. 02874-0112.
|
|
Packit |
a4aae4 |
//
|
|
Packit |
a4aae4 |
// Portions of this code were taken verbatim from Josuttis,
|
|
Packit |
a4aae4 |
// "The C++ Standard Library," p.672
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#include "config.h"
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#include <arpa/inet.h>
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#include <stdint.h>
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#include <string>
|
|
Packit |
a4aae4 |
#include <streambuf>
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#include <cstring>
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
//#define DODS_DEBUG
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#include "chunked_stream.h"
|
|
Packit |
a4aae4 |
#include "chunked_ostream.h"
|
|
Packit |
a4aae4 |
#include "debug.h"
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
namespace libdap {
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// flush the characters in the buffer
|
|
Packit |
a4aae4 |
/**
|
|
Packit |
a4aae4 |
* @brief Write out the contents of the buffer as a chunk.
|
|
Packit |
a4aae4 |
*
|
|
Packit |
a4aae4 |
* @return EOF on error, otherwise the number of bytes in the chunk body.
|
|
Packit |
a4aae4 |
*/
|
|
Packit |
a4aae4 |
std::streambuf::int_type
|
|
Packit |
a4aae4 |
chunked_outbuf::data_chunk()
|
|
Packit |
a4aae4 |
{
|
|
Packit |
a4aae4 |
DBG(cerr << "In chunked_outbuf::data_chunk" << endl);
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
int32_t num = pptr() - pbase(); // num needs to be signed for the call to pbump
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Since this is called by sync() (e.g., flush()), return 0 and do nothing
|
|
Packit |
a4aae4 |
// when there's no data to send.
|
|
Packit |
a4aae4 |
if (num == 0)
|
|
Packit |
a4aae4 |
return 0;
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// here, write out the chunk headers: CHUNKTYPE and CHUNKSIZE
|
|
Packit |
a4aae4 |
// as a 32-bit unsigned int. Here I assume that num is never
|
|
Packit |
a4aae4 |
// more than 2^24 because that was tested in the constructor
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Trick: This method always writes CHUNK_DATA type chunks so
|
|
Packit |
a4aae4 |
// the chunk type is always 0x00, and given that num never has
|
|
Packit |
a4aae4 |
// anything bigger than 24-bits, the high order byte is always
|
|
Packit |
a4aae4 |
// 0x00. Of course bit-wise OR with 0x00 isn't going to do
|
|
Packit |
a4aae4 |
// much anyway... Here's the general idea all the same:
|
|
Packit |
a4aae4 |
//
|
|
Packit |
a4aae4 |
// unsigned int chunk_header = (unsigned int)num | CHUNK_type;
|
|
Packit |
a4aae4 |
uint32_t header = num;
|
|
Packit |
a4aae4 |
#if !BYTE_ORDER_PREFIX
|
|
Packit |
a4aae4 |
// Add encoding of host's byte order. jhrg 11/24/13
|
|
Packit |
a4aae4 |
if (!d_big_endian) header |= CHUNK_LITTLE_ENDIAN;
|
|
Packit |
a4aae4 |
// network byte order for the header
|
|
Packit |
a4aae4 |
htonl(header);
|
|
Packit |
a4aae4 |
#endif
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
d_os.write((const char *)&header, sizeof(int32_t));
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Should bad() throw an error?
|
|
Packit |
a4aae4 |
// Are these functions fast or would the bits be faster?
|
|
Packit |
a4aae4 |
d_os.write(d_buffer, num);
|
|
Packit |
a4aae4 |
if (d_os.eof() || d_os.bad())
|
|
Packit |
a4aae4 |
return traits_type::eof();
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
pbump(-num);
|
|
Packit |
a4aae4 |
return num;
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
/**
|
|
Packit |
a4aae4 |
* @brief Send an end chunk.
|
|
Packit |
a4aae4 |
*
|
|
Packit |
a4aae4 |
* This is like calling flush_chunk(), but it sends a chunk header with a type of
|
|
Packit |
a4aae4 |
* CHUNK_END (instead of CHUNK_DATA). Whatever is in the buffer is written out, but
|
|
Packit |
a4aae4 |
* the stream is can be used to send more chunks.
|
|
Packit |
a4aae4 |
* @note This is called by the chunked_outbuf destructor, so closing a stream using
|
|
Packit |
a4aae4 |
* chunked_outbuf always sends a CHUNK_END type chunk, even if it will have zero
|
|
Packit |
a4aae4 |
* bytes
|
|
Packit |
a4aae4 |
* @return EOF on error, otherwise the number of bytes sent in the chunk.
|
|
Packit |
a4aae4 |
*/
|
|
Packit |
a4aae4 |
std::streambuf::int_type
|
|
Packit |
a4aae4 |
chunked_outbuf::end_chunk()
|
|
Packit |
a4aae4 |
{
|
|
Packit |
a4aae4 |
DBG(cerr << "In chunked_outbuf::end_chunk" << endl);
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
int32_t num = pptr() - pbase(); // num needs to be signed for the call to pbump
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// write out the chunk headers: CHUNKTYPE and CHUNKSIZE
|
|
Packit |
a4aae4 |
// as a 32-bit unsigned int. Here I assume that num is never
|
|
Packit |
a4aae4 |
// more than 2^24 because that was tested in the constructor
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
uint32_t header = (uint32_t)num | CHUNK_END;
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#if !BYTE_ORDER_PREFIX
|
|
Packit |
a4aae4 |
// Add encoding of host's byte order. jhrg 11/24/13
|
|
Packit |
a4aae4 |
if (!d_big_endian) header |= CHUNK_LITTLE_ENDIAN;
|
|
Packit |
a4aae4 |
// network byte order for the header
|
|
Packit |
a4aae4 |
htonl(header);
|
|
Packit |
a4aae4 |
#endif
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Write out the CHUNK_END header with the byte count.
|
|
Packit |
a4aae4 |
// This should be called infrequently, so it's probably not worth
|
|
Packit |
a4aae4 |
// optimizing away chunk_header
|
|
Packit |
a4aae4 |
d_os.write((const char *)&header, sizeof(uint32_t));
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Should bad() throw an error?
|
|
Packit |
a4aae4 |
// Are these functions fast or would the bits be faster?
|
|
Packit |
a4aae4 |
d_os.write(d_buffer, num);
|
|
Packit |
a4aae4 |
if (d_os.eof() || d_os.bad())
|
|
Packit |
a4aae4 |
return traits_type::eof();
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
pbump(-num);
|
|
Packit |
a4aae4 |
return num;
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
/**
|
|
Packit |
a4aae4 |
* @brief Send an error chunk
|
|
Packit |
a4aae4 |
* While building up the next chunk, send an error chunk, ignoring the data currently
|
|
Packit |
a4aae4 |
* write buffer. The buffer is left in a consistent state.
|
|
Packit |
a4aae4 |
* @param msg The error message to include in the error chunk
|
|
Packit |
a4aae4 |
* @return The number of characters ignored.
|
|
Packit |
a4aae4 |
*/
|
|
Packit |
a4aae4 |
std::streambuf::int_type
|
|
Packit |
a4aae4 |
chunked_outbuf::err_chunk(const std::string &m)
|
|
Packit |
a4aae4 |
{
|
|
Packit |
a4aae4 |
DBG(cerr << "In chunked_outbuf::err_chunk" << endl);
|
|
Packit |
a4aae4 |
std::string msg = m;
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Figure out how many chars are in the buffer - these will be
|
|
Packit |
a4aae4 |
// ignored.
|
|
Packit |
a4aae4 |
int32_t num = pptr() - pbase(); // num needs to be signed for the call to pbump
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// write out the chunk headers: CHUNKTYPE and CHUNKSIZE
|
|
Packit |
a4aae4 |
// as a 32-bit unsigned int. Here I assume that num is never
|
|
Packit |
a4aae4 |
// more than 2^24 because that was tested in the constructor
|
|
Packit |
a4aae4 |
if (msg.length() > 0x00FFFFFF)
|
|
Packit |
a4aae4 |
msg = "Error message too long";
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
uint32_t header = (uint32_t)msg.length() | CHUNK_ERR;
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
#if !BYTE_ORDER_PREFIX
|
|
Packit |
a4aae4 |
// Add encoding of host's byte order. jhrg 11/24/13
|
|
Packit |
a4aae4 |
if (!d_big_endian) header |= CHUNK_LITTLE_ENDIAN;
|
|
Packit |
a4aae4 |
// network byte order for the header
|
|
Packit |
a4aae4 |
htonl(header);
|
|
Packit |
a4aae4 |
#endif
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Write out the CHUNK_END header with the byte count.
|
|
Packit |
a4aae4 |
// This should be called infrequently, so it's probably not worth
|
|
Packit |
a4aae4 |
// optimizing away chunk_header
|
|
Packit |
a4aae4 |
d_os.write((const char *)&header, sizeof(uint32_t));
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Should bad() throw an error?
|
|
Packit |
a4aae4 |
// Are these functions fast or would the bits be faster?
|
|
Packit |
a4aae4 |
d_os.write(msg.data(), msg.length());
|
|
Packit |
a4aae4 |
if (d_os.eof() || d_os.bad())
|
|
Packit |
a4aae4 |
return traits_type::eof();
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Reset the buffer pointer, effectively ignoring what's in there now
|
|
Packit |
a4aae4 |
pbump(-num);
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// return the number of characters ignored
|
|
Packit |
a4aae4 |
return num;
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
/**
|
|
Packit |
a4aae4 |
* @brief Virtual method called when the internal buffer would overflow.
|
|
Packit |
a4aae4 |
* When the internal buffer fills, this method is called by the byte that
|
|
Packit |
a4aae4 |
* would cause that overflow. The buffer pointers have been set so that
|
|
Packit |
a4aae4 |
* there is actually space for one more character, so \c c can really be
|
|
Packit |
a4aae4 |
* sent. Put \c c into the buffer and send it, prefixing the buffer
|
|
Packit |
a4aae4 |
* contents with a chunk header.
|
|
Packit |
a4aae4 |
* @note This method is called by the std::ostream code.
|
|
Packit |
a4aae4 |
* @param c The last character to add to the buffer before sending the
|
|
Packit |
a4aae4 |
* next chunk.
|
|
Packit |
a4aae4 |
* @return EOF on error, otherwise the value of \c c.
|
|
Packit |
a4aae4 |
*/
|
|
Packit |
a4aae4 |
std::streambuf::int_type
|
|
Packit |
a4aae4 |
chunked_outbuf::overflow(int c)
|
|
Packit |
a4aae4 |
{
|
|
Packit |
a4aae4 |
DBG(cerr << "In chunked_outbuf::overflow" << endl);
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Note that the buffer and eptr() were set so that when pptr() is
|
|
Packit |
a4aae4 |
// at the end of the buffer, there is actually one more character
|
|
Packit |
a4aae4 |
// available in the buffer.
|
|
Packit |
a4aae4 |
if (!traits_type::eq_int_type(c, traits_type::eof())) {
|
|
Packit |
a4aae4 |
*pptr() = traits_type::not_eof(c);
|
|
Packit |
a4aae4 |
pbump(1);
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
// flush the buffer
|
|
Packit |
a4aae4 |
if (data_chunk() == traits_type::eof()) {
|
|
Packit |
a4aae4 |
//Error
|
|
Packit |
a4aae4 |
return traits_type::eof();
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
return traits_type::not_eof(c);
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
/*
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
d_buffer
|
|
Packit |
a4aae4 |
|
|
|
Packit |
a4aae4 |
v
|
|
Packit |
a4aae4 |
|--------------------------------------------|....
|
|
Packit |
a4aae4 |
| | .
|
|
Packit |
a4aae4 |
|--------------------------------------------|....
|
|
Packit |
a4aae4 |
^ ^ ^
|
|
Packit |
a4aae4 |
| | |
|
|
Packit |
a4aae4 |
pbase() pptr() epptr()
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
*/
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
/**
|
|
Packit |
a4aae4 |
* @brief Write bytes to the chunked stream
|
|
Packit |
a4aae4 |
* Write the bytes in \c s to the chunked stream
|
|
Packit |
a4aae4 |
* @param s
|
|
Packit |
a4aae4 |
* @param num
|
|
Packit |
a4aae4 |
* @return The number of bytes written
|
|
Packit |
a4aae4 |
*/
|
|
Packit |
a4aae4 |
std::streamsize
|
|
Packit |
a4aae4 |
chunked_outbuf::xsputn(const char *s, std::streamsize num)
|
|
Packit |
a4aae4 |
{
|
|
Packit |
a4aae4 |
DBG(cerr << "In chunked_outbuf::xsputn: num: " << num << endl);
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// if the current block of data will fit in the buffer, put it there.
|
|
Packit |
a4aae4 |
// else, there is at least a complete chunk between what's in the buffer
|
|
Packit |
a4aae4 |
// and what's in 's'; send a chunk header, the stuff in the buffer and
|
|
Packit |
a4aae4 |
// bytes from 's' to make a complete chunk. Then iterate over 's' sending
|
|
Packit |
a4aae4 |
// more chunks until there's less than a complete chunk left in 's'. Put
|
|
Packit |
a4aae4 |
// the bytes remaining 's' in the buffer. Return the number of bytes sent
|
|
Packit |
a4aae4 |
// or 0 if an error is encountered.
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
int32_t bytes_in_buffer = pptr() - pbase(); // num needs to be signed for the call to pbump
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Will num bytes fit in the buffer? The location of epptr() is one back from
|
|
Packit |
a4aae4 |
// the actual end of the buffer, so the next char written will trigger a write
|
|
Packit |
a4aae4 |
// of the buffer as a new data chunk.
|
|
Packit |
a4aae4 |
if (bytes_in_buffer + num < d_buf_size) {
|
|
Packit |
a4aae4 |
DBG2(cerr << ":xsputn: buffering num: " << num << endl);
|
|
Packit |
a4aae4 |
memcpy(pptr(), s, num);
|
|
Packit |
a4aae4 |
pbump(num);
|
|
Packit |
a4aae4 |
return traits_type::not_eof(num);
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// If here, write a chunk header and a chunk's worth of data by combining the
|
|
Packit |
a4aae4 |
// data in the buffer and some data from 's'.
|
|
Packit |
a4aae4 |
uint32_t header = d_buf_size;
|
|
Packit |
a4aae4 |
#if !BYTE_ORDER_PREFIX
|
|
Packit |
a4aae4 |
// Add encoding of host's byte order. jhrg 11/24/13
|
|
Packit |
a4aae4 |
if (!d_big_endian) header |= CHUNK_LITTLE_ENDIAN;
|
|
Packit |
a4aae4 |
// network byte order for the header
|
|
Packit |
a4aae4 |
htonl(header);
|
|
Packit |
a4aae4 |
#endif
|
|
Packit |
a4aae4 |
d_os.write((const char *)&header, sizeof(int32_t)); // Data chunk's CHUNK_TYPE is 0x00000000
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Reset the pptr() and epptr() now in case of an error exit. See the 'if'
|
|
Packit |
a4aae4 |
// at teh end of this for the only code from here down that will modify the
|
|
Packit |
a4aae4 |
// pptr() value.
|
|
Packit |
a4aae4 |
setp(d_buffer, d_buffer + (d_buf_size - 1));
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
d_os.write(d_buffer, bytes_in_buffer);
|
|
Packit |
a4aae4 |
if (d_os.eof() || d_os.bad())
|
|
Packit |
a4aae4 |
return traits_type::not_eof(0);
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
int bytes_to_fill_out_buffer = d_buf_size - bytes_in_buffer;
|
|
Packit |
a4aae4 |
d_os.write(s, bytes_to_fill_out_buffer);
|
|
Packit |
a4aae4 |
if (d_os.eof() || d_os.bad())
|
|
Packit |
a4aae4 |
return traits_type::not_eof(0);
|
|
Packit |
a4aae4 |
s += bytes_to_fill_out_buffer;
|
|
Packit |
a4aae4 |
uint32_t bytes_still_to_send = num - bytes_to_fill_out_buffer;
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Now send all the remaining data in s until the amount remaining doesn't
|
|
Packit |
a4aae4 |
// fill a complete chunk and buffer those data.
|
|
Packit |
a4aae4 |
while (bytes_still_to_send >= d_buf_size) {
|
|
Packit |
a4aae4 |
// This is header for a chunk of d_buf_size bytes; the size was set above
|
|
Packit |
a4aae4 |
d_os.write((const char *) &header, sizeof(int32_t));
|
|
Packit |
a4aae4 |
d_os.write(s, d_buf_size);
|
|
Packit |
a4aae4 |
if (d_os.eof() || d_os.bad()) return traits_type::not_eof(0);
|
|
Packit |
a4aae4 |
s += d_buf_size;
|
|
Packit |
a4aae4 |
bytes_still_to_send -= d_buf_size;
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
if (bytes_still_to_send > 0) {
|
|
Packit |
a4aae4 |
// if the code is here, one or more chunks have been sent, the
|
|
Packit |
a4aae4 |
// buffer is empty and there are < d_buf_size bytes to send. Buffer
|
|
Packit |
a4aae4 |
// them.
|
|
Packit |
a4aae4 |
memcpy(d_buffer, s, bytes_still_to_send);
|
|
Packit |
a4aae4 |
pbump(bytes_still_to_send);
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
// Unless an error was detected while writing to the stream, the code must
|
|
Packit |
a4aae4 |
// have sent num bytes.
|
|
Packit |
a4aae4 |
return traits_type::not_eof(num);
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
/**
|
|
Packit |
a4aae4 |
* @brief Synchronize the stream with its data sink.
|
|
Packit |
a4aae4 |
* @note This method is called by flush() among others
|
|
Packit |
a4aae4 |
* @return -1 on error, 0 otherwise.
|
|
Packit |
a4aae4 |
*/
|
|
Packit |
a4aae4 |
std::streambuf::int_type
|
|
Packit |
a4aae4 |
chunked_outbuf::sync()
|
|
Packit |
a4aae4 |
{
|
|
Packit |
a4aae4 |
DBG(cerr << "In chunked_outbuf::sync" << endl);
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
if (data_chunk() == traits_type::eof()) {
|
|
Packit |
a4aae4 |
// Error
|
|
Packit |
a4aae4 |
return traits_type::not_eof(-1);
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
return traits_type::not_eof(0);
|
|
Packit |
a4aae4 |
}
|
|
Packit |
a4aae4 |
|
|
Packit |
a4aae4 |
} // namespace libdap
|