The Daily WTF

Subscribe to The Daily WTF feed
Curious Perversions in Information Technology
Updated: 2 hours 23 min ago

CodeSOD: The Default Path

Thu, 2024-02-22 07:30

I've had the misfortune to inherit a VB .Net project which started life as a VB6 project, but changed halfway through. Such projects are at best confused, mixing idioms of VB6's not-quite object oriented programming with .NET's more modern OO paradigms, plus all the chaos that a mid-project lanugage change entails. Honestly, one of the worst choices Microsoft ever made (and they have made a lot of bad choices) was trying to pretend that VB6 could easily transition into VB .Net. It was a lie that too many managers fell for, and too many developers had to try and make true.

Maurice inherited one of these projects. Even worse, the project started in a municipal IT department then was handed of to a large consulting company. Said consulting company then subcontracted the work out to the lowest bidder, who also subcontracted out to an even lower bidder. Things spiraled out of control, and the resulting project had 5,188 GOTO statements in 1321 code files. None of the code used Option Explicit (which requires you to define variables before you use them), or Option Strict (which causes errors when you misuse implicit data-type conversions). In lieu of any error handling, it just pops up message boxes when things go wrong.

Private Function getDefaultPath(ByRef obj As Object, ByRef Categoryid As Integer) As String Dim sQRY As String Dim dtSysType As New DataTable Dim iMPTaxYear As Integer Dim lEffTaxYear As Long Dim dtControl As New DataTable Const sSDATNew As String = "NC" getDefaultPath = False sQRY = "select TAXBILLINGYEAR from t_controltable" dtControl = goDB.GetDataTable("Control", sQRY) iMPTaxYear = dtControl.Rows(0).Item("TAXBILLINGYEAR") 'iMPTaxYear = CShort(cmbTaxYear.Text) If goCalendar.effTaxYearByTaxYear(iMPTaxYear, lEffTaxYear) Then End If sQRY = " " sQRY = "select * from T_SysType where MTYPECODE = '" & sSDATNew & "'" & _ " and msystypecategoryid = " & Categoryid & " and meffstatus = 'A' and " & _ lEffTaxYear & " between mbegTaxYear and mendTaxYear" dtSysType = goDB.GetDataTable("SysType", sQRY) If dtSysType.Rows.Count > 0 Then obj.Text = dtSysType.Rows(0).Item("MSYSTYPEVALUE1") Else obj.Text = "" End If getDefaultPath = True End Function

obj is defined as Object, but is in fact a TextBox. The function is called getDefaultPath, which is not what it seems to do. What does it do?

Well, it looks up the TAXBILLINGYEAR from a table called t_controltable. It runs this query by using a variable called goDB which thanks to hungarian notation, I know is a global object. I'm not going to get too upset about reusing a single database connection as a global object, but it's definitely hinting at other issues in the code.

We check only the first row from that query, which shows a great deal of optimism about how the data is actually stored in the table. While there are many ways to ensure that tables store data in sorted order, an ORDER BY clause would go a long way to making the query clear. Also, since we only need one row, a TOP N or some equivalent would be nice.

Then we use a global calendar object to do absolutely nothing in our if statement.

That leads us to the second query, which at least Categoryid is an integer and lEffTaxYear is a long, which makes this potential SQL injection not really an issue. We run that query, and then check the number of rows- a sane check which we didn't do for the last query- and then once again, only look at the first row.

I'm going to note that MSYSTYPEVALUE1 may or may not be a "default path", but I certainly have no idea what they're talking about and what data this function is actually getting here. The name of the function and the function of the function seem disconnected.

In any case, I especially like that it doesn't return a value, but directly mutates the text box, ensuring minimal reusability of the function. It could have returned a string, instead.

Speaking of returning strings, that gets us to our final bonus. It does return a string- a string of "True", using the "delightful" functionName = returnValue syntax. Presumably, this is meant to represent a success condition, but it only ever returns true, concealing any failures (or, more likely, just bubbling up an exception). The fact that the return value is a string hints at another code smell- loads of stringly typed data.

The "good" news is that what it took layers of subcontractors to destroy, Maurice's team is going to fix by June. Well, that's the schedule anyway.

