一个简单的程序允许您在文本文件中隐藏文本消息,如下所示:
- 短信被翻译成二进制代码;
- 在文件的每一行末尾,如果当前消息位等于 1,则在换行符之前写入一个空格字符,否则不写入任何内容。
假定文件中的行数足以包含该消息。还假设文件中最初没有以空格结尾的单行(用于明确解码)。
关于代码的一些注释:
- 代码中没有错误处理以简化程序。没有错误处理,而是使用了很多断言;
strdup
我为and编写了自己的对应物strrev
,因为它们都不包含在标准中。我分别命名了我的实现duplicateString
,reverseString
因为名称str*
是由标准保留的;- 我检查输入参数的有效性以防止使用内部 API 出错;
- 所有函数都被声明为静态的,因为它们在同一个翻译单元中。
- 输入文件以文本模式打开,而临时文件始终以二进制模式 (
w+b
) 打开。这会导致一些问题吗?
编译器
我正在使用带有以下键的 Clang:
-std=c11
-Weverything
-Wpedantic
-fsanitize=address
-D_CRT_SECURE_NO_WARNINGS
编译器只发出一个警告:
warning: implicit conversion turns floating-point number into integer: 'double' to 'size_t' (aka 'unsigned int') [-Wfloat-conversion]
const size_t newCapacity = ceil(s->capacity * DYNAMIC_STRING_GROW_FACTOR);
我看不出修复它的意义。
静态代码分析器
我还用 CppCheck 分析器检查了代码,它给出了一个错误:“内存泄漏:函数 stringToBinary 中的 ret ”:
char *stringToBinary(const char *s)
{
assert(s);
assert(strlen(s) > 0);
char *ret = calloc((strlen(s) + 1) * CHAR_BIT + 1, 1);
assert(ret);
for (size_t i = 0; s[i] != '\0'; i++)
strcat(ret, charToBinary(s[i]));
return strcat(ret, charToBinary('\0'));
}
但这似乎是一个误报,因为它res
是在函数中释放的hideMessage
:
char *msgBinary = stringToBinary(msg);
...
free(msgBinary);
实际上,代码是:
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <assert.h>
#define DYNAMIC_STRING_GROW_FACTOR 1.5
typedef struct DynamicString {
char *s;
size_t capacity;
size_t length;
} DynamicString;
static void createDynamicString(DynamicString *, size_t);
static void destroyDynamicString(DynamicString *);
static void increaseDynamicStringCapacity(DynamicString *);
static void appendCharToDynamicString(DynamicString *, char);
static void hideMessage(FILE *, const char *);
static char *extractMessage(FILE *);
static void copyFile(FILE *, FILE *);
static char *stringToBinary(const char *);
static char *charToBinary(char);
static char charFromBinary(const char *);
static char *reverseString(char *);
static char *duplicateString(const char *s);
int main(void)
{
FILE *file = fopen("file.txt", "r+");
assert(file);
hideMessage(file, "hello, world");
char *msg = extractMessage(file);
assert(msg);
puts(msg);
free(msg);
fclose(file);
}
/* The HIDEMESSAGE function
*
* The function hides text message into the file by that way: 1) the message
* converts to binary; 2) each bit of the converted message writes at the end
* of each file's line (right before the new-line character): if the bit
* is 1 then a space (' ') appends to the line otherwise it's nothing appends
* to the line.
*
* Assumes the the file does not contain any spaces right before new-line
* characters. Also assumes that the file has enough lines for storing
* the message.
*/
void hideMessage(FILE *f, const char *msg)
{
assert(f);
assert(msg);
assert(strlen(msg) > 0);
char *msgBinary = stringToBinary(msg);
assert(msgBinary);
FILE *tempFile = tmpfile();
assert(tempFile);
for (int ch, i = 0; (ch = fgetc(f)) != EOF;) {
if (msgBinary[i] && (ch == '\n'))
if (msgBinary[i++] == '1')
fputc(' ', tempFile);
fputc(ch, tempFile);
}
copyFile(f, tempFile);
free(msgBinary);
fclose(tempFile);
}
/* The EXTRACTMESSAGE function
*
* The function extracts message hidden by the HIDEMESSAGE function from
* the input file and returns a pointer to heap-allocated string which
* contains the message.
*/
char *extractMessage(FILE *f)
{
assert(f);
DynamicString msgBuffer;
createDynamicString(&msgBuffer, 128);
char charInBinary[CHAR_BIT + 1] = {0};
for (int prevCh = 0, ch, i = 0; (ch = fgetc(f)) != EOF; prevCh = ch) {
if (ch == '\n')
charInBinary[i++] = (prevCh == ' ') ? '1' : '0';
if (i % CHAR_BIT == 0 && i != 0) {
if (!strcmp(charInBinary, charToBinary('\0')))
break;
i = 0;
appendCharToDynamicString(&msgBuffer, charFromBinary(charInBinary));
}
}
char *ret = duplicateString(msgBuffer.s);
assert(ret);
destroyDynamicString(&msgBuffer);
return ret;
}
/* The CREATEDYNAMICSTRING function
*
* The function initializes a DynamicString passing by the first argument.
* The initial capacity of the string is passing by the second argument.
* Capacity is the max length of the string. At the same time length is
* current length of the string. Thus the function allocates capacity + 1
* bytes for the string (considering the null-character).
*
* The input pointer to DynamicString struture should be a valid pointer and
* capacity should be greater than 0.
*/
void createDynamicString(DynamicString *ret, size_t capacity)
{
assert(ret);
assert(capacity > 0);
ret->s = malloc(capacity + 1);
assert(ret->s);
ret->length = 0;
ret->capacity = capacity;
}
/* The APPENDCHARTODYNAMICSTRING function
*
* The function appends a character to the input DynamicString. If capacity of
* the string is not enough the function increases it.
*
* The input pointer to a DynamicString should be a valid pointer as well as
* its string buffer.
*/
void appendCharToDynamicString(DynamicString *s, char c)
{
assert(s);
assert(s->s);
if (s->length == s->capacity)
increaseDynamicStringCapacity(s);
s->s[s->length++] = c;
s->s[s->length] = '\0';
}
/* The INCREASEDYNAMICSTRINGCAPACITY function
*
* The function increases capacity of the input DynamicString. Grow factor
* is sets by a macro constant DYNAMIC_STRING_GROW_FACTOR.
*
* The input pointer to a DynamicString struture should be a valid pointer
* as well as its string buffer.
*/
void increaseDynamicStringCapacity(DynamicString *s)
{
assert(s);
assert(s->s);
const size_t newCapacity = ceil(s->capacity * DYNAMIC_STRING_GROW_FACTOR);
s->s = realloc(s->s, newCapacity + 1);
assert(s->s);
s->capacity = newCapacity;
}
/* The DESTROYDYNAMICSTRING function
*
* The function destroys the input DynamicString. It frees the string buffer
* of the input DynamicString.
*
* The input pointer to a DynamicString should be a valid pointer as well as
* its string buffer.
*/
void destroyDynamicString(DynamicString *s)
{
assert(s);
assert(s->s);
free(s->s);
}
/* The COPYFILE function
*
* The function copies all the contents of src to dest. Both arguments should
* be valid pointers. dest should be open for writing, src should be open
* for reading. The function does not close the files. The both file cursor
* position sets to the beginning.
*/
void copyFile(FILE *dest, FILE *src)
{
assert(dest);
assert(src);
rewind(dest);
rewind(src);
for (int ch; (ch = fgetc(src)) != EOF;)
fputc(ch, dest);
rewind(dest);
rewind(src);
}
/* The CHARFROMBINARY function
*
* The function converts the input string returned by the CHARTOBINARY function
* to a character.
*
* The input string should be a valid null-terminated string and its length
* should be greater 0.
*
* charFromBinary(charToBinary(c)) == c
*/
char charFromBinary(const char *s)
{
assert(s);
assert(strlen(s) > 0);
char ret = 0;
unsigned int p = 1;
for (size_t i = strlen(s); i-- > 0; p *= 2)
if (s[i] == '1')
ret += p;
return ret;
}
/* The STRINGTOBINARY function
*
* The function converts the input string to binary form and returns a pointer
* to heap-allocated null-terminated string. Null-terminator of the input
* string also converts to binary form and appends to the result. The caller
* should free memory allocated for the output string.
*
* The input string should be a valid null-terminated string and its length
* should be greater 0.
*
* stringToBinary("cat") => "01100011011000010111010000000000"
* stringToBinary("dog") => "01100100011011110110011100000000"
* stringToBinary("R\0") => "0101001000000000"
*/
char *stringToBinary(const char *s)
{
assert(s);
assert(strlen(s) > 0);
char *ret = calloc((strlen(s) + 1) * CHAR_BIT + 1, 1);
assert(ret);
for (size_t i = 0; s[i] != '\0'; i++)
strcat(ret, charToBinary(s[i]));
return strcat(ret, charToBinary('\0'));
}
/* The CHARTOBINARY function
*
* The function converts value of the input character to binary form and
* returns a pointer to the static null-terminated string which contains
* the result. The result contains leading zeroes.
*
* charToBinary(100) => "01100100"
* charToBinary('A') => "01000001"
* charToBinary('\0') => "00000000"
*/
char *charToBinary(char c)
{
static char ret[CHAR_BIT + 1];
memset(ret, '0', sizeof ret);
ret[sizeof ret - 1] = '\0';
for (size_t i = 0; c; i++) {
ret[i] = (c % 2) ? '1' : '0';
c /= 2;
}
return reverseString(ret);
}
/* The REVERSESTRING function
*
* The input string should be a valid pointer to a null-terminated string.
* If the input string is empty the function does noting.
*
* reverseString("hello") => "olleh"
* reverseString("a") => "a"
* reverseString("") => "a"
*/
char *reverseString(char *s)
{
assert(s);
char *begin = s;
char *end = s + strlen(s) - 1;
for (; begin < end; begin++, end--) {
const char t = *begin;
*begin = *end;
*end = t;
}
return s;
}
/* The DUPLICATESTRING function
*
* The function returns a pointer to a heap-allocated string, which is a
* duplicate of the input string. The returned pointer can be passed to the
* free function.
*/
char *duplicateString(const char *s)
{
assert(s);
assert(strlen(s) > 0);
char *copy = malloc(strlen(s) + 1);
assert(copy);
return strcpy(copy, s);
}
更新
重写版本(Github Gist)(由CodeReview的 @avp 和 @Edward 审查)。
PS我用蹩脚的英文写评论,我希望至少有些东西会很清楚:)
@eanmos,阅读 git 中的代码。在我看来,没关系,比第一个版本更好。
一些笔记。
1) Я не нашел, как кодируется конец сообщения при записи в файл. Такое впечатление, что его надо будет обнаруживать по нулевым байтам в конце, которые неявно появятся после записи в достаточно длинный файл.
Может его стоит обозначить явно, например, записав 2 пробела в конец первой строки, идущей после сообщения (перед копированием остатка файла)?
2) При разборе
argc/argv
вmain()
надо проверять количество аргументов для--hide
Иначе свалится в
fopen(argv[3], ...)
3) Наверное стоит проверять
assert(ferror(infile) == 0);
после чтенияEOF
, а также проверитьfclose(outfile)
на предмет IO errors (реально запись на диск может и не поймать, но все же, раз уж вы результатmalloc/realloc
проверяете ..., то эту паранойю надо держать до конца -)).4) Очевидно, что после
duplicateString(msgBuffer.s)
вextractMessage()
память надо либо освобождать, либо вместо ее вызова можно просто возвращать весь выделенныйmalloc/realloc
-ами буфер(или если хотите фрагментировать память -), то можно вернуть
realloc(msgBuffer.s, msgBuffer.length + 1)
)Еще одно.
Мелочь, но умножение на 1.5 можно сделать в целых:
n * 2 - n / 2
(или сдвигами, если компайлер плохо оптимизирует, то:(n << 1) - (n >> 1)
).И еще мелочь. В принципе, в
char
может быть не 8 бит, аCHAR_BIT
(из<limits.h>
) и тогда 0x80, которую вы используете в цикле по битам, не играет.Я бы рекомендовал просто
for (int j = 0; j < CHAR_BIT; j++) {if (encodeBit(infile, outfile, *msg & (1 << j)) == EOF) ... }
(да, я понимаю, что тут порядок бит в байте обратный вашему, но мне он кажется более естественным).
Возвращаясь к моему замечанию 3) о
duplicateString()
. В общем, я имел в виду, что она совсем не нужна.Но если бы она понадобилась, то в контексте использования
struct DynamicString
вместо вызоваstrlen()
в ней надо использовать.length
из структурыDynamicString
и уж по любомуmemcpy()
вместоstrcpy()
, поскольку уже в вызовеmalloc()
размер строки известен.