Ara.T.Howard
2005-Mar-13 01:19 UTC
[BUG] time handling/impossible rails time verification situation
i''ve been looking into rails time verification because mysql time handling blows. this, of course, is the ''feb 31st'' issue which keeps cropping up on the rails list, but it stems from the system impl of mktime/localtime which ''normalizes'' (man mktime) bad dates like ''feb 31st'' silently into ''march 2rd''. eg. the underlying system calls ruby uses accept month=02 and day=31 as arguments. this is doccumented and un-likely to change. o.k. so this will always work in ruby (and c, python, etc.) : [ahoward@localhost ~]$ ruby -e''p(Time::mktime(2000, 02, 31))'' Thu Mar 02 00:00:00 MST 2000 it''s possible ruby core could be RCR''d so some sort of validating time method could be added, but that''s probably a pretty slippery method to wait for. so this is problem one standing in the way of verifying time input : ruby/c accepts fubar time input (well, fubar from a web app perspective). problem two: active_record/base.rb defines execute_callstack_for_multiparameter_attributes to parse out multiparamter form fields and reconstitue them into viable args to pass to a class constructor. fair enough (and kind of slick too) but this means by the time form input reaches models for verification it already IS a fubar time object and, thus, is valid any way you slice it. true, you could do something horrible in your controller to extract the multiparam fields and compare them against the year, month, day, etc. fields of your (already constituted) Time object - but that''s getting silly. so what to do? i''ve come up with the following heuristic: if i pass a bunch of values into Time::local, and they wrap, i''ve got bad input. eg. if Time::local was passed [2005, 2, 31, 0, 0, 0, 0] and ends up with time.month #=> 3 then the system mktime/localtime has normalized me and i can assume that the input was fubar (out of ''normal'' human ranges). o.k. - how to do that. i can think of a couple ways at the moment: 1) hack a check into execute_callstack_for_multiparameter_attributes, for instance something like --- active_record/base.rb.org 2005-03-12 16:58:20.869614941 -0700 +++ active_record/base.rb 2005-03-12 17:39:31.648893648 -0700 @@ -1284,6 +1284,15 @@ send(name + "=", nil) else begin + value + if Time == klass + t = klass.local(*values) + tvalues = %w(year month day hour min sec usec).map{|m| t.send m} + values.zip(tvalues){|tin,tout| raise RangeError unless tin != tout} + t + else + klass.new(*values) + end send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values)) rescue => ex errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name) basically this just checks that the year, month, day, etc. input into Time::local came out the other side - eg. it did not get ''normalized''. this is _alright_ but still leaves a million other places in rails where one could do party = 1999 Time::local party, 2, 31 and either mean it - or not mean it. 2) supplant the built-in Time::local (and mktime, gm, utc) to do something similar after alias''ing the originals. alternatively rails could add new time methods with this behavior (validating) and use them where it''s obvious they should be (unpacking form input for example). 3) provide hooks to access unpacked form fields as arrays so these can be checked. this still requires quite a bit additional coding in the case of times though... i HOPE i''m missing something and there''s an elegant solution to this, but so far reading the source hasn''t really yielded much insight... thoughts? -a -- ==============================================================================| EMAIL :: Ara [dot] T [dot] Howard [at] noaa [dot] gov | PHONE :: 303.497.6469 | When you do something, you should burn yourself completely, like a good | bonfire, leaving no trace of yourself. --Shunryu Suzuki ===============================================================================