473,569 Members | 2,477 Online
Bytes | Software Development & Data Engineering Community
+ Post

Home Posts Topics Members FAQ

fgets() replacement

Hi all,

There was a recent thread in this group which talked about the
shortcomings of fgets(). I decided to try my hand at writing a
replacement for fgets() using fgetc() and realloc() to read a line of
arbitrary length. I know that the better programmers in this group could
write a more robust function, but here is my shot at it anyway.
I would appreciate people's comments on my fget_line() code below
(usage example included). Any constructive criticism welcome regarding
logic, design, style, etc. Thanks.

Paul

/* fget_line(): a function to read a line of input of arbitrary length.
*
* Arguments:
* 'in' -- the input stream from which data is wanted.
* 'buf' -- the address of a pointer to char. The read in results
* will be contained in this buffer after the fget_line returns.
* *** THE CALLER MUST FREE THIS POINTER ***
* 'sz' -- the caller can supply an estimate of the length of line to be
* read in. If this argument is 0, then fget_line() uses a
* default.
* 'validate' -- a user supplied callback function which is used to validate
* each input character. This argument may be NULL in which
* case no input validation is done.
*
* RETURN values:
* fget_line() on success: returns the number of bytes read
* realloc() related failure: returns -1 (#define'd below as ERROR_MEMORY)
* illegal input: returns -2 (#define'd below as ERROR_ILLEGAL_C HAR)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#ifndef USER_DIALOG_H /* if I am not using this as part of my
* 'user_dialog' utilities.
*/
#define LINE_LEN 80
#define DELIMITER '\n'
#define ERROR_MEMORY (-1)
#define ERROR_ILLEGAL_C HAR (-2)
#else
#include "user_dialo g.h"
#endif

int fget_line( FILE *in, char **buf, size_t sz, int (*validate)(int ) )
{
int n_bytes = 0; /* total number of bytes read or error flag */
size_t n_alloc = 0; /* number of bytes allocated */

unsigned int mult = 1;

char *tmp, *local_buffer = NULL;
int read_in;
*buf = NULL;

if( 0 == sz ) sz = LINE_LEN;

while( (read_in = fgetc( in )) != DELIMITER && read_in != EOF ) {
if( 0 == n_alloc ) {
n_alloc = sz * mult + n_bytes + 1;
tmp = realloc( local_buffer, n_alloc );
if ( NULL != tmp ) {
local_buffer = tmp;
mult++;
}
else {
local_buffer[n_bytes] = '\0';
*buf = local_buffer;
return ERROR_MEMORY;
}

}
if( NULL != validate ) {
if( 0 != validate( read_in ) ) {
local_buffer[n_bytes++] = read_in;
n_alloc--;
}
else {
local_buffer[n_bytes] = '\0';
*buf = local_buffer;
return ERROR_ILLEGAL_C HAR;
}
}

}

local_buffer[n_bytes] = '\0';

/* trim excess memory if any */
if( n_alloc > (size_t)n_bytes ) {
tmp = realloc( local_buffer, n_bytes );
if( NULL != tmp ) {
local_buffer = tmp;
}
}

local_buffer[n_bytes] = '\0';
*buf = local_buffer;
return n_bytes;
}

/* usage example */
int main( void )
{
char *line = NULL;
int ret_value;
size_t len;

fputs( "Enter a string: ", stdout );
fflush( stdout );

ret_value = fget_line( stdin, &line, 0, isalnum );
len = strlen( line );

fprintf( stdout, "fget_line( ) returned %d\nstrlen() returns %d bytes\n",
ret_value, len );
fprintf( stdout, "String is: \"%s\"\n", line );
free( line );
exit( EXIT_SUCCESS );
}


--
Paul D. Boyle
bo***@laue.chem .ncsu.edu
North Carolina State University
http://www.xray.ncsu.edu
Nov 14 '05 #1
20 5631

"Paul D. Boyle" <bo***@laue.che m.ncsu.edu> wrote
Hi all,

There was a recent thread in this group which talked about the
shortcomings of fgets(). I decided to try my hand at writing a
replacement for fgets() using fgetc() and realloc() to read a line of
arbitrary length.

Why not look a CB Falconer's "ggets()"?

<http://cbfalconer.home .att.net>

or email him on

Chuck F (cb********@yah oo.com)