[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!
Categories: Computer

CodeSOD: Route to Success

Wed, 2024-02-21 07:30

Imagine you're building a PHP web application, and you need to display different forms on different pages. Now, for most of us, we'd likely be using some framework to solve this problem, but even if we weren't, the obvious solution of "use a different PHP file for each screen" is a fairly obvious solution.

Dare I say, too obvious a solution?

What if we could have one page handle requests for many different URLs? Think of the convenience of having ONE file to run your entire application? Think of the ifs.

if( substr( $_SERVER['REQUEST_URI'], strrpos($_SERVER['REQUEST_URI'], "=" ) + 1 ) == "request" ) { echo "<form name=\"request\" action=\"\" method=\"post\" enctype=\"multipart/form-data\" onsubmit=\"return validrequest();\">\n"; } else if( substr( $_SERVER['REQUEST_URI'], strrpos($_SERVER['REQUEST_URI'], "=" ) + 1 ) == "response" ) { echo "<form action=\"\" method=\"post\" onsubmit=\"return validresponse()\">\n"; } else if( substr( substr( $_SERVER['REQUEST_URI'], stripos($_SERVER['REQUEST_URI'], "=" ) + 1 ), 0, 7 ) == "respond" ) { echo "<form name=\"respond\" action=\"\" method=\"post\" enctype=\"multipart/form-data\" onsubmit=\"return validresponse();\">\n"; } else if( substr( substr( $_SERVER['REQUEST_URI'], stripos($_SERVER['REQUEST_URI'], "=" ) + 1 ), 0, 6 ) == "upload" ) { echo "<form name=\"upload\" method=\"post\" action=\"\" enctype=\"multipart/form-data\">\n"; } else if( substr( substr( $_SERVER['REQUEST_URI'], stripos($_SERVER['REQUEST_URI'], "=" ) + 1 ), 0, 8 ) == "showitem" ) { echo "<form name=\"showitem\" action=\"\" method=\"post\" enctype=\"multipart/form-data\">\n"; } else if( substr( substr( $_SERVER['REQUEST_URI'], stripos($_SERVER['REQUEST_URI'], "=" ) + 1 ), 0, 7 ) == "adduser" ) { echo "<form name=\"adduser\" action=\"\" method=\"post\" onsubmit=\"return validadduser();\">\n"; } else if( substr( substr( $_SERVER['REQUEST_URI'], stripos($_SERVER['REQUEST_URI'], "=" ) + 1 ), 0, 8 ) == "edituser" ) { echo "<form name=\"adduser\" action=\"\" method=\"post\" onsubmit=\"return validedituser();\">\n"; } else { echo "<form action=\"\" method=\"post\">\n"; }

Someone reinvented routing, badly. We split the requested URL on an =, so that we can compare the tail of the string against one of our defined routes. Oops, no, we don't split, we take a substring, which means we couldn't have a route upload_image or showitems, since they'd collide with upload and showitem.

And yes, you can safely assume that there are a bunch more ifs that control which specific form fields get output.

[Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.
Categories: Computer

CodeSOD: Merge the Files

Tue, 2024-02-20 07:30

XML is, arguably, an overspecified language. Every aspect of XML has a standard to interact with it or transform it or manipulate it, and that standard is also defined in XML. Each specification related to XML fits together into a soup that does all the things and solves every problem you could possibly have.

Though Owe had a problem that didn't quite map to the XML specification(s). Specifically, he needed to parse absolutely broken XML files.

bool Sorter::Work() { if(this->__disposed) throw gcnew ObjectDisposedException("Object has been disposed"); if(this->_splitFiles) { List<Document^>^ docs = gcnew List<Document^>(); for each(FileInfo ^file in this->_sourceDir->GetFiles("*.xml")) { XElement ^xml = XElement::Load(file->FullName); xml->Save(file->FullName); long count = 0; for each(XElement^ rec in xml->Elements("REC")) { if(rec->Attribute("NAME")->Value == this->_mainLevel) count++; } if(count < 2) continue; StreamReader ^reader = gcnew StreamReader(file->OpenRead()); StringBuilder ^sb = gcnew StringBuilder("<FILE NAME=\"blah\">"); bool first = true; bool added = false; Regex ^isRecOrFld = gcnew Regex("^\\s+\\<[REC|FLD].*$"); Regex ^isEndOfRecOrFld = gcnew Regex("^\\s+\\<\\/[REC|FLD].*$"); Regex ^isMainLevelRec = gcnew Regex("^\\s+\\<REC NAME=\\\""+this->_mainLevel+"\\\".*$"); while(!reader->EndOfStream) { String ^line = reader->ReadLine(); if(!isRecOrFld->IsMatch(line) && !isEndOfRecOrFld->IsMatch(line)) continue; if(isMainLevelRec->IsMatch(line) && !String::IsNullOrEmpty(sb->ToString()) && !first) { sb->AppendLine("</FILE>"); XElement^ xml = XElement::Parse(sb->ToString()); String ^key = String::Empty; for each(XElement ^rec in xml->Elements("REC")) { key = this->findKey(rec); if(!String::IsNullOrEmpty(key)) break; } docs->Add(gcnew Document(key, gcnew XElement("container", xml))); sb = gcnew StringBuilder("<FILE NAME=\"blah\">"); first = true; added = true; } sb->AppendLine(line); if(first && !added) first = false; if(added) added = false; } delete reader; file->Delete(); } int i = 1; for each(Document ^doc in docs) { XElement ^splitted = doc->GetData()->Element("FILE"); splitted->Save(Path::Combine(this->_sourceDir->FullName, this->_docPrefix + "_" + i++ + ".xml")); delete splitted; } delete docs; } List<Document^>^ docs = gcnew List<Document^>(); for each(FileInfo ^file in this->_sourceDir->GetFiles(String::Format("{0}*.xml", this->_docPrefix))) { XElement ^xml = XElement::Load(file->FullName); String ^key = findKey(xml->Element("REC")); // will always be first element in document order Document ^doc = gcnew Document(key, gcnew XElement("data", xml)); docs->Add(doc); file->Delete(); } List<Document^>^ sorted = MergeSorter::MergeSort(docs); XElement ^sortedMergedXml = gcnew XElement("FILE", gcnew XAttribute("NAME", "MergedStuff")); for each(Document ^doc in sorted) { sortedMergedXml->Add(doc->GetData()->Element("FILE")->Elements("REC")); } sortedMergedXml->Save(Path::Combine(this->_sourceDir->FullName, String::Format("{0}_mergedAndSorted.xml", this->_docPrefix))); // returning a sane value return true; }

This is in the .NET dialect of C++, so the odd ^ sigil is a handle to a garbage collected object.

There's a lot going on here. The purpose of this function is to possibly split some pre-merged XML files into separate XML files, and then take a set of XML files and merge them back together (properly sorted).

So we start by confirming that this object hasn't been disposed, and throwing an exception if it has. Then we try and split.

To do this, we search the directory for "*.xml", and then we… load the file and then save the file? The belief about this code is that it corrects the whitespace, because later on we require some whitespace- but the .NET XML writer doesn't add whitespace, only preserve it, so I suspect this line isn't necessary- or at least shouldn't be. I can envision a world where this somehow makes the code work for reasons that are best not thought about.

Owe writes, to the preceding developers: "Thanks guys, I really appreciate this!"

Now, since we're iterating across an entire directory of XML files, some of the files have been pre-merged (and need to be unmerged), and others haven't been merged at all. How do we tell them apart? We find every element named "REC", and check if it's "NAME" attribute is equivalent to our _mainLevel value. If there are at least two such element, we know that this file has been premerged and thus needs to be unmerged.

Owe writes: "Thanks guys, I really appreciate this!"

And then we get into the dreaded parse XML with regex phase. This is done because the XML files aren't actually valid XML. So it's a mix of string operations and regex matches to try and interpret the data. And remember that whitespace that we thought we required back when we wrote the documents out? Well here's why: our regexes are matching on whitespace.

Owe writes: "Thanks guys, I really appreciate this!"

Once we've constructed all the documents in memory, we can then dump them out to a new set of files. And then, once that's done, we can reopen those files, because now the merging happens. Here we find all the "REC" elements and build new XML documents based off of them. Then a MergeSorter::MergeSort function actually does the merging- and honestly, I dread to think about what that looks like.

The merge sorter sorts the documents, but we actually want to output one document with the elements in that sorted order, so we create one last XML document, iterate across all our sorted document fragments, and then inject the "REC" elements into the output.

Owe writes: "Thanks guys, I really appreciate this!"

While the code and the entire process here is terrible, the core WTF is the "we need to store our XML with the elements sorted in a specific order". That's not what XML is for. But obviously, they don't know what XML is for, since they're doing things in their documents that can't successfully be parsed by an XML parser. Or, perhaps more accurately, they couldn't figure out how to parse as XML, hence the regexes and string munging.

Were the documents sensible, this whole thing could probably have been solved with some fairly straightforward (by XML standards) XQuery/XSLT operations. Instead, we have this. Thanks guys, I really appreciate this.

[Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.
Categories: Computer

Representative Line: From a String Builder

Mon, 2024-02-19 07:30

Inheritance is one of those object-oriented concepts that creates a lot of conflicts. You'll hear people debating what constitutes an "is-a" versus a "has-a" relationship, you'll hear "favor composition of inheritance", you'll see languages adopt mix-in patterns which use inheritance to do composition. Used well, the feature can make your code cleaner to read and easier to maintain. Used poorly, it's a way to get your polymorphism with a side of spaghetti code.

Greg was working on a medical data application's front end. This representative line shows how they use inheritance:

public class Patient extends JavascriptStringBuilder

Greg writes: "Unfortunately, the designers were quite unacquainted with newfangled ideas like separating model objects from the UI layer, so they gave us this gem."

This, of course, was a common pattern in the application's front end. Many data objects inherited from string builder. Not all of them, which only helped to add to the confusion.

As for why? Well, it gave these objects a "string" function, which they could override to generate output. You want to print a patient to the screen? What could be easier than calling patient.string()?

[Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.
Categories: Computer

Error'd: Mirror mirror

Fri, 2024-02-16 07:30

An abstitution, an assortment, time travel, bad language, and an error'd.

First up, Jeremy Pereira pushes the boundaries of this column by sharing something right. "Sort of an anti-WTF. It took them 44 minutes to realise they'd made a boo-boo." They probably were notified, but it's still pretty good time to repair. Especially considering the issues we know of that last for years and years.

 

Snecod, Darren S. interjects "We had this bit of marketing from SmartSheet. The irony is that is was all about how to use tags to customise data or views."

 

Apeman Ford Prefect of Collation hooted "Perfectly (machine-)translated , except for sorting. German is called Deutsch auf Deutsch... Spanish might be Espagnol. What may be the local name of Welsh?" I know what it is, in Welsh, and it does start with C, but I don't think the original language of this list is actually Cymraeg.

 

While Wordsmith Sebas whined "Don't see how this is related." It's not.

 

Finally, junge Jurgen reflects on wording we all took for granted, until just now. "When an upload process failed, it told me to check the logs for more details. This is what I found: Prove that logs can also be recursive..."

 

[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!
Categories: Computer

CodeSOD: While Nothing

Thu, 2024-02-15 07:30

José received a bit of VB .Net UI code that left him scratching his head.

While IsNothing(Me.FfrmWait) If Not IsNothing(Me.FfrmWait) Then Exit While End If End While

While a form doesn't exist, if the form does exist, break out of the loop. I suspect this was intended as a busy loop, waiting for some UI element to be ready. Because busy loops are always the best way to wait for things.

But even with the busy loop, the If is a puzzle- why break out of the loop early, when the loop condition is going to break itself? Did they just not understand how loops work?

Now, the last thing to note is the variable naming conventions. frmMyForm is a common convention in WinForms programming, a little bit of Hungarian notation to tell you what the UI element actually is. But what's that *leading F there? Are they… doing a double Hungarian? Does it tell us that this is a form? Or maybe they're trying to tell us that it's a field of the class? Or maybe a developer was saying "F my life"?

We'd all be better off if this code were nothing.

[Advertisement] BuildMaster allows you to create a self-service release management platform that allows different teams to manage their applications. Explore how!
Categories: Computer

A Stalled Upgrade

Wed, 2024-02-14 07:30

It was time to start developing version 2 of Initech's flagship software product. This meant planning meetings. So many planning meetings.

The most important one, for the actual development team, was the user story meeting. The core of these meetings was a few folks from the programming team, including Steve, the director of architecture, Brian, and a variety of product owners, responsible for different segments of the overall product.

As a group, they'd review the user stories, and approve them. Once they were approved for development, work would begin.

The meetings were difficult to schedule, because of the number of stakeholders, and they were viewed as a checkpoint- you can't start implementing features until the product owner has walked through the user story with the team, but they were also a priority.

At the meeting, product owner Renee started walking the team through some of the features she owned. "So, here's my user, Fred Flinstone." Her slide popped up an image of the animated character next to a set of bullet points describing the feature. "He needs to add an item into inventory, so that it can actually be sold. To do that…"

Renee walked them through the details of the feature. Heads nodded around the table- it was a pretty straightforward CRUD application. It was good that the product owner walked them through a few workflow things unique to Initech's product, but there wasn't anything particularly shocking.

Renee advanced to the next slide. "And now, Fred needs to run a report. This report needs to…"

Again, Renee walked them through the key fields that needed to be on the report, how the report was triggered, what the reasonable filtering options would be.

"Any questions?"

Brian, the director, looked thoughtful for a moment, and then said, "I think I do. I can't really see a reason why one person would want to both add items to inventory- a receiving job- and run reports on inventory consumption. There's no reason someone doing the task would also need to run the report. I can't accept your user stories until you change one of the users to be a different person."

"What? The name doesn't matter," Renee protested.

"We should probably just stick a pin in this and pick up when we can reschedule another meeting. Thank you everyone," Brian said. He grabbed his laptop, stood, and left the meeting. Everyone sat there for a moment, realized the meeting was actually over, and followed him out a few minutes later.

That afternoon, Steve finally managed to find Brian. "What the heck was that?"

Brian sighed. "Look, the entire development team is swamped, you know it. We don't have bandwidth to take on new work. Tracey should have that priority-one bug done in a few days, and maybe once that's done we can start putting resources on the the version 2 project. For now, we need to stall, and that was the only thing I could think of."

[Advertisement] Keep the plebs out of prod. Restrict NuGet feed privileges with ProGet. Learn more.
Categories: Computer

Representative Line: A Memory of Strings

Tue, 2024-02-13 07:30

As we saw yesterday, padding can be hard.

Jasmine received a set of tickets all complaining that the file download portion of an ASP .Net application took too long. Users frequently had to wait for up to 15 minutes before their browser finished downloading the output of the web application.

This particular module generated the file by doing a set of Response.Writes to build the file as part of the HTTP response. This particular line ran 200,000 times in the process of creating the file:

myResponse.Write("P;P#,##0." + new string('0', 2) + "_);;[Red]\\(#,##0." + new string('0', 2) + "\\)\n");

I'm not entirely clear what they're writing out here- it looks like some kind of format template. But the contents don't really matter. The string concatenation operations look like they're handling decimal places- 0.00. Given that the new string is hard coded to two zeros, there's no need for string concatenation, they could have just put the 00 right in the output string.

But for fun, let's count the strings that this operation creates, noting that strings are immutable in C#, so each concatenation creates a new string.

First, let's give credit to the literals- C# is smart, and string literals get only one instance created, so we can ignore those. The new string operation creates two new strings. Then each concatenation creates a new string- four.

That's a total of 6 new string instances, which isn't a lot, but given that if they just did a literal there'd be 0 new string instances, it's still a notable increase. But this line executes in a loop, 200,000 times, meaning we're spamming 1,200,000 string instances in each execution of that loop. And this line is a representative line; the whole loop has many such lines, allowing this code to quite effectively stress test the garbage collector.

Without a single change to the logic, and just switching to string formatting and string builders, Jasmine was able to get this particular file generation down from 15 minutes to closer to a few seconds.

[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!
Categories: Computer

CodeSOD: Timestamped File Name

Mon, 2024-02-12 07:30

It's been a minute since some bad date handling code. Wesley inherited this C# blob which exists to generate timestamped filenames.

// filename format = yyyyMMddhhmmss_<bipad>.dat char c0 = '0'; this.m_FileName = DateTime.Now.Year.ToString() + new String(c0, 2-DateTime.Now.Month.ToString().Length) + DateTime.Now.Month.ToString() + new String(c0, 2-DateTime.Now.Day.ToString().Length) + DateTime.Now.Day.ToString() + new String(c0, 2-DateTime.Now.Hour.ToString().Length) + DateTime.Now.Hour.ToString() + new String(c0, 2-DateTime.Now.Minute.ToString().Length) + DateTime.Now.Minute.ToString() + new String(c0, 2-DateTime.Now.Second.ToString().Length) + DateTime.Now.Second.ToString() + "_" + new String(c0, 5-publication.Bipad.ToString().Length) + publication.Bipad.ToString() + ".dat";

The new String(c0, n-myString.Length) pattern is a perfectly cromulent way to pad a string- that is to say, completely wrong, even though it somehow communicates its intent. Even if you refuse to use a built-in pad method, writing a pad method would be good. Of course, there's no need to do any of this, as C# has date formatters that will handle generating the string for you in a single line.

Still, I have to give this credit- I had to stop and ask myself "what the hell is this even doing?" It was only for a second, but I needed to read the code twice.

[Advertisement] BuildMaster allows you to create a self-service release management platform that allows different teams to manage their applications. Explore how!
Categories: Computer

Error'd: ROUS

Fri, 2024-02-09 07:30

I don't think they exist.

Choosy Chris P. wants more choices, carping "You keep using that word, I don't think it means what you think it means."

 

Romantic Ryan S. rued buying holiday flowers, regretting "A sure fire way to end the love and laugh on valentine's day this year."

 

Nonlocal lothario romeo reminds us that it's important to validate your inputs. "After entering nonUSA format of data amount you get NaN for annual price and the same for discounted price"

 

From beyond the realm of space and time, Stewart sent the following. "Yet more proof of how hard dates are. Even the UKs largest broadband provider can't get it right."

 

Trader Tim R. thinks he found an angle. "While having a look at the $ to Euro exchange rate, I was surprised to see Google had such accurate predictions for 500 years into the future," he says. I say "how do you know they're accurate?"

 

[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!
Categories: Computer

A Bit About the HP3000

Thu, 2024-02-08 07:30

Today's anonymously submitted story is a case where the WTF isn't the code itself, per se. This arguably could be a CodeSOD, and we'll get to the code, but there's so much more to the story.

Our submitter, let's call them Janice, used to work for a financial institution with a slew of legacy systems. One such system was an HP3000 minicomputer. "Mini", of course, meant "refrigerator sized".

The HP3000 itself is an interesting, if peripheral story, because it's one of the tales of a product launch going incredibly wrong. Let's talk a little history.

We start with the HP2100 in 1966, which Hewlett Packard did nothing to design, and instead purchased the company that designed it. The core innovation of the HP2100 was that it was architecturally similar to a PDP-8, but supported full 16-bit memory, instead of PDP's 12-bit.

HP didn't really know what they had bought- they marketed it as a "test and instrumentation" system, and were surprised when businesses purchased it for back office operations. They ended up with one of the most popular minicomputers for office use, despite it not being designed for that purpose.

Thus began the projects "Alpha" and "Omega". Alpha was a hardware refresh of the 2100, with a better memory model. Omega was a ground-up redesign for 32-bit memory, which would allow it to support a whopping 4MB of RAM. There was just one problem with the Omega design: they didn't have funding to actually finish it. The project was killed in 1970, which threw some of the staff into "wear black armbands to work" levels of mourning.

Unfortunately, while work was done on Omega, the scope of Alpha crept, which resulted in another project management wasn't sure could be delivered. But the market was there for a time-sharing minicomputer, so they pressed on despite the concerns.

The HP2000-line, had time sharing system that used multiple processors. There was a front-end processor which handled user interactions. Then there was the actual CPU, which ran programs. This meant that time-sharing was simplified- the CPU just ran programs in a round-robin fashion, and didn't have to worry about pesky things like user inputs. Essentially, it was really just a batch processing system with a multi-user front-end.

The designers of Alpha wanted to support full multiprogramming, instead of this hybrid-ish model. But they also needed to support traditional batch processing, as well as real-time execution. So the team split up to build the components of the "Multi-Programming Executive" module, which would allow all of these features.

The Alpha, which was still 16-bit, didn't have the luxurious 4MB of RAM- it had 128kB. The MPE used much more memory than 128kB. This lead to a massive crunch as the programmers worked to shrink MPE into something usable, while marketing looked at the deadlines and said, "We were supposed to be selling this thing months ago!"

The result was a massive war between engineering and marketing, where marketing gave customers promises about what the performance would be, engineering told marketing what the actual performance would be (significantly worse than what marketing was promising), and then management would demand engineering "prove" that marketing's over-promises could be met.

The initial ship-date was November, 1972, and by god, they shipped on time. Nothing actually worked, but they shipped. The first computer out the door was returned almost immediately. It could only handle two simultaneous users before slowing to a crawl, and crashed every ten minutes. By December, HP had gotten that to "crashes every two hours". They kept shipping machines even as they had to cut features and reliability promises.

Those frequent crashes also concealed another bug: after running for 24 days, the HP3000's clock would overflow (2^31 milliseconds) and the clock would magically reverse by 25 days. As one sysop of a purchased HP3000 put it: "The original designers of MPE [apparently] never thought the OS would stay up for 25+ days in a row".

After a bunch of management shuffling, the titular Packard of Hewlett Packard sent a memo: production was stopping and all sold computers were being recalled. Customers were offered HP2000s in its place, or they could wait until fall 1973 for a revised version- that would only support 4-6 users, far fewer than marketing's initial promises of 64. This pleased no one, and it's reported that some customers cried over the disappointment.

With sales paused, the entire computer underwent a design overhaul. The resulting machine was faster and cheaper and could actually handle 8 simultaneous users. One year after the botched launch, the HP3000 went back on the market, and ended up being a full success.

It was so successful, HP continued supporting the HP3000 until 2010, which is where Janice enters our story. Circa 2006, she needed to update some Pascal code. That code used a lot of bit-masks to handle flags, which is normally a pretty easy function in Pascal- the language has a standard set of bitwise operations. So Janice was surprised to see:

FUNCTION BITON(A , B : INTEGER): BOOLEAN; VAR C : INTEGER; BEGIN CASE A OF 15 : C:=1; 14 : C:=2; 13 : C:=4; 12 : C:=8; 11 : C:=16; 10 : C:=32; 9 : C:=64; 8 : C:=128; 7 : C:=256; 6 : C:=512; 5 : C:=1024; 4 : C:=2048; 3 : C:=4096; 2 : C:=8192; 1 : C:=16384; 0 : C:=32768; OTHERWISE BITON:=FALSE; END; IF ((B DIV C) MOD 2) = 1 THEN BITON:=TRUE ELSE BITON:=FALSE; END; FUNCTION SETBITON(A, B : INTEGER) : INTEGER; VAR C : INTEGER; BEGIN CASE A OF 15 : C:=1; 14 : C:=2; 13 : C:=4; 12 : C:=8; 11 : C:=16; 10 : C:=32; 9 : C:=64; 8 : C:=128; 7 : C:=256; 6 : C:=512; 5 : C:=1024; 4 : C:=2048; 3 : C:=4096; 2 : C:=8192; 1 : C:=16384; 0 : C:=32768; OTHERWISE C:=0; END; IF NOT BITON(A,B) THEN SETBITON:=B + C ELSE SETBITON:=B; END; FUNCTION SETBITOFF(A, B : INTEGER) : INTEGER; VAR C : INTEGER; BEGIN CASE A OF 15 : C:=1; 14 : C:=2; 13 : C:=4; 12 : C:=8; 11 : C:=16; 10 : C:=32; 9 : C:=64; 8 : C:=128; 7 : C:=256; 6 : C:=512; 5 : C:=1024; 4 : C:=2048; 3 : C:=4096; 2 : C:=8192; 1 : C:=16384; 0 : C:=32768; OTHERWISE C:=0; END; IF BITON(A,B) THEN SETBITOFF:=B - C ELSE SETBITOFF:=B; END; FUNCTION LAND(A,B : INTEGER) : INTEGER; VAR I : INTEGER; BEGIN I:=0; REPEAT IF BITON(I,A) THEN IF BITON(I,B) THEN A:=SETBITON(I,A) ELSE A:=SETBITOFF(I,A) ELSE A:=SETBITOFF(I,A); I:=I + 1; UNTIL I > 15; LAND:=A; END;

This is a set of hand-reinvented bitwise operations, culminating in an LAND, which does a bitwise and (not a logical and, which makes it annoyingly misnamed). I wouldn't call the code a horrible approach to doing this, even if it's definitely an inefficient approach (and when you're running a 33 year old computer, efficiency matters), but absent built-in bitwise operations, I can't see a lot of other options. The biggest problem is that LAND will set bits on that are already on, which is unnecessary- an AND should really only ever turn bits off.

Which, as it turns out, is the root WTF. The developer responsible wasn't ignorant about bitwise operations. The version of Pascal that shipped on the HP3000 simply didn't have any. No and, or, not, or xor. Not even a shift-left or shift-right operation.

In any case, this is what happens when I start doing research on a story and end up getting sucked down a rabbit hole. As always, while Wikipedia's true value is as a bibliography. A lot of those links have much more detail, but I hope this quick overview was an interesting story.

[Advertisement] Continuously monitor your servers for configuration changes, and report when there's configuration drift. Get started with Otter today!
Categories: Computer

CodeSOD: Return Country

Wed, 2024-02-07 07:30

Let's say you have a database table containing a list of countries. Given the primary key of a country in that table- an arbitrary ID field- you need to look up the name of that country.

Curtis's predecessor dropped this solution:

function return_country($id) { $sql = "SELECT * FROM countries"; $qry = db_query($sql); if(mysql_num_rows($qry)>0){ while($row = mysql_fetch_assoc($qry)){ $a[$row['id']] = $row['name']; } }else{ return array(); } return $a[$id]; }

I guess they got the memo about not doing SQL injection flaws, but missed the "because you use query parameters". Instead, this queries the entire list of countries, iterates across them to build a mapping of ID to country name, and then uses that map to return the correct result.

This code really "shines" in its details. Sure, we could solve this with a query, but even if we opt to iterate across the table, we could just return from inside the loop. But no, we build an associative array.

And while it'd be better to return an error when the ID can't be found, we could return an empty string, but no- we return an empty array.

Return country? I'd rather return this code.

[Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.
Categories: Computer

CodeSOD: Max Character Width

Tue, 2024-02-06 07:30

One of the "features" of the Oracle database is that, in addition to the "wonderful" PL/SQL language for building stored procedures, you can also write stored procedures in Java.

Now, the skills of "being a good database programmer" and "being a good Java programmer" are not necessarily overlapping, especially when you're deep in the world of Oracle's approach to programming. Which is where this submission, from Tomas comes from.

private static final short m_maxCharWidth[][] = { { 1, 1 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 }, { 6, 1 }, { 7, 1 }, { 8, 1 }, { 9, 1 // ... some time later ... 9998, 3 }, { 9999, 3 } };

The first thing to note is that this 2D array uses its first dimension to store index+1. Maybe someone really liked Matlab and wanted 1-based indexes. It's hard to say for sure.

It's also hard to say what this is actually for. My first thought was that it was mapping numbers to their number of digits, for display purposes. Given the name, it seems likely- but that would mean they're using 3 characters to display 4 digit numbers, which would be wrong. Maybe it's mapping Unicode code points to the number of bytes they require to be represented? That doesn't jive either. Or perhaps it's (short)log(n)- a truncated log, but that would be wrong for all the single digit numbers. Our submitter snipped the 2 digit range, so I'm not sure what was inside of there, and that might answer the question.

But then again, why am I even trying to answer this question? This code just shouldn't exist.

.comment { border: none; } [Advertisement] Otter - Provision your servers automatically without ever needing to log-in to a command prompt. Get started today!
Categories: Computer

A Laboratory Upgrade

Mon, 2024-02-05 07:30

Unfortunately for Elena, the laboratory information management system (LIMS) her team used was being sunsetted by the vendor, Initech. She's a biochemist working in a pathology department, and this software is vital to tracking the tests need, the tests already run, and providing critical notifications about abnormal test results.

Since Initech was sunsetting its products, the hospital system put out an RFQ for a replacements, and after a multi-year bidding process, offered the contract for replacing the software to Initech.

And thus, IniLab went away, and was replaced by IniHeal. Gone was the classic terminal interface that everyone had learned to use, and in its place was a "user friendly" GUI- a GUI that buried functionality behind fifteen clicks, had no meaningful keyboard shortcuts, and was constantly changing as they pushed updates, making it impossible to adjust to.

Also gone was the IniLab scripting language. IniLab's scripting language was how they had assembled workflows for laboratory processes. You could have fine grained control of which scripts tied to which kinds of tests, and even build custom workflows as one-offs, if necessary. They had many thousands of lines of code that needed to be ported over into IniHeal. The problem was IniHeal's approach to scripting was… well, bad.

First, IniHeal doesn't allow you write scripts. Instead, it has a pair of "rule" files. The marketing copy calls these "declarative", and promises that they're an upgrade. In practice, these are gigantic files that contain a set of rules that are evaluated for every test and every sample.

But the worse problem in IniHeal is how it lets you handle data. In IniLab, if you wanted tho patient's age, you might reference the field by name: Patient.age. In IniLab, you need to use the PPPPLL. What is the PPPPLL?

PPPPLL stands for "position" and "length". Instead of referencing a field by name, you have to reference the field by its byte offset and length. So Patient.age might be [091001]. And no, there's no easy way to create constants or variables to hold your PPPPLLs, you just have to know that [091001] is age, and hope that the input data never changes its layout.

Now, one of the things Elena noticed while techs were on-site is that the length of the date time fields was 32 bits, and the data was seconds since the Unix epoch. So she asked one of the senior developers "Hey, is this software Year 2038 safe?"

The developer laughed. "I'll be retired by then."

The final insult to injury is that this software wasn't installed on any laboratory workstations. Instead, they had to use Remote Desktop to access it inside of a VM. While the environment was theoretically locked down so that the only program you could run was IniHeal, in practice it was trivially easy to get to a desktop inside the VM.

Curious, Elena poked around. She not only found the installation logs from her lab's deployment, but the installation logs from several other labs. These logs included various internal and private details, including account passwords. Somehow, log files from client installs had ended up in their installation media and were deployed to every client site.

Elena reported the potential data breach. The final finding was that the password in question was long defunct, which led management to decide "well, that's no problem then."

IniHeal has a slogan about equipping customers with innovative tools, but Elena proposes a new one:

"The Q in IniHeal is for Quality."

[Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.
Categories: Computer

Error'd: Groundhog Day

Fri, 2024-02-02 07:30

In this week's episode we have some more adventures in shipping and misadventures with dates. I just asked "Hey Google, how many days are there in February THIS year" and they answered confidently "28 days." I briefly considered autoplaying that query on this page to see how many of your devices would answer correctly, but then I decided I should autoplay a command to order me a pizza and then I thought better of the whole thing.

Following up on an earlier submission, Dave P. reports "Much to my amazement, the package did arrive in time for Christmas, but just barely. USPS really came through on their end, taking less than 2 days to deliver the package, after the original delivery service took 12 days to find the USPS dropoff facility." Hah! Somebody owes me a nickel!

 

Peter S. on the other hand, had a less-stellar experience with the competition. "Not sure if I can trust UPS ever again. They didn't just lose my package, but also their homepage."

 

Supporter of the arts Chris remark'd "I know February 2024 is supposed to be longer than usual, but I expected just one extra day, not over 330..."

 

Old-timer Barry M. wistfully remembers this movie released in a year that never existed. "man, that's a real classic," he said softly. Wasn't that movie title actually "The Hand that Rocks the Cradle?" Two error'ds in one!

 

Finally, a new anonymous (that is to say, previous anonymous submissions were from different anonymice) sent us a lovely photo of a hill and a fully charged battery, exclaiming "I don't think I was supposed to see this class name. I wonder if I could inject something by escaping strings in my message!" Be careful, it might be a trap.

 

[Advertisement] BuildMaster allows you to create a self-service release management platform that allows different teams to manage their applications. Explore how!
Categories: Computer

CodeSOD: A Well Known Address

Thu, 2024-02-01 07:30

Amanda's company wanted to restrict access to a service by filtering on the requestor's IP address. Yes, this is a terrible idea. So they wanted to make it a bit smarter, and also filter on various subnets. But they had a LOT of different subnets.

So the result was this:

ok = 0 ip = Request.ServerVariables("REMOTE_ADDR") if ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" or ip = "xxx.xxx.xxx.xxx" then ok = 1 end if ip2 = Split(ip,".") ip3 = ip2(0) &"."& ip2(1) &"."& ip2(2) if ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" then ok = 1 end if if ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" or ip3 = "xxx.xxx.xxx" then ok = 1 end if if ok = 1 then response.redirect "http://www.somedomain.com/something/that/is/meant/to/be/private" else response.redirect "index.asp?error=1" end if

Imagine that each xxx in there is part of an IP address. Whitespace as in original, apologies to your scrollbar.

This code is fairly old- classic ASP, but it was still in use as of a decade ago. Which, it happens, is when Amanda worked on it. She did the sane thing and deleted this block and just used the authentication system that the application already had. Customers were happy, as it meant they didn't need to get their IP address allowlisted, they could just sign in.

At least one manager was unhappy, because they were convinced that by allowlisting, they were enforcing a "per seat" license- "Every computer has a unique IP address!" they insisted. "Without this check, they could sign on from any computer, anywhere!"

Fortunately, that manager was eventually talked down when someone suggested that this gives each user their own account, and thus prevents two people from sharing the same computer.

[Advertisement] Otter - Provision your servers automatically without ever needing to log-in to a command prompt. Get started today!
Categories: Computer

CodeSOD: A Voice Map

Wed, 2024-01-31 07:30

Let's say your company wanted to offer special deals. When a customer calls about one of these deals, you want to play an automated customer support message using SignalWire, a tool for scripting phone voice trees.

This is a natural case for using a Map data structure. Which is what Ajay's predecessor did. They just… uh… weren't sure how to use a Map.

public String convert(Deal deal) { List<Deal> single = Collections.singletonList(deal); Map<Deal, String> swml = client.getSWMLs(single, version); for(Map.Entry<Deal, String> entry : swml.entrySet()) { if (deal.equals(entry.getKey())) { return entry.getValue(); } } return ""; }

This Java code looks like a case of trying to fit some APIs together awkwardly.

Our function takes a Deal object. The client.getSWMLs function apparently requires a list as its input, because we start by converting the input into a singletonList- a list with one item in it.

client.getSWMLs then returns a map, presumably mapping every input in the list to the appropriate SWML script. Of course, we only sent it one, so I suspect that our map only has one key. No problem, though, as we can just call get on the Map…

Except that's not what we do. We convert the Map into a Set of Map.Entrys, and then iterate across the set. If the Deal in the set is the Deal we're looking for, we've found our value.

Bonus points for simply returning an empty string instead of a meaningful error when the input doesn't map to an output. I'm sure that never created any awkward customer support moments.

[Advertisement] Continuously monitor your servers for configuration changes, and report when there's configuration drift. Get started with Otter today!
Categories: Computer

CodeSOD: 2019 Was a Fine Year

Tue, 2024-01-30 07:30

Efren's employer recently acquired a competitor. The competitor had been struggling for a number of years, and the acquisition was a last ditch attempt to preserve at least some of the business (a complete closure was the only other option).

Now, "struggling for a number of years" sounds fairly vague, but due to some bad database design, we actually have a clear indicator of exactly when the company gave up:

CASE YEAR(SomeDate) WHEN 2013 THEN SUM(Estimate2013) WHEN 2014 THEN SUM(Estimate2014) WHEN 2015 THEN SUM(Estimate2015) WHEN 2016 THEN SUM(Estimate2016) WHEN 2017 THEN SUM(Estimate2017) WHEN 2018 THEN SUM(Estimate2018) WHEN 2019 THEN SUM(Estimate2019) WHEN 2020 THEN SUM(Estimate2019) WHEN 2021 THEN SUM(Estimate2019) WHEN 2022 THEN SUM(Estimate2019) WHEN 2023 THEN SUM(Estimate2019) WHEN 2024 THEN SUM(Estimate2019) WHEN 2025 THEN SUM(Estimate2019) WHEN 2026 THEN SUM(Estimate2019) END

Here we have the classic "add a new column for every unit of time" design anti-pattern. A fun, traditional piece of difficult maintenance. But here we see exactly when maintenance stopped: sometime in late 2019 or 2020.

Now, I'm speculating a bit, but, I suspect this is pretty clear evidence of how the pandemic may have impacted some businesses. Someone was supposed to add the new column. They may have been laid off, or retasked. The maintenance task that had seemed so affordable to the business a year prior suddenly became untenable.

"Debts that can't be paid won't be paid," as the saying goes. Here's technical debt that couldn't be paid. So it wasn't. And the result was a company going under.

[Advertisement] Continuously monitor your servers for configuration changes, and report when there's configuration drift. Get started with Otter today!
Categories: Computer

CodeSOD: Bent Struts

Mon, 2024-01-29 07:30

Luke has inherited a Java Struts-based application. Struts is one of the many Java web frameworks, designed around having HTTP requests trigger actions- that is routing HTTP requests to a specific function call.

Now, on one screen, the user filled in a form, and then the corresponding action on the server side needed to read the form data for an eventId and act upon it. In Struts, this can be very simple:

int event_id = Integer.parseInt(request.getParameter("event_id"));

That's the assumed way of doing that. But if you don't learn how to use the framework properly, you might end up writing something else:

public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { int event_id = 0; try { InputStream is = request.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); String eventStr = ""; StringBuffer buff = new StringBuffer(); String line; do { line = br.readLine(); buff.append(line); } while (br.readLine() != null); eventStr = buff.toString(); StringTokenizer parseEventId = new StringTokenizer(eventStr, ","); while (parseEventId.hasMoreTokens()) { String eventString = parseEventId.nextToken(); if (eventString.startsWith("event_id")) { event_id = Integer.parseInt(eventString.substring(eventString.indexOf("=") + 1)); } } } catch (Exception e) { e.printStackTrace(); } ... }

This pile of code opts to read in the entire body of the input stream as a string, and then parse that string using a tokenizer, searching for substring which starts with event_id, at which point they can split on the = and get the integer value.

All of this is too complicated and reinventing a wheel badly, but the specific token we split on hints at deeper problems: ", ", as well as the fact that our read do/while loop only reads every other line.

An HTML form POST request encodes the data either as application/x-www-form-urlencoded or multipart/form-data. Neither of those formats sends up commas to separate key/value pairs. Either the client side is applying its own custom formatting, which we need to parse, or this code is just plain wrong.

But also, Struts does have a whole model/form binding feature set, so the "official" way to do this would be to just map to a Java Bean object.

Everything about this is wrong and overengineered, and smells like it was written by someone who was "smarter" than everyone else, and thus couldn't be bothered with using standard approaches to anything.

[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!
Categories: Computer

Error'd: Ride Ride Ride

Fri, 2024-01-26 07:30

File under "Old Business": Swissrail just can't catch a break.

Diligent Richard B. dug into the news and reports "After following the link from this week's Error'd, I came across this interesting description in Swissrail's Member list. The filename means 'Company description En(glish)'. They have the same thing in French; the German description is normal."

 

Allie C. didn't order this. "Sorting by views descending works perfectly with no issues at all!" I don't think we found a catchy name for this category yet. I'm leaning towards assortment.

 

Persistent Peter G. grumbles "This building company claims to have been in the business for a long time. By their testimonials I'd say at least two thousand years."

 

Faithful Adam R. comments "Comcast is having an outage in my area, and this time they're not even giving me a bad guess for the Estimated Time of Recovery."

 

Finally, conscientious Carly G. discovered a very disappointing windfall. "Cashfoward Bonus® would be more accurate. I wonder what happens if I spend it." Don't give it to me!

 

[Advertisement] Continuously monitor your servers for configuration changes, and report when there's configuration drift. Get started with Otter today!
Categories: Computer

Pages