Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

682 CHAPTER 20 FILE I/O AND ISOLATED STORAGE

Working with BinaryWriters and BinaryReaders

The final writer/reader sets you will examine here are BinaryReader and BinaryWriter, both of which derive directly from System.Object. These types allow you to read and write discrete data types to an underlying stream in a compact binary format. The BinaryWriter class defines a highly overloaded Write() method to place a data type in the underlying stream. In addition to Write(), BinaryWriter provides additional members that allow you to get or set the Stream-derived type and offers support for random access to the data (see Table 20-10).

Table 20-10. BinaryWriter Core Members

Member

Meaning in Life

BaseStream

This read-only property provides access to the underlying stream used with the

 

BinaryWriter object.

Close()

This method closes the binary stream.

Flush()

This method flushes the binary stream.

Seek()

This method sets the position in the current stream.

Write()

This method writes a value to the current stream.

 

 

The BinaryReader class complements the functionality offered by BinaryWriter with the members described in Table 20-11.

Table 20-11. BinaryReader Core Members

Member

Meaning in Life

BaseStream

This read-only property provides access to the underlying stream used with the

 

BinaryReader object.

Close()

This method closes the binary reader.

PeekChar()

This method returns the next available character without actually advancing the

 

position in the stream.

Read()

This method reads a given set of bytes or characters and stores them in the

 

incoming array.

ReadXXXX()

The BinaryReader class defines numerous read methods that grab the next type

 

from the stream (ReadBoolean(), ReadByte(), ReadInt32(), and so forth).

 

 

The following example (a Console Application named BinaryWriterReader) writes a number of data types to a new *.dat file:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Binary Writers / Readers *****\n");

// Open a binary writer for a file.

FileInfo f = new FileInfo("BinFile.dat"); using(BinaryWriter bw = new BinaryWriter(f.OpenWrite()))

{

//Print out the type of BaseStream.

//(System.IO.FileStream in this case).

Console.WriteLine("Base stream is: {0}", bw.BaseStream);

CHAPTER 20 FILE I/O AND ISOLATED STORAGE

683

//Create some data to save in the file double aDouble = 1234.67;

int anInt = 34567;

string aString = "A, B, C";

//Write the data

bw.Write(aDouble);

bw.Write(anInt);

bw.Write(aString);

}

Console.ReadLine();

}

Notice how the FileStream object returned from FileInfo.OpenWrite() is passed to the constructor of the BinaryWriter type. Using this technique, it is very simple to “layer in” a stream before writing out the data. Do understand that the constructor of BinaryWriter takes any Stream-derived type (e.g., FileStream, MemoryStream, or BufferedStream). Thus, if you would rather write binary data to memory, simply supply a valid MemoryStream object.

To read the data out of the BinFile.dat file, the BinaryReader type provides a number of options. Here, you will call various read-centric members to pluck each chunk of data from the file stream:

static void Main(string[] args)

{

...

FileInfo f = new FileInfo("BinFile.dat");

...

// Read the binary data from the stream. using(BinaryReader br = new BinaryReader(f.OpenRead()))

{

Console.WriteLine(br.ReadDouble());

Console.WriteLine(br.ReadInt32());

Console.WriteLine(br.ReadString());

}

Console.ReadLine();

}

Source Code The BinaryWriterReader application is included under the Chapter 20 subdirectory.

Programmatically “Watching” Files

Now that you have a better handle on the use of various readers and writers, next you’ll look at the role of the FileSystemWatcher class. This type can be quite helpful when you wish to programmatically monitor (or “watch”) files on your system. Specifically, the FileSystemWatcher type can be instructed to monitor files for any of the actions specified by the System.IO.NotifyFilters enumeration (while many of these members are self-explanatory, check the .NET Framework 3.5 SDK documentation for further details):

public enum NotifyFilters

{

Attributes, CreationTime, DirectoryName, FileName,

684 CHAPTER 20 FILE I/O AND ISOLATED STORAGE

LastAccess, LastWrite, Security, Size,

}

The first step you will need to take to work with the FileSystemWatcher type is to set the Path property to specify the name (and location) of the directory that contains the files to be monitored, as well as the Filter property that defines the file extensions of the files to be monitored.

At this point, you may choose to handle the Changed, Created, and Deleted events, all of which work in conjunction with the FileSystemEventHandler delegate. This delegate can call any method matching the following pattern:

//The FileSystemEventHandler delegate must point

//to methods matching the following signature.

void MyNotificationHandler(object source, FileSystemEventArgs e)

As well, the Renamed event may also be handled via the RenamedEventHandler delegate type, which can call methods matching the following signature:

//The RenamedEventHandler delegate must point

//to methods matching the following signature.

void MyNotificationHandler(object source, RenamedEventArgs e)