Do you think your way of doing things is better?
Nov 14 '05 #2
Malcolm <ma*****@55bank .freeserve.co.u k> wrote:

: "Paul D. Boyle" <bo***@laue.che m.ncsu.edu> wrote
:> Hi all,
:>
:> There was a recent thread in this group which talked about the
:> shortcomings of fgets(). I decided to try my hand at writing a
:> replacement for fgets() using fgetc() and realloc() to read a line of
:> arbitrary length.
:>
: Why not look a CB Falconer's "ggets()"?

I did, but I wanted to try doing it (mostly for the heck of it) using
fgetc() and realloc().

: Do you think your way of doing things is better?

I don't see it as a matter of better. I wrote a function which did the
things I thought would make a safe and useful function, and I wanted
other people's opinion of what I had done. In particular, in fget_line(),
I provided a way to do some input validation. Was that a good and useful
design decision?

Paul

--
Paul D. Boyle
bo***@laue.chem .ncsu.edu
North Carolina State University
http://www.xray.ncsu.edu
Nov 14 '05 #3
Paul D. Boyle <bo***@laue.che m.ncsu.edu> wrote:
: if( NULL != validate ) {
: if( 0 != validate( read_in ) ) {
: local_buffer[n_bytes++] = read_in;
: n_alloc--;
: }
: else {
: local_buffer[n_bytes] = '\0';
: *buf = local_buffer;
: return ERROR_ILLEGAL_C HAR;
: }
: }
:
: }

: local_buffer[n_bytes] = '\0';

Naturally, I have to discover a little(?) *after* I post to
comp.lang.c. (grrr). I am missing an 'else' block to cover the case
where the 'validate' function pointer is NULL. The above code should be:

if( NULL != validate ) {
if( 0 != validate( read_in ) ) {
local_buffer[n_bytes++] = read_in;
n_alloc--;
}
else {
local_buffer[n_bytes] = '\0';
*buf = local_buffer;
return ERROR_ILLEGAL_C HAR;
}
}
else {
local_buffer[n_bytes++] = read_in;
n_alloc--;
}

}

local_buffer[n_bytes] = '\0';

/* and so on ... */

Paul
--
Paul D. Boyle
bo***@laue.chem .ncsu.edu
North Carolina State University
http://www.xray.ncsu.edu
Nov 14 '05 #4
Paul D. Boyle wrote:
Hi all,

There was a recent thread in this group which talked about the
shortcomings of fgets(). I decided to try my hand at writing a
replacement for fgets() using fgetc() and realloc() to read a line of
arbitrary length. I know that the better programmers in this group could
write a more robust function, but here is my shot at it anyway.
I would appreciate people's comments on my fget_line() code below
(usage example included). Any constructive criticism welcome regarding
logic, design, style, etc. Thanks.

Paul

