- •Introduction
- •Saving Time with This Book
- •Conventions Used in This Book
- •Part II: Working with the Pre-Processor
- •Part III: Types
- •Part IV: Classes
- •Part V: Arrays and Templates
- •Part VI: Input and Output
- •Part VII: Using the Built-in Functionality
- •Part VIII: Utilities
- •Part IX: Debugging C++ Applications
- •Part X: The Scary (or Fun!) Stuff
- •Icons Used in This Book
- •Creating and Implementing an Encapsulated Class
- •Creating a Mailing-List Application
- •Testing the Mailing-List Application
- •Customizing a Class with Polymorphism
- •Testing the Virtual Function Code
- •Why Do the Destructors Work?
- •Delayed Construction
- •The cDate Class
- •Testing the cDate Class
- •Creating the Header File
- •Testing the Header File
- •The Assert Problem
- •Fixing the Assert Problem
- •Using the const Construct
- •Identifying the Errors
- •Fixing the Errors
- •Fixing What Went Wrong with the Macro
- •Using Macros Appropriately
- •Using the sizeof Function
- •Evaluating the Results
- •Using sizeof with Pointers
- •Implementing the Range Class
- •Testing the Range Class
- •Creating the Matrix Class
- •Matrix Operations
- •Multiplying a Matrix by a Scalar Value
- •Multiplying a Matrix by Scalar Values, Take 2
- •Testing the Matrix Class
- •Implementing the Enumeration Class
- •Testing the Enumeration Class
- •Implementing Structures
- •Interpreting the Output
- •Defining Constants
- •Testing the Constant Application
- •Using the const Keyword
- •Illustrating Scope
- •Interpreting the Output
- •Using Casts
- •Addressing the Compiler Problems
- •Testing the Changes
- •Implementing Member-Function Pointers
- •Updating Your Code with Member-Function Pointers
- •Testing the Member Pointer Code
- •Customizing Functions We Wrote Ourselves
- •Testing the Default Code
- •Fixing the Problem
- •Testing the Complete Class
- •Implementing Virtual Inheritance
- •Correcting the Code
- •Rules for Creating Overloaded Operators
- •Using Conversion Operators
- •Using Overloaded Operators
- •Testing the MyString Class
- •Rules for Implementing new and delete Handlers
- •Overloading new and delete Handlers
- •Testing the Memory Allocation Tracker
- •Implementing Properties
- •Testing the Property Class
- •Implementing Data Validation with Classes
- •Testing Your SSN Validator Class
- •Creating the Date Class
- •Testing the Date Class
- •Some Final Thoughts on the Date Class
- •Creating a Factory Class
- •Testing the Factory
- •Enhancing the Manager Class
- •Implementing Mix-In Classes
- •Testing the Template Classes
- •Implementing Function Templates
- •Creating Method Templates
- •Using the Vector Class
- •Creating the String Array Class
- •Working with Vector Algorithms
- •Creating an Array of Heterogeneous Objects
- •Creating the Column Class
- •Creating the Row Class
- •Creating the Spreadsheet Class
- •Testing Your Spreadsheet
- •Working with Streams
- •Testing the File-Reading Code
- •Creating the Test File
- •Reading Delimited Files
- •Testing the Code
- •Creating the XML Writer
- •Testing the XML Writer
- •Creating the Configuration-File Class
- •Setting Up Your Test File
- •Building the Language Files
- •Creating an Input Text File
- •Reading the International File
- •Testing the String Reader
- •Creating a Translator Class
- •Testing the Translator Class
- •Creating a Virtual File Class
- •Testing the Virtual File Class
- •Using the auto_ptr Class
- •Creating a Memory Safe Buffer Class
- •Throwing and Logging Exceptions
- •Dealing with Unhandled Exceptions
- •Re-throwing Exceptions
- •Creating the Wildcard Matching Class
- •Testing the Wildcard Matching Class
- •Creating the URL Codec Class
- •Testing the URL Codec Class
- •Testing the Rot13 Algorithm
- •Testing the XOR Algorithm
- •Implementing the transform Function to Convert Strings
- •Testing the String Conversions
- •Implementing the Serialization Interface
- •Creating the Buffer Class
- •Testing the Buffer Class
- •Creating the Multiple-Search-Path Class
- •Testing the Multiple-Search-Path Class
- •Testing the Flow Trace System
- •The assert Macro
- •Logging
- •Testing the Logger Class
- •Design by Contract
- •Adding Logging to the Application
- •Making Functions Inline
- •Avoiding Temporary Objects
- •Passing Objects by Reference
- •Choosing Initialization Instead of Assignment
- •Learning How Code Operates
- •Testing the Properties Class
- •Creating the Locking Mechanism
- •Testing the Locking Mechanism
- •Testing the File-Guardian Class
- •Implementing the Complex Class
- •Creating the Conversion Code
- •Testing the Conversion Code
- •A Sample Program
- •Componentizing
- •Restructuring
- •Specialization
- •Index
Creating the Buffer Class 361
The simple answers look pretty silly: This is the way that we’ve always written code, the problem is not that serious, and we aren’t going to change now. Actually the problem is reasonably easy to fix — and there is no excuse not to do so. This technique shows you how. By eliminating security risks, you will provide a safer application and minimize the amount of time you spend issuing security patches and dealing with customer support nightmares, which will save you a lot of time.
Imagine, for example, rewriting the routine just given so it looks like this:
Buffer szBuffer(80); |
|
1 |
|
int |
nPos = 0; |
|
try
{
while ( !eof(fp) )
{
szBuffer[nPos] = fgetc(fp); if ( szBuffer[nPos] == ‘|’ )
break;
nPos++;
}
}
catch ( BufferException& exc )
{
printf(“Buffer Overflow!”);
}
return 0;
Now, this code cannot crash the program. Unlike the earlier code, which used a static array, this program uses a Buffer class (see 1) to manage the buffer.
The Buffer class would check to see that the underlying buffer was not overwritten, and prevent memory from getting stomped on. If either condition were to occur, the program would throw an exception — and recover gracefully.
Catching problems before they occur and spread to odd parts of the program will save you time and frustration later on down the line. If you build your programs as defensively as possible, you will limit the time you have to spend debugging them.
This solution allows programs to behave in that ideal fashion of dealing with problems all by themselves. Perhaps it is not possible to recover completely from an error like this, but at least the program could exit in a safe manner, shutting down all connections and not allowing the operating system to be compromised. This is what software security is all about. The issue, then, is to create a class that implements that generic buffer and protects your data. That is the aim of this technique.
Creating the Buffer Class
The solution we are going to implement for buffer overflows is to create a class that protects the data buffer by allowing access to valid areas of the buffer only. This class overrides the operators that provide access to the individual characters of the buffer, and makes sure that all assignments, copies, and manipulations work only on valid sections of the buffer. Let’s create that class now.
1. In the code editor of your choice, create a new file to hold the code for the source file of the technique.
In this example, the file is named ch60.cpp, although you can use whatever you choose. This file will contain the class definition for your automation object.
2. Type the code in Listing 60-1 into your file.
Better yet, copy the code from the source file on this book’s companion Web site.
LISTING 60-1: THE BUFFER CLASS
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <string>
using namespace std; |
|
|
class BufferException |
|
4 |
{ |
|
(continued)
362 Technique 60: Creating a Generic Buffer Class
LISTING 60-1 (continued)
private: |
|
|
string _errMsg; |
|
|
public: |
|
5 |
BufferException( void ) |
|
|
{ |
|
_errMsg = “”;
}
BufferException( const char *msg )
{
_errMsg = msg;
}
BufferException( const BufferException& aCopy )
{
_errMsg = aCopy._errMsg;
}
const char *Message()
{
return _errMsg.c_str();
}
void setMessage( const char *msg )
{
_errMsg = msg;
}
};
class Buffer
{
private:
//$Member: m_Buffer - This is the actual area of allocated memory.
char *m_Buffer;
//$Member: m_Size - This is the size of the allocated memory.
int m_Size; protected:
virtual void print_buffer( const char *strPrefix )
{
printf(“%s: [“, strPrefix ); for ( int i=0; i<m_Size; ++i )
printf(“%c”, m_Buffer[i] ); printf(“]\n”);
}
virtual void clear()
{
if ( m_Buffer ) delete m_Buffer;
m_Buffer = NULL; m_Size = 0;
}
virtual void copy( const Buffer& aBuffer
)
{
m_Buffer = new char[ aBuffer.Size() ];
memcpy( m_Buffer, aBuffer._Buffer(), aBuffer.Size() );
m_Size = aBuffer.Size();
}
virtual void allocate( int nSize )
{
m_Buffer = new char[ nSize ]; memset( m_Buffer, 0, nSize ); m_Size = nSize;
}
const char *_Buffer(void) const
{
return m_Buffer;
}
public:
//Void constructor. No memory is allocated or available.
Buffer(void)
: m_Buffer(NULL), m_Size(0)
{
// Clear everything. clear();
}
// This is a standard constructor. Buffer( int nSize )
: m_Buffer(NULL), m_Size(0)
{
clear(); allocate( nSize );
}
// Copy the constructor. Buffer( const Buffer& aBuffer )
: m_Buffer(NULL), m_Size(0)
{
clear();
copy( aBuffer );
}
Creating the Buffer Class 363
virtual ~Buffer()
{
clear();
}
// Operators
Buffer &operator=(const Buffer& aBuffer)
{
clear();
copy( aBuffer ); return *this;
}
Buffer &operator=(const char *strBuffer)
{
//If they are assigning us NULL, just clear
//everything out and get out of here.
clear();
if ( strBuffer == NULL ) return *this;
//Otherwise, we need to set up this object
//as the passed-in string. allocate( (int)strlen(strBuffer)+1
);
memcpy( m_Buffer, strBuffer, strlen(strBuffer) );
return *this;
} |
|
2 |
char& operator[](int nPos) |
|
|
{ |
|
if ( nPos < 0 || nPos > m_Size-1 ) throw BufferException( “Buffer:
Array index out of range” ); return m_Buffer[ nPos ];
} |
|
3 |
operator const char*() |
|
|
{ |
|
//Just give them back the entire buffer.
return m_Buffer;
}
const char *c_str( void )
{
return m_Buffer;
}
//Here come the accessor functions. int Size() const
{
return m_Size;
}
//These are memory-based functions. void Set( char c )
{
for ( int i=0; i<m_Size-1; ++i ) m_Buffer[i] = c;
}
void Clear( void )
{
Set( 0 );
}
Buffer |
operator()(int nStart, |
4 |
{ int nLength) |
||
// |
Do some validation |
|
if |
( nStart < 0 || nStart > m_Size-1 |
|
) |
|
|
{ |
|
|
throw BufferException(“Buffer: Array index out of range”);
}
if ( nLength < 0 || nLength > m_Size-1 || nStart+nLength > m_Size-1 )
{
throw BufferException(“Buffer: Length out of range”);
}
Buffer b(nLength+1);
for ( int i=0; i<nLength; ++i ) b[i] = m_Buffer[i+nStart];
return b;
}
};
In the code in the listing above, certain elements make it an important step up from the standard C++ character array. First of all, observe the way in which the characters are retrieved using the
364 Technique 60: Creating a Generic Buffer Class
indexing operator ([]). The operator[], shown at 2, carefully checks to see whether the index requested is within a valid range. If it is not, it will throw an exception. Similarly, the sub-string operator() (shown at 4) checks to make sure that all of the characters are in the valid range. Unlike the character array, the Buffer class allows you to extract small segments of itself, but protects against those segments being invalid. Because the returned value is a Buffer object, this method is safe from overruns. You might notice the conversion operator, shown at the line marked 3; it appears to provide a way for the programmer to destroy the string. In fact, because it returns a constant pointer to the buffer, the programmer cannot directly change it without casting the string, a fact that the compiler will be happy to note. The overall idea is that we are preventing the programmer from doing something that will cause problems without thinking about it more than once.
3. Save the source code in the code editor.
Notice that we create our own Exception class (shown at the line marked with 5) to return errors from the Buffer class. It’s a good idea to have your own forms of exceptions, rather than using the basic types; that way the programmer can deal with system errors in ways that are different from programmatic solutions.
Testing the Buffer Class
After you create a class, you should create a test driver that not only ensures your code is correct, but also shows people how to use your code. The following steps show you how:
1. In the code editor of your choice, reopen the source file to hold the code for your test program.
In this example, I named the test program ch60.cpp.
2. Type the code from Listing 60-2 into your file.
Better yet, copy the code from the source file on this book’s companion Web site.
LISTING 60-2: THE TEST DRIVER FOR THE BUFFER CLASS
int main(int argc, char* argv[])
{
Buffer b1(20); Buffer b2;
b2 = b1; |
|
6 |
|
b1 = “This is a test”; |
|
||
printf(“The buffer is: %s\n”, (const |
|
||
|
char *)b1); |
|
|
b1[2] = ‘a’; |
|
|
|
printf(“The buffer is: %s\n”, (const |
|
|
|
|
char *)b1); |
|
|
try |
|
|
|
{ |
b1[-1] = ‘a’; |
7 |
|
} |
|||
catch ( BufferException& exc ) |
|
|
|
{ |
printf(“Caught the error: %s\n”, |
|
|
|
|
|
|
} |
exc.Message() ); |
|
|
|
|
|
|
// Test the “sub-buffer” function. |
|
|
|
Buffer b3; |
|
|
|
b3 = b1(0,4); |
|
8 |
|
printf(“The new buffer is: [“); |
|
for ( int i=0; i<b3.Size(); ++i ) printf(“%c”, b3[i] );
printf(“]\n”);
return 0;
}
The above code simply exercises some of the important functionality of the Buffer class. First, we check to see that the assignment operators work as they are supposed to. This is shown
at 6 in Listing 60-2. Next, we test to see whether the indexing logic works properly, as shown
at 7. If it is working properly, we would expect to see the exception thrown and printed out on
Testing the Buffer Class 365
the console. Finally, we test the sub-string functions, by extracting a piece of the buffer and making it into a new Buffer object (shown at
8). If the operator is working correctly, weshould see the new buffer be the first four characters of the original string. Let’s test it out and see.
3. Save the source file in the code editor and close the editor application.
4. Compile the source file with your favorite compiler on your favorite operating system.
5. Run the application on your favorite operating system.
If you’ve done everything right, you should see a session similar to this one on your console window:
$ ./a.exe
The buffer is: This is a test The buffer is: Thas is a test
Caught the error: Buffer: Array index out of range
The new buffer is: [Thas ]
As you can see, the output is exactly what we expected. The error was generated and caught, and the exception information was printed to the console. The sub-string was the characters we expected as well, indicating that both the original assignment and the sub-string operators are correct. By using this class, we can therefore expect to save a lot of time in debugging our applications and in developing applications, because a lot of the functionality exposed makes it quicker to check input data.
As you can see, this code offers greater safety. It sure beats allowing the buffers to be overrun and the memory to be stomped on.
61 Opening a File
Using Multiple
Technique Paths
Save Time By
Adding code to allow users to specify search paths to files
Creating a multiple- search-path class
Testing your class
It happens quite often when designing computer software: You ask a user to specify a file to open in your application, and then permit him to choose that file by “browsing” to the correct path. Unfortunately,
there is no good way to utilize the underlying operating system to find specific files, especially if you want your application to be portable across various operating systems. It makes more sense, therefore, to build a method for actually looking across all search paths that might exist when you open a file automatically, without worrying about where they are, or how to get at them.
This technique does the job by building a utility class that manages multiple paths for searching files, using that class to find and read whichever file the user specifies. No magic here — just a handy way to utilize the built-in functionality of the C++ Standard Library functions to avoid the problems of using the operating system to find files, since this process is different for each operating system. There is nothing magical about searching various search paths and opening files, but combining the two into a single object provides some power and flexibility that you can utilize in numerous applications — with little effort on your own part. That, of course, is the entire point to building utility classes in C++: You get a lot of bang for your buck.
The new utility class is responsible for three things:
It stores the various paths used for searching and ensures that those paths are all valid.
It uses the input search paths to find a given file when the user specifies one.
It opens the chosen file and returns a stream-object reference to the programmer for use in manipulating the file. The user can then find out where the file is — and read the file from the stream object as needed. The user may not know, or care, where the file they requested is actually located, so long as it can be found along one of the various search paths.