To illustrate the process of watching a file, assume you have created a new directory on your C drive named MyFolder that contains various *.txt files (named whatever you wish). The following Console Application (named MyDirectoryWatcher) will monitor the *.txt files within the

MyFolder directory and print out messages in the event that the files are created, deleted, modified, or renamed:

static void Main(string[] args)

{

Console.WriteLine("***** The Amazing File Watcher App *****\n");

//Establish the path to the directory to watch.

FileSystemWatcher watcher = new FileSystemWatcher(); try

{

watcher.Path = @"C:\MyFolder";

}

catch(ArgumentException ex)

{

Console.WriteLine(ex.Message);

return;

}

//Set up the things to be on the lookout for. watcher.NotifyFilter = NotifyFilters.LastAccess

| NotifyFilters.LastWrite | NotifyFilters.FileName

| NotifyFilters.DirectoryName;

//Only watch text files.

watcher.Filter = "*.txt";

// Add event handlers.

watcher.Changed += new FileSystemEventHandler(OnChanged); watcher.Created += new FileSystemEventHandler(OnChanged); watcher.Deleted += new FileSystemEventHandler(OnChanged); watcher.Renamed += new RenamedEventHandler(OnRenamed);

CHAPTER 20 FILE I/O AND ISOLATED STORAGE

685

//Begin watching the directory. watcher.EnableRaisingEvents = true;

//Wait for the user to quit the program.

Console.WriteLine(@"Press 'q' to quit app."); while(Console.Read()!='q');

}

The two event handlers simply print out the current file modification:

static void OnChanged(object source, FileSystemEventArgs e)

{

// Specify what is done when a file is changed, created, or deleted.

Console.WriteLine("File: {0} {1}!", e.FullPath, e.ChangeType);

}

static void OnRenamed(object source, RenamedEventArgs e)

{

// Specify what is done when a file is renamed.

Console.WriteLine("File: {0} renamed to\n{1}", e.OldFullPath, e.FullPath);

}

To test this program, run the application and open Windows Explorer. Try renaming your files, creating a *.txt file, deleting a *.txt file, and so forth. You will see various bits of information regarding the state of the text files within MyFolder (see Figure 20-8).

Figure 20-8. Watching some text files

Source Code The MyDirectoryWatcher application is included under the Chapter 20 subdirectory.

Performing Asynchronous File I/O

To conclude our examination of the System.IO namespace, let’s see how to interact with FileStream types asynchronously. You have already seen the asynchronous support provided by the .NET Framework during the examination of multithreading (see Chapter 18). Because large file I/O operations can be a lengthy task, all types deriving from System.IO.Stream inherit a set of methods that enable asynchronous processing of the data. As you would expect, these methods work in conjunction with the IAsyncResult type:

public abstract class Stream : MarshalByRefObject, IDisposable

{

686 CHAPTER 20 FILE I/O AND ISOLATED STORAGE

...

public virtual IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state);

public virtual IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state);

public virtual int EndRead(IAsyncResult asyncResult); public virtual void EndWrite(IAsyncResult asyncResult);

}

The process of working with the asynchronous behavior of Stream-derived types is identical to working with asynchronous delegates and asynchronous remote method invocations. While it’s unlikely that asynchronous behaviors will greatly improve file access, other streams (e.g., socket based) are much more likely to benefit from asynchronous handling. In any case, the following Console Application (AsyncFileStream) illustrates one manner in which you can asynchronously interact with a FileStream type (be sure to import the System.Threading and System.IO namespaces):

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Async File I/O *****\n");

Console.WriteLine("Main thread started. ThreadID = {0}",

Thread.CurrentThread.GetHashCode());

//Must use this ctor to get a FileStream with asynchronous

//read or write access.

FileStream fs = new FileStream("logfile.txt", FileMode.Append,

FileAccess.Write, FileShare.None, 4096, true);

string msg = "this is a test";

byte[] buffer = Encoding.ASCII.GetBytes(msg);

//Start the asynchronous write. WriteDone invoked when finished.

//Note that the FileStream object is passed as state info to the

//callback method.

fs.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteDone), fs);

}

private static void WriteDone(IAsyncResult ar)

{

Console.WriteLine("AsyncCallback method on ThreadID = {0}", Thread.CurrentThread.GetHashCode());

Stream s = (Stream)ar.AsyncState; s.EndWrite(ar);

s.Close();

}

}

The only point of interest in this example (assuming you recall the process of working with delegates!) is that in order to enable the asynchronous behavior of the FileStream type, you must make use of a specific constructor (shown here). The final System.Boolean parameter (when set to true) informs the FileStream object to perform its work on a secondary thread of execution.

CHAPTER 20 FILE I/O AND ISOLATED STORAGE