/* fget_line(): a function to read a line of input of arbitrary length.
*
* Arguments:
* 'in' -- the input stream from which data is wanted.
* 'buf' -- the address of a pointer to char. The read in results
* will be contained in this buffer after the fget_line returns.
* *** THE CALLER MUST FREE THIS POINTER ***
* 'sz' -- the caller can supply an estimate of the length of line to be
* read in. If this argument is 0, then fget_line() uses a
* default.
* 'validate' -- a user supplied callback function which is used to validate
* each input character. This argument may be NULL in which
* case no input validation is done.
*
* RETURN values:
* fget_line() on success: returns the number of bytes read
* realloc() related failure: returns -1 (#define'd below as ERROR_MEMORY)
* illegal input: returns -2 (#define'd below as ERROR_ILLEGAL_C HAR)
*/
First criticism: The function does too much. This is, of
course, a matter of taste, but if the goal is "a replacement
for fgets()" I think the validate() business is extraneous.
(Even the `sz' parameter raises my eyebrows a little, albeit
not a lot.)

IMHO, a low-level library function should do one thing,
do it well, and do it in a manner that facilitates combining
it with other functions to create grander structures. Or as
my old fencing coach used to admonish me when I got overenthused
with tricky multiple-feint combinations: "Keep It Simple, Stupid."

By the way, you've described the purpose of validate() but
not how it is supposed to operate. What value(s) should it
return to cause fget_line() to take this or that action? To
find this out one must read the code of fget_line() -- and that,
I think, is a poor form for documentation.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#ifndef USER_DIALOG_H /* if I am not using this as part of my
* 'user_dialog' utilities.
*/
Here, I think, is another clue that the design leans too
far towards the baroque. In effect, USER_DIALOG_H and the
associated macros become part of the function's specification.
That specification now encompasses one return value encoding
three distinguishable states, four function arguments (two
with usage rules not expressible to the compiler), and five
macros. That strikes me as *way* too much for "a replacement
for fgets()."

("All right, Smarty Pants, how would *you* do it?")

Fair enough. Everybody, it seems, writes an fgets()
replacement eventually, and here's the .h text for mine:

char *getline(FILE *stream);
/*
* Reads a complete line from an input stream, stores
* it and a NUL terminator in an internal buffer, and
* returns a pointer to the start of the buffer. Returns
* NULL if end-of-file occurs before any characters are
* read, or if an I/O error occurs at any time, or if
* unable to allocate buffer memory (in the latter cases,
* any characters read before the I/O error or allocation
* failure are lost). If the argument is NULL, frees the
* internal buffer and returns NULL.
*
* A "complete line" consists of zero or more non-newline
* characters followed by a newline, or one or more non-
* newline characters followed by EOF.
*/

Now, I'm not saying that this is the only way to replace fgets().
I'm not even claiming it's the "best" way; some choices have been
made that could just as well have been made differently. The
point is to show that the specification can be a whole lot sparer
than for fget_line() and still be useful. (In fact, my original
getline() was sparer still: that "discard the buffer on a NULL
argument" gadget was warted on afterwards. It's ugly and very
little used; I may decide to go back to the simpler form.)

Now, on to the implementation itself.
#define LINE_LEN 80
#define DELIMITER '\n'
#define ERROR_MEMORY (-1)
#define ERROR_ILLEGAL_C HAR (-2)
Pointless parentheses.
#else
#include "user_dialo g.h"
#endif

int fget_line( FILE *in, char **buf, size_t sz, int (*validate)(int ) )
{
int n_bytes = 0; /* total number of bytes read or error flag */
size_t n_alloc = 0; /* number of bytes allocated */
This stands out as an Odd Thing: You're using a `size_t' to
keep track of the allocated buffer's size, but a mere `int' to
count the characters therein. Ah, yes: You also want to return
negative values to indicate errors! But that doesn't excuse the
type of `n_bytes', because the error codes are never stored in
it; they're always transmitted as part of a `return' statement.

... in connection with which, I wonder about the wisdom of
using an `int' as the function's value. Maybe a `long' would
be better? At any rate, if you feel you must use `int' you
should at least guard against lines longer than INT_MAX.
unsigned int mult = 1;

char *tmp, *local_buffer = NULL;
int read_in;
*buf = NULL;

if( 0 == sz ) sz = LINE_LEN;

while( (read_in = fgetc( in )) != DELIMITER && read_in != EOF ) {
Why fgetc() instead of getc()? In this instance they're
functionally equivalent, but getc() is likely to have less
overhead.
if( 0 == n_alloc ) {
n_alloc = sz * mult + n_bytes + 1;
tmp = realloc( local_buffer, n_alloc );
if ( NULL != tmp ) {
local_buffer = tmp;
mult++;
}
else {
local_buffer[n_bytes] = '\0';
Undefined behavior if the very first realloc() fails,
because `local_buffer' will still be NULL.
*buf = local_buffer;
return ERROR_MEMORY;
}

}
if( NULL != validate ) {
if( 0 != validate( read_in ) ) {
local_buffer[n_bytes++] = read_in;
n_alloc--;
}
else {
local_buffer[n_bytes] = '\0';
*buf = local_buffer;
return ERROR_ILLEGAL_C HAR;
}
}
You mentioned that `validate' could be given as NULL,
but somehow you didn't mention that doing so would suppress
*all* the input ...
}

local_buffer[n_bytes] = '\0';
Undefined behavior if you get EOF or DELIMITER on the
very first fgetc(), because `local_buffer' will still be
NULL.
/* trim excess memory if any */
if( n_alloc > (size_t)n_bytes ) {
I think this test is wrong: You've been decrementing
`n_alloc' with each character stored, so it is no longer
the size of the allocated area. (The record-keeping of
sizes in this function seems to involve a lot more work
than is really necessary. Two variables should suffice
for the job; fget_line() uses four.)
tmp = realloc( local_buffer, n_bytes );
if( NULL != tmp ) {
local_buffer = tmp;
}
}

local_buffer[n_bytes] = '\0';
What, again? Didn't we already do this, just a few
lines ago? Oh, wait, it's different this time:

Undefined behavior if the "trim excess" realloc()
*succeeds*, because it writes beyond the end of the memory
pointed to by `local_buffer'.
*buf = local_buffer;
return n_bytes;
}


General impressions: The design is overcomplicated , the
implementation is more intricate than even the complex
design requires, insufficient attention has been paid to
boundary conditions, and insufficient testing has been done.

"Simplify! Simplify!" -- H.D. Thoreau

--
Er*********@sun .com

Nov 14 '05 #5
"Paul D. Boyle" wrote:
Malcolm <ma*****@55bank .freeserve.co.u k> wrote:
: "Paul D. Boyle" <bo***@laue.che m.ncsu.edu> wrote
:>
:> There was a recent thread in this group which talked about the
:> shortcomings of fgets(). I decided to try my hand at writing a
:> replacement for fgets() using fgetc() and realloc() to read a
:> line of arbitrary length.
:>
: Why not look a CB Falconer's "ggets()"?

I did, but I wanted to try doing it (mostly for the heck of it)
using fgetc() and realloc().

: Do you think your way of doing things is better?

I don't see it as a matter of better. I wrote a function which
did the things I thought would make a safe and useful function,
and I wanted other people's opinion of what I had done. In
particular, in fget_line(), I provided a way to do some input
validation. Was that a good and useful design decision?


Thanks, Malcolm, for the kind words. The mail address you gave
will reach my spam trap. The URL is good.

My objective was to simplify the calling sequence as far as
possible. So I didn't look at yours in detail. Your validation
idea may well be useful in some areas. I don't think the
preliminary size estimate is worthwhile, but that is just an
opinion.

If I were creating a routine with such input validation, I would
probably simply pass it a routine to input a char, say "rdchar()",
returning EOF on error or invalid. I have grave doubts that such
will be useful in string input. For stream conversion to integer,
real, etc. the tests belong in the conversion function. Again,
IMO.

--
fix (vb.): 1. to paper over, obscure, hide from public view; 2.
to work around, in a way that produces unintended consequences
that are worse than the original problem. Usage: "Windows ME
fixes many of the shortcomings of Windows 98 SE". - Hutchison

Nov 14 '05 #6
In <c9**********@n ews8.svr.pol.co .uk> "Malcolm" <ma*****@55bank .freeserve.co.u k> writes:

"Paul D. Boyle" <bo***@laue.che m.ncsu.edu> wrote
Hi all,

There was a recent thread in this group which talked about the
shortcomings of fgets(). I decided to try my hand at writing a
replacement for fgets() using fgetc() and realloc() to read a line of
arbitrary length.

Why not look a CB Falconer's "ggets()"?


Because this is the kind of wheel most programmers prefer to reinvent
on their own. I'm still using scanf and friends, since I have yet to
write an application where getting arbitrarily long input lines makes
sense.

Dan
--
Dan Pop
DESY Zeuthen, RZ group
Email: Da*****@ifh.de
Nov 14 '05 #7
Dan Pop wrote:
In <c9**********@n ews8.svr.pol.co .uk> "Malcolm" <ma*****@55bank .freeserve.co.u k> writes:
"Paul D. Boyle" <bo***@laue.che m.ncsu.edu> wrote
Hi all,

There was a recent thread in this group which talked about the
shortcomin gs of fgets(). I decided to try my hand at writing a
replacemen t for fgets() using fgetc() and realloc() to read a line of
arbitrary length.


Why not look a CB Falconer's "ggets()"?

Because this is the kind of wheel most programmers prefer to reinvent
on their own. I'm still using scanf and friends, since I have yet to
write an application where getting arbitrarily long input lines makes
sense.


Arbitrarily long input lines are quite likely senseless.
But the problem's really the other side of the issue: Arbitrarily
*short* input lines -- meaning, "Input lines artificially truncated
to a length J. Random Programmer chose at compile time" -- are
not very sensible, either. (Off-topic aside: look up "curtation"
in "The Computer Contradictionar y," or Google for "MOZDONG." These
are limitations on output rather than input, but the idea is similar.)