687

Source Code The AsyncFileStream application is included under the Chapter 20 subdirectory.

Understanding the Role of Isolated Storage

Each of the file I/O examples you have just examined make a very big assumption regarding the execution of the application: that it has been granted full trust security privileges by the CLR. As you would imagine, the act of reading from, or writing to, a machine’s hard drive could be a potential security threat, based on the origin of the application. Recall that the .NET platform supports the ability to download assemblies from a variety of locations including external websites, from an intranet, or even dynamically in memory using an assembly created via the types of the System. Reflection.Emit namespace (examined in Chapter 19).

Furthermore, you may also recall from Chapter 15 that the <codeBase> element of a client *.config file allows you to declaratively specify such an arbitrary path for the CLR to find an external assembly; while the Assembly.LoadFrom() method (see Chapter 16) allows you to programmatically load assemblies located at an external URI.

It’s a Matter of Trust

Given the fact that a .NET assembly can be loaded from a variety of locations beyond the current machine’s local hard drive, the issue of trust becomes very important. By way of illustration, assume you have an application that executes code downloaded from a remote location. If this code library attempts to read files on the local computer, how can we ensure the library is not attempting to read sensitive information? Likewise, if we download a remote executable to run on a local machine, how can we make sure this assembly does not attempt to read sensitive data within the system registry, make calls to the underlying API of the operating system (for evil purposes), or other such potential security risks?

The answer, as far as the .NET platform is concerned, is to make use of a .NET-centric security mechanism known as Code Access Security. Using CAS, the CLR can deny or grant a number of security privileges to the executing assembly, including the following:

Manipulation of a machine’s directory/file structure

Manipulation of network/web/database connections

Creation of new application domains/dynamic assemblies

Use of .NET reflection services

Calls to unmanaged code using PInvoke

Focusing on the I/O-specific security concerns, assume that you are authoring an application that will be deployed to external users via some remote web-based URL. Based on how you configure the deployment script (and based on the security policy of the user’s machine), it may run under a restricted security environment that will prevent access to the local file system. If your application must store user or application settings using the types of the System.IO namespace, the CLR will throw runtime security exceptions!

The types within the System.IO.IsolatedStorage namespace can be used to create an application that reads and writes data to a very specific location of a .NET-aware machine using isolated storage. This can be understood as a safe “sandbox” where the CLR will allow file I/O operations to occur, even if the application has been downloaded from an external URL or has in other ways been placed in a security sandbox by a system administrator.

688 CHAPTER 20 FILE I/O AND ISOLATED STORAGE

Other Uses of the Isolated Storage API

Understand, however, that the isolated storage API is not limited to controlling read/write file operations on remotely downloaded code. This API also provides a simple way to persist per-user data in a manner that ensures other applications cannot indirectly (or directly) tamper with said data. For example, using isolated storage, it is possible to build a single application that saves data in isolated folders for each user logged on to a specific workstation.

Another benefit of using the isolated storage API is that your code base does not need to hardcode paths or directory names in the application. Rather, when using isolated storage, an application indirectly saves data to a unique data compartment that is associated with some aspect of the code’s identity, such as its URL, strong name, or X509 digital signature (more information on code identity later in this chapter).

Thankfully, programming with the isolated storage API is very simple to those who understand basic file I/O operations. However, before we examine how to do so, allow me to provide an overview of the .NET Code Access Security model.

Note Complete coverage of CAS would easily entail an entire chapter (or two). Here I will explain the core operation of CAS in order to set the foundation for the role of the isolated storage API. Please consult the .NET Framework 3.5 SDK for further details of CAS if you are so inclined.

A Primer on Code Access Security

To address the security issues involved with downloading and executing remote .NET assemblies, the CLR will automatically determine the assembly’s identity and assign it to one of many preconfigured code groups. Simply put, a code group is a collection of assemblies that all meet the same criteria (such as the point of origin).

The criterion used by the CLR to determine which code group an assembly belongs to is referred to as evidence. Beyond the point of origin, an assembly can be placed into a code group using other forms of evidence such as an assembly’s strong name, an embedded X509 digital certificate, or some sort of custom criteria you have accounted for programmatically.

Note Strictly speaking, evidence comes in two flavors: assembly evidence and host evidence. While assembly evidence is compiled into the assembly, host evidence can only be specified programmatically using the AppDomain type.

Once an assembly’s evidence has been evaluated to determine which code group it belongs to, the CLR will then consult the permission set (which, as you might guess, is simply a named collection of individual permissions) associated with the code group to determine what the assembly can and, more importantly, cannot do.

Collectively, code groups and their related permissions constitute a security policy, which can actually be partitioned at three major levels (enterprise, machine, and user). Using this stratified approach, it is possible for a system administrator to create unique policies for the company at large as well as at the machine/user level.