The utility of an fgets() substitute/wrapper/whatever isn't
that one is now free to read "lines" of umpty-skillion gigabytes,
but that one can stop worrying about the line length altogether.

--
Er*********@sun .com

Nov 14 '05 #8
In <40************ **@sun.com> Eric Sosman <Er*********@su n.com> writes:
Dan Pop wrote:
In <c9**********@n ews8.svr.pol.co .uk> "Malcolm" <ma*****@55bank .freeserve.co.u k> writes:
"Paul D. Boyle" <bo***@laue.che m.ncsu.edu> wrote

Hi all,

There was a recent thread in this group which talked about the
shortcoming s of fgets(). I decided to try my hand at writing a
replaceme nt for fgets() using fgetc() and realloc() to read a line of
arbitrary length.

Why not look a CB Falconer's "ggets()"?
Because this is the kind of wheel most programmers prefer to reinvent
on their own. I'm still using scanf and friends, since I have yet to
write an application where getting arbitrarily long input lines makes
sense.


Arbitrarily long input lines are quite likely senseless.
But the problem's really the other side of the issue: Arbitrarily
*short* input lines -- meaning, "Input lines artificially truncated
to a length J. Random Programmer chose at compile time" -- are
not very sensible, either.


Most of the time, there are perfectly sensible limits that can be imposed
on the user input. As a matter of fact, I have yet to see a
counterexample. And when the user input is obtained interactively, the
user can be warned of those limits, in the text prompting him to
provide the input.

Of course, there is always the option of treating a line longer than the
limit as erroneous input and completely rejecting it, rather than
truncating it. It up to the programmer to decide what makes more sense
in the presence of nonsensical input...
The utility of an fgets() substitute/wrapper/whatever isn't
that one is now free to read "lines" of umpty-skillion gigabytes,
but that one can stop worrying about the line length altogether.


But it can be trivially abused into reading umpty-skillion gigabytes,
unless it imposes a limit ;-)

Dan
--
Dan Pop
DESY Zeuthen, RZ group
Email: Da*****@ifh.de
Nov 14 '05 #9

"Dan Pop" <Da*****@cern.c h> wrote in message
Most of the time, there are perfectly sensible limits that can be
imposed on the user input. As a matter of fact, I have yet to see a
counterexample. And when the user input is obtained interactively, > the user can be warned of those limits, in the text prompting him to provide the input.
What limit should be imposed on a line of BASIC? Most human-readable code is
under 100 characters, but some code might be machine-generated, and someone
might add a long string as a single line.
But it can be trivially abused into reading umpty-skillion gigabytes,
unless it imposes a limit ;-)

My BASIC interpreter uses a recursive-decent parser, so very long
expressions could overflow the stack. There is actually a case for imposing
a line limit, though of course stack size / usage is hard to determine.
Nov 14 '05 #10

This thread has been closed and replies have been disabled. Please start a new discussion.

Similar topics

5
7228
by: Rob Somers | last post by:
Hey all I am writing a program to keep track of expenses and so on - it is not a school project, I am learning C as a hobby - At any rate, I am new to structs and reading and writing to files, two aspects which I want to incorporate into my program eventually. That aside, my most pressing problem right now is how to get rid of the newline...
7
1763
by: William L. Bahn | last post by:
I recently challenged one of my students to figure out a way to determine if fgets() actually received the entire contents of the input string. When he was having trouble figuring it out even after I recommended he look carefully at the difference between the two cases when the input was short enough and when the input was too long, I...
0
1583
by: Paul Hsieh | last post by:
"Paul D. Boyle" <boyle@laue.chem.ncsu.edu> wrote: > There was a recent thread in this group which talked about the > shortcomings of fgets(). I decided to try my hand at writing a > replacement for fgets() using fgetc() and realloc() to read a line of > arbitrary length. I know that the better programmers in this group could > write a more...
35
9915
by: David Mathog | last post by:
Every so often one of my fgets() based programs encounters an input file containing embedded nulls. fgets is happy to read these but the embedded nulls subsequently cause problems elsewhere in the program. Since fgets() doesn't return the number of characters read it is pretty tough to handle the embedded nulls once they are in the buffer....
32
3855
by: FireHead | last post by:
Hello C World & Fanatics I am trying replace fgets and provide a equavivalant function of BufferedInputReader::readLine. I am calling this readLine function as get_Stream. In the line 4 where default_buffer_length is changed from 4 --24 code works fine. But on the same line if I change the value of default_buffer_length from 4 --10 and I...
20
10986
by: Xavoux | last post by:
Hello all... I can't remind which function to use for safe inputs... gets, fgets, scanf leads to buffer overflow... i compiled that code with gcc version 2.95.2, on windows 2000 char tmp0 = "ABCDEFGHI\0"; char buff; /* Input buffer. */ char tmp1 = "ABCDEFGHI\0";
24
2911
by: allpervasive | last post by:
hi all, this is reddy, a beginner to c lang,,here i have some problems in reading and modifying the contents of a file,, hope you can help to solve this problem. Here i attach the file to be modified and the program code. In the attached file below i just want to change the value of data(only float value) after the line 1 P V T 1 15 till 2...
285
8703
by: Sheth Raxit | last post by:
Machine 1 : bash-3.00$ uname -a SunOS <hostname5.10 Generic_118822-30 sun4u sparc SUNW,Sun-Fire-280R bash-3.00$ gcc -v Reading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/2.95.3/ specs gcc version 2.95.3 20010315 (release)
27
5077
by: Jeff | last post by:
Im trying to figure out why I cant read back a binary file correctly. I have the following union: #define BITE_RECORD_LEN 12 typedef union { unsigned char byte; struct { unsigned char type; /* 0 Type */ unsigned char sn; /* 1-3 Serial Number */
0
7721
marktang
by: marktang | last post by:
ONU (Optical Network Unit) is one of the key components for providing high-speed Internet services. Its primary function is to act as an endpoint device located at the user's premises. However, people are often confused as to whether an ONU can Work As a Router. In this blog post, we’ll explore What is ONU, What Is Router, ONU & Router’s main...
0
7633
by: Hystou | last post by:
Most computers default to English, but sometimes we require a different language, especially when relocating. Forgot to request a specific language before your computer shipped? No problem! You can effortlessly switch the default language on Windows 10 without reinstalling. I'll walk you through it. First, let's disable language...
1
7699
by: Hystou | last post by:
Overview: Windows 11 and 10 have less user interface control over operating system update behaviour than previous versions of Windows. In Windows 11 and 10, there is no way to turn off the Windows Update option using the Control Panel or Settings app; it automatically checks for updates and installs any it finds, whether you like it or not. For...
0
6320
agi2029
by: agi2029 | last post by:
Let's talk about the concept of autonomous AI software engineers and no-code agents. These AIs are designed to manage the entire lifecycle of a software development project—planning, coding, testing, and deployment—without human intervention. Imagine an AI that can take a project description, break it down, write the code, debug it, and then...
0
5247
by: conductexam | last post by:
I have .net C# application in which I am extracting data from word file and save it in database particularly. To store word all data as it is I am converting the whole word file firstly in HTML and then checking html paragraph one by one. At the time of converting from word file to html my equations which are in the word document file was convert...
0
3680
by: TSSRALBI | last post by:
Hello I'm a network technician in training and I need your help. I am currently learning how to create and manage the different types of VPNs and I have a question about LAN-to-LAN VPNs. The last exercise I practiced was to create a LAN-to-LAN VPN between two Pfsense firewalls, by using IPSEC protocols. I succeeded, with both firewalls in...
0
3669
by: adsilva | last post by:
A Windows Forms form does not have the event Unload, like VB6. What one acts like?
1
2130
by: 6302768590 | last post by:
Hai team i want code for transfer the data from one system to another through IP address by using C# our system has to for every 5mins then we have to update the data what the data is updated we have to send another system
0
971
bsmnconsultancy
by: bsmnconsultancy | last post by:
In today's digital era, a well-designed website is crucial for businesses looking to succeed. Whether you're a small business owner or a large corporation in Toronto, having a strong online presence can significantly impact your brand's success. BSMN Consultancy, a leader in Website Development in Toronto offers valuable insights into creating...

By using Bytes.com and it's services, you agree to our Privacy Policy and Terms of Use.

To disable or enable advertisements and analytics tracking please visit the manage ads & tracking page.