Once each of the security policies have been applied (enterprise, machine, and user), the assembly will execute under the .NET runtime. If the assembly attempts to execute code outside of its permission set, the CLR will throw a runtime security exception. Figure 20-9 illustrates how

CHAPTER 20 FILE I/O AND ISOLATED STORAGE

689

these building blocks of CAS (evidence, code groups/permission sets, and policies) intertwine from a high level.

Figure 20-9. The building blocks of CAS

The act of evaluating evidence, placing assemblies into code groups, and mapping permission sets to the assembly in question happens transparently in the background whenever you run a .NET application. For the most part, the default CAS security settings and CLR/CAS interactions can be allowed to function in the background without any direct interaction on your part. However, it is worth your while to dig a bit deeper into the building blocks of CAS, beginning with the notion of assembly evidence.

The Role of Evidence

In order for the CLR to determine which code group to place an assembly into, the first step is to read the supplied evidence. As mentioned, evidence is simply information obtained from an assembly (or possible the hosting application domain) at the point it is loaded into memory. Table 20-12 documents the major types of evidence an assembly can present to the CLR.

Table 20-12. Various Types of Assembly Evidence

Host Evidence Type

Meaning in Life

Application directory

The installation directory of the assembly

Assembly hash code

The hash value of an assembly’s contents

Publisher certificate

The Authenticode X509 digital certificate assigned to the assembly (if

 

any)

Site

The source website where an assembly was loaded (does not apply to

 

assemblies loaded from the local machine)

Assembly strong name

The strong name of an assembly (if any)

URL

The URL from which an assembly was loaded (HTTP, FTP, file, and so on)

Zone

The name of the zone where the assembly was loaded

 

 

While the reading of evidence happens automatically, it is possible to programmatically read evidence as well using the reflection APIs and the Evidence type within the System.Security.Policy namespace. To deepen your understanding of evidence, create a new Console Application named

690CHAPTER 20 FILE I/O AND ISOLATED STORAGE

MyEvidenceViewer. Once you have done so, be sure to import the System.Reflection, System. Collections, and System.Security.Policy namespaces.

We will now build a simple application that will prompt the user for the name of an assembly to load into memory. At this point, we will enumerate over each supplied form of assembly evidence and print the data to the console window. To begin, the Program type provides a Main() method that allows users to enter the full path to the assembly they wish to evaluate. If they enter the L option, we will call a helper method that attempts to load the specified assembly into memory. If successful, we will pass the Assembly reference to another helper method named DisplayAsmEvidence(). Here is the story so far:

class Program

{

static void Main(string[] args)

{

bool isUserDone = false; string userOption = ""; Assembly asm = null;

Console.WriteLine("***** Evidence Viewer *****\n");

do

{

Console.Write("L (load assembly) or Q (quit): "); userOption = Console.ReadLine();

switch (userOption.ToLower())

{

case "l":

asm = LoadAsm(); if (asm != null)

{

DisplayAsmEvidence(asm);

}

break;

case "q": isUserDone = true; break;

default:

Console.WriteLine("I have no idea what you want!"); break;

}

} while (!isUserDone);

}

}

The LoadAsm() method will simply call Assembly.LoadFrom() to set the private Assembly member variable:

private static Assembly LoadAsm()

{

Console.Write("Enter path to assembly: "); try

{

return Assembly.LoadFrom(Console.ReadLine());

}

catch

{

CHAPTER 20 FILE I/O AND ISOLATED STORAGE

691

Console.WriteLine("Load error..."); return null;

}

}

Finally, the DisplayAsmEvidence() method will extract out the evidence of the loaded assembly via the Evidence property of the Assembly type. From here, we obtain an enumerator (via the GetHostEvidence() method of the Evidence type) and print out each flavor of presented evidence:

private static void DisplayAsmEvidence(Assembly asm)

{

//Get evidence collection and underlying enumerator.

Evidence e = asm.Evidence;

IEnumerator itfEnum = e.GetHostEnumerator();

//Now print out the evidence.

while (itfEnum.MoveNext())

{

Console.WriteLine(" **** Press Enter to continue ****");

Console.ReadLine();

Console.WriteLine(itfEnum.Current);

}

}

To test our application, my suggestion is to create a folder directly off your C drive named MyAsms. Into this folder, copy the strongly named CarLibrary.dll assembly (from Chapter 15), and run your program. Assuming you opt for the L command, specify the full path to your assembly and press Enter. Your application should now print out each flavor of evidence to the console, as shown in Figure 20-10 (note that the way our application was created, you will need to hit the Enter key to display each form of evidence).

Figure 20-10. Viewing the evidence of CarLibrary.